/* * JBoss, Home of Professional Open Source * Copyright 2008, Red Hat Middleware LLC, and individual contributors * by the @authors tag. See the copyright.txt in the distribution for a * full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.mobicents.ipbx.session.registrar; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.persistence.EntityManager; import javax.servlet.ServletException; import javax.servlet.sip.Address; import javax.servlet.sip.ServletParseException; import javax.servlet.sip.SipFactory; import javax.servlet.sip.SipServletRequest; import javax.servlet.sip.SipServletResponse; import javax.servlet.sip.SipSession; import javax.servlet.sip.SipURI; import javax.servlet.sip.URI; import org.jboss.seam.ScopeType; import org.jboss.seam.annotations.In; import org.jboss.seam.annotations.Logger; import org.jboss.seam.annotations.Name; import org.jboss.seam.annotations.Observer; import org.jboss.seam.annotations.Out; import org.jboss.seam.annotations.Scope; import org.jboss.seam.annotations.Transactional; import org.jboss.seam.core.Events; import org.jboss.seam.log.Log; import org.mobicents.ipbx.entity.Binding; import org.mobicents.ipbx.entity.Registration; import org.mobicents.ipbx.entity.User; import org.mobicents.ipbx.session.DataLoader; import org.mobicents.ipbx.session.call.model.WorkspaceStateManager; import org.mobicents.ipbx.session.configuration.PbxConfiguration; import org.mobicents.ipbx.session.util.URIUtil; /** * The registrar service as defined per RFC 3261, Section 10.3 * * TODO have a background timer task checking for unused bindings * * @author jean.deruelle@gmail.com * @author Thomas Leseney from Nexcom Systems */ @Name("registrarService") @Scope(ScopeType.STATELESS) @Transactional public class RegistrarService { public static final String CONTACT_HEADER = "Contact"; public static final String MIN_EXPIRES_HEADER = "Min-Expires"; public static final String CSEQ_HEADER = "CSeq"; public static final String DATE_HEADER = "Date"; private String _255 = "\\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)"; private String privateIPRegex ="(?:10"+_255+_255+_255+")|(?:172\\.1[6-9]"+_255+_255+")" + "|(?:172\\.2[0-9]"+_255+_255+")|(?:172\\.3[0-1]"+_255+_255+")|(192\\.168"+_255+_255+")"; private Pattern privateIPpattern =Pattern.compile(privateIPRegex); private int minExpires = 60; private int maxExpires = 86400; private int defaultExpires = 3600; @Logger Log log; @In EntityManager entityManager; @In DataLoader dataLoader; @In SipFactory sipFactory; @In(required=true) SipSession sipSession; @In(required=false) @Out(required=false) User user; private java.text.DateFormat dateFormat; public RegistrarService() { dateFormat = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss z", Locale.US); dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); } @Observer("REGISTER") public void doRegister(SipServletRequest request) throws ServletException, IOException { if(log.isDebugEnabled()) { log.debug("Registrar received register request: " + request); } int response = SipServletResponse.SC_OK; SipServletResponse resp = request.createResponse(response); // TODO RFC3261, Section 10.3 : A registrar has to know the set // of domain(s) for which it maintains bindings : get the set of domains we maintain the registration // from the overall pbx configuration // TODO RFC 3261, Section 10.3.1 : The registrar inspects the Request-URI to determine whether it // has access to bindings for the domain identified in the Request-URI. If not, and if the server // also acts as a proxy server, the server SHOULD forward the request to the addressed domain, // following the general behavior for proxying messages described in Section 16 // TODO RFC3261, Section 10.3.2 : To guarantee that the registrar supports any necessary // extensions, the registrar MUST process the Require header field values as described for UASs in Section 8.2.2. // TODO RFC3261, Section 10.3.3 : A registrar SHOULD authenticate the UAC. Mechanisms for the // authentication of SIP user agents are described in Section 22. Registration behavior in no way overrides the generic // authentication framework for SIP. If no authentication mechanism is available, the registrar MAY take the From address // as the asserted identity of the originator of the request. // TODO RFC3261, Section 10.3.4 : The registrar SHOULD determine if the authenticated user is // authorized to modify registrations for this address-of-record. For example, a registrar might consult an authorization // database that maps user names to a list of addresses-of-record for which that user has authorization to modify bindings. If // the authenticated user is not authorized to modify bindings, the registrar MUST return a 403 (Forbidden) and skip the // remaining steps. try { Registration registration = processAddressOfRecord(request); // RC3261, Section 10.3.6 : The registrar checks whether the request contains the Contact header field. // If not, it skips to the last step. Iterator<Address> it = request.getAddressHeaders(CONTACT_HEADER); if(it != null) { if (it.hasNext()) { List<Address> contacts = new ArrayList<Address>(); boolean wildcard = false; // RC3261, Section 10.3.6 : If the Contact header field is present, the registrar checks if there // is one Contact field value that contains the special value "*" and an Expires field. // If the request has additional Contact fields or an expiration time other than zero, the request is // invalid, and the server MUST return a 400 (Invalid Request) and skip the remaining steps. while (it.hasNext()) { Address contact = it.next(); if(contact.getURI().toString().contains("0.0.0.0")) continue; if (contact.isWildcard()) { wildcard = true; if (it.hasNext() || contacts.size() > 0 || request.getExpires() > 0) { throw new BadRegistrationException(SipServletResponse.SC_BAD_REQUEST, "invalid wildcard"); } } contacts.add(contact); if(contact.getURI().isSipURI()){ log.debug("found sip contact with URI "+ contact.getURI().toString()); Matcher ipMatcher =privateIPpattern.matcher(((SipURI)contact.getURI()).getHost()); if(ipMatcher.matches()){ int receiveFromIndex = request.getHeader("Via").indexOf("received=")+9; int receiveToIndex = request.getHeader("Via").indexOf(";",receiveFromIndex); String publicIP = request.getHeader("Via").substring(receiveFromIndex); if(receiveToIndex>-1){ publicIP = request.getHeader("Via").substring(receiveFromIndex,receiveToIndex); } int rportFromIndex = request.getHeader("Via").indexOf("rport=")+6; int rportToIndex = request.getHeader("Via").indexOf(";",rportFromIndex); int publicPort = -1; if(rportToIndex>-1){ publicPort = Integer.parseInt(request.getHeader("Via").substring(rportFromIndex,rportToIndex)); } else { publicPort = Integer.parseInt(request.getHeader("Via").substring(rportFromIndex)); } log.info("the contact URI is private, we replace its host by "+publicIP+", Via ='"+request.getHeader("Via")+"' ,receiveFromIndex ="+receiveFromIndex+", receiveToIndex ="+receiveToIndex); ((SipURI)contact.getURI()).setHost(publicIP); log.info("and we replace its port ("+((SipURI)contact.getURI()).getPort()+") by"+publicPort); ((SipURI)contact.getURI()).setPort(publicPort); log.info("new contact URI :"+contact.getURI().toString()); } } } processContacts(request, wildcard, registration, contacts); } } // RFC3261, Section 10.3.8 : The registrar returns a 200 (OK) response. // The response MUST contain Contact header field values enumerating all current bindings. // Each Contact value MUST feature an "expires" parameter indicating its expiration interval chosen by the registrar. // The response SHOULD include a Date header field. Set<Binding> bindings = registration.getBindings(); if (bindings != null) { for (Binding binding : bindings) { resp.addHeader(CONTACT_HEADER, "<" + binding.getContactAddress() + ">;expires=" + binding.getExpires()); } } resp.addHeader(DATE_HEADER, dateFormat.format(new Date())); resp.send(); } catch (BadRegistrationException e) { log.error("the registration concerning the request " + request + " is wrong, cause : " + e.getReason()); resp = request.createResponse(e.getStatus(), e.getReason()); resp.send(); } } // The binding updates MUST be committed (that is, made visible to the proxy or redirect server) if and only if all binding // updates and additions succeed. If any one of them fails (for example, because the back-end database commit failed), the // request MUST fail with a 500 (Server Error) response and all tentative binding updates MUST be removed. private void processContacts(SipServletRequest request, boolean wildcard, Registration registration, List<Address> contacts) throws BadRegistrationException { String callId = request.getCallId(); String cseqHeaderValue = request.getHeader(CSEQ_HEADER); int cseq = Integer.parseInt(cseqHeaderValue.substring(0, 1)); Set<Binding> bindings = registration.getBindings(); if (wildcard) { for (Binding binding : bindings) { if (callId.equals(binding.getCallId()) && cseq < binding.getCSeq()) { throw new BadRegistrationException(SipServletResponse.SC_SERVER_INTERNAL_ERROR, "lower cseq"); } registration.removeBinding(binding); } } else { for (Address contact : contacts) { int expires = getContactExpiresValue(request, contact); Binding binding = findBinding(bindings, contact); if(binding != null) { updateOrRemoveExistingBinding(request, registration, callId, cseq, contact, expires, binding); } else if (expires != 0) { createBinding(registration, callId, cseq, contact, expires); } } } WorkspaceStateManager.instance().getWorkspace(registration.getUser().getName()).makeRegistrationsDirty(); } /** * If the binding does exist, the registrar checks the Call-ID value. * This algorithm ensures that out-of-order requests from the same UA are ignored. * Each binding record records the Call-ID and CSeq values from the request. * * If the Call-ID value in the existing binding differs from the Call-ID value in the request, the binding MUST be removed if * the expiration time is zero and updated otherwise. * * If they are the same, the registrar compares the CSeq value. If the value * is higher than that of the existing binding, it MUST update or remove the binding as above. If not, the update MUST be * aborted and the request fails. * * @param request the request used to construct the response if update must be aborted * @param registration the registration on which the binding update or removal should be performed * @param callId the new callId * @param cseq the new cseq * @param contact the new contact address * @param expires the expires * @param binding the binding to update or remove * @throws BadRegistrationException if an update should be aborted */ private void updateOrRemoveExistingBinding( SipServletRequest request, Registration registration, String callId, int cseq, Address contact, int expires, Binding binding) throws BadRegistrationException { if (!callId.equals(binding.getCallId())) { // If the Call-ID value in the existing binding differs from the Call-ID value in the request, the binding MUST be removed if // the expiration time is zero and updated otherwise. if (expires == 0) { removeBinding(registration, binding); } else { updateBinding(registration, callId, cseq, contact, expires, binding); } } // If they are the same, the registrar compares the CSeq value. If the value // is higher than that of the existing binding, it MUST update or remove the binding as above. If not, the update MUST be // aborted and the request fails. else if (cseq > binding.getCSeq()) { if (expires == 0) { removeBinding(registration, binding); } else { updateBinding(registration, callId, cseq, contact, expires, binding); } } else if (cseq < binding.getCSeq()) { throw new BadRegistrationException(SipServletResponse.SC_SERVER_INTERNAL_ERROR, "lower cseq"); } } /** * Create and add a binding * @param registration the registration where the new binding should be added * @param callId the callId * @param cseq the cseq * @param contact the contact address * @param expires the expires for the given contact */ private void createBinding(Registration registration, String callId, int cseq, Address contact, int expires) { Binding newBinding = new Binding(); newBinding.setContactAddress(contact.getURI().toString()); newBinding.setCallId(callId); newBinding.setCSeq(cseq); newBinding.setExpires(expires); newBinding.setRegistration(registration); registration.addBinding(newBinding); entityManager.persist(newBinding); if(log.isDebugEnabled()) { log.debug("Added binding: " + newBinding); } } /** * Update a binding * @param registration * @param callId * @param cseq * @param contact * @param expires * @param binding */ private void updateBinding(Registration registration, String callId, int cseq, Address contact, int expires, Binding binding) { binding.setContactAddress(contact.getURI().toString()); binding.setCallId(callId); binding.setCSeq(cseq); binding.setExpires(expires); registration.updateBinding(binding); entityManager.persist(binding); if(log.isInfoEnabled()) { log.info("Updated binding: " + binding); } } /** * Remove a binding * @param registration * @param binding */ private void removeBinding(Registration registration, Binding binding) { registration.removeBinding(binding); entityManager.remove(binding); if(log.isInfoEnabled()) { log.info("Removed binding: " + binding); } } /** * For each address, the registrar then searches the list of current bindings using the URI comparison rules. * If the binding does not exist, it is tentatively added. * @param bindings the bindings to look into for the contact * @param contact the contact to check against the current bindings * @return the binding found, null otherwise */ private Binding findBinding(Set<Binding> bindings, Address contact) { if(bindings == null) return null; for (Binding binding : bindings) { URI contactUri = null; try { contactUri = sipFactory.createURI(binding.getContactAddress()); } catch (ServletParseException e) { log.error("Invalid URI present in the registrar: " + binding.getContactAddress(), e); // TODO shall we remove the binding in this case ? return null; } if (contact.getURI().equals(contactUri)) { return binding; } } return null; } /** * RFC3261, Section 10.3.5 : The registrar extracts the address-of-record from the To header * field of the request. If the address-of-record is not valid for the domain in the Request-URI, * the registrar MUST send a 404 (Not Found) response and skip the remaining steps. The URI * MUST then be converted to a canonical form. To do that, all URI parameters MUST be removed (including the user-param), and * any escaped characters MUST be converted to their unescaped form. * The result serves as an index into the list of bindings. * @param request the REGISTER request used to extract the address of record and to construct the 404 Not Found * @return the registration if it has been found, null otherwise * @throws IOException if the 404 response could not be sent */ private Registration processAddressOfRecord(SipServletRequest request) throws BadRegistrationException { URI toURI = request.getTo().getURI(); String addressOfRecord = URIUtil.toCanonical(toURI); Registration registration = findRegistration(addressOfRecord); if(registration == null) { String strict = PbxConfiguration.getProperty("pbx.registration.strict"); if("true".equals(strict)) { throw new BadRegistrationException(SipServletResponse.SC_NOT_FOUND, "Address of Record not found"); } if(request.getFrom().getURI() instanceof SipURI) { SipURI uri = (SipURI) request.getFrom().getURI(); String user = uri.getUser(); registration = findNotStrictRegistration(user, uri.toString()); if(registration == null) { throw new BadRegistrationException(SipServletResponse.SC_NOT_FOUND, "Address of Record not found"); } //Events.instance().raiseAsynchronousEvent("globalSettingsChanged", (Object[]) null); } } return registration; } /** * RFC 3261, Section 10.3.7 : The registrar determines the expiration interval as follows: * - If the field value has an "expires" parameter, that value MUST be taken as the requested expiration. * - If there is no such parameter, but the request has an Expires header field, that value MUST be taken as the requested expiration. * - If there is neither, a locally-configured default value MUST be taken as the requested expiration. * @param request the request used to create the response * @param contact the contact address to check * @return -1 if no expires was found, otherwise the value * @throws BadRegistrationException if the 423 Interval too Brief response should be sent */ private int getContactExpiresValue(SipServletRequest request, Address contact) throws BadRegistrationException { int expires = contact.getExpires(); if (expires < 0) { expires = request.getExpires(); } // The registrar MAY choose an expiration less than the requested // expiration interval. If and only if the requested expiration // interval is greater than zero AND smaller than one hour AND // less than a registrar-configured minimum, the registrar MAY // reject the registration with a response of 423 (Interval Too // Brief). This response MUST contain a Min-Expires header field // that states the minimum expiration interval the registrar is // willing to honor. It then skips the remaining steps. if (expires != 0) { if (expires < 0) { expires = defaultExpires; } if (expires > maxExpires) { expires = maxExpires; } else if (expires < minExpires) { throw new BadRegistrationException(SipServletResponse.SC_INTERVAL_TOO_BRIEF); } } return expires; } public Registration findRegistration(String uri) { List<Registration> registrations = entityManager.createQuery( "SELECT registration FROM Registration registration WHERE registration.uri = :requestUri") .setParameter("requestUri", uri).getResultList(); if(registrations.size() <= 0) return null; Registration reg = registrations.get(0); User user = reg.getUser(); if(user == null) return null; this.user = user; sipSession.setAttribute("user", user); return reg; } public Registration findNotStrictRegistration(String name, String uri) { List<User> users = entityManager.createQuery( "SELECT user FROM User user WHERE user.name = :name") .setParameter("name", name).getResultList(); if(users.size() <= 0) return null; User user = users.get(0); Registration reg = new Registration(); reg.setUser(user); reg.setUri(uri); user.getRegistrations().add(reg); entityManager.persist(reg); sipSession.setAttribute("user", user); return reg; } }