/* * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client. * * Distributable under LGPL license. * See terms of license at gnu.org. */ package net.java.sip.communicator.impl.protocol.sip; import gov.nist.javax.sip.stack.*; import java.util.*; import javax.sip.*; import javax.sip.address.*; import javax.sip.header.*; import javax.sip.message.*; import net.java.sip.communicator.service.protocol.*; import net.java.sip.communicator.util.*; /** * This class is the <tt>SipListener</tt> for all JAIN-SIP * <tt>SipProvider</tt>s. It is in charge of dispatching the received messages * to the suitable <tt>ProtocolProviderServiceSipImpl</tt>s registered with * <tt>addSipListener</tt>. It also contains the JAIN-SIP pieces which are * common between all <tt>ProtocolProviderServiceSipImpl</tt>s (namely 1 * <tt>SipStack</tt>, 2 <tt>SipProvider</tt>s, 3 <tt>ListeningPoint</tt>s). * * @author Emil Ivov * @author Lubomir Marinov * @author Alan Kelly * @author Sebastien Mazy */ public class SipStackSharing implements SipListener { /** * We set a custom parameter in the contact address for registrar accounts, * so as to ease dispatching of incoming requests in case several accounts * have the same username in their contact address, eg: * sip:username@192.168.0.1:5060;transport=udp;registering_acc=example_com */ public static final String CONTACT_ADDRESS_CUSTOM_PARAM_NAME = "registering_acc"; /** * Logger for this class. */ private static final Logger logger = Logger.getLogger(SipStackSharing.class); /** * Our SIP stack (provided by JAIN-SIP). */ private final SipStack stack; /** * The JAIN-SIP provider that we use for clear UDP/TCP. */ private SipProvider clearJainSipProvider = null; /** * * The JAIN-SIP provider that we use for TLS. */ private SipProvider secureJainSipProvider = null; /** * The candidate recipients to choose from when dispatching messages * received from one the JAIN-SIP <tt>SipProvider</tt>-s. for thread safety * issues reasons, better iterate on a copy of that set using * <tt>getSipListeners()</tt>. */ private final Set<ProtocolProviderServiceSipImpl> listeners = new HashSet<ProtocolProviderServiceSipImpl>(); /** * The property indicating the preferred UDP and TCP * port to bind to for clear communications. */ public static final String PREFERRED_CLEAR_PORT_PROPERTY_NAME = "net.java.sip.communicator.SIP_PREFERRED_CLEAR_PORT"; /** * The property indicating the preferred TLS (TCP) * port to bind to for secure communications. */ private static final String PREFERRED_SECURE_PORT_PROPERTY_NAME = "net.java.sip.communicator.SIP_PREFERRED_SECURE_PORT"; /** * Constructor for this class. Creates the JAIN-SIP stack. */ SipStackSharing() throws OperationFailedException { // init of the stack try { SipFactory sipFactory = SipFactory.getInstance(); sipFactory.setPathName("gov.nist"); Properties sipStackProperties = new SipStackProperties(); // Create SipStack object this.stack = sipFactory.createSipStack(sipStackProperties); logger.trace("Created stack: " + this.stack); // set our custom address resolver managing SRV records AddressResolverImpl addressResolver = new AddressResolverImpl(); ((SIPTransactionStack) this.stack) .setAddressResolver(addressResolver); } catch(Exception ex) { logger.fatal("Failed to get SIP Factory.", ex); throw new OperationFailedException("Failed to get SIP Factory" , OperationFailedException.INTERNAL_ERROR , ex); } } /** * Adds this <tt>listener</tt> as a candidate recipient for the dispatching * of new messages received from the JAIN-SIP <tt>SipProvider</tt>s. * * @param listener a new possible target for the dispatching process. */ public void addSipListener(ProtocolProviderServiceSipImpl listener) throws OperationFailedException { synchronized(this.listeners) { if(this.listeners.size() == 0) startListening(); this.listeners.add(listener); logger.trace(this.listeners.size() + " listeners now"); } } /** * This <tt>listener</tt> will no longer be a candidate recipient for the * dispatching of new messages received from the JAIN-SIP * <tt>SipProvider</tt>s. * * @param listener possible target to remove for the dispatching process. */ public void removeSipListener(ProtocolProviderServiceSipImpl listener) { synchronized(this.listeners) { this.listeners.remove(listener); int listenerCount = listeners.size(); logger.trace(listenerCount + " listeners left"); if(listenerCount == 0) stopListening(); } } /** * Returns a copy of the <tt>listeners</tt> (= candidate recipients) set. * * @return a copy of the <tt>listeners</tt> set. */ private Set<ProtocolProviderServiceSipImpl> getSipListeners() { synchronized(this.listeners) { return new HashSet<ProtocolProviderServiceSipImpl>(this.listeners); } } /** * Returns the JAIN-SIP <tt>ListeningPoint</tt> associated to the given * transport string. * * @param transport a string like "UDP", "TCP" or "TLS". * @return the LP associated to the given transport. */ public ListeningPoint getLP(String transport) { ListeningPoint lp; Iterator<ListeningPoint> it = this.stack.getListeningPoints(); while(it.hasNext()) { lp = it.next(); // FIXME: JAIN-SIP stack is not consistent with case // (reported upstream) if(lp.getTransport().toLowerCase().equals(transport.toLowerCase())) return lp; } throw new IllegalArgumentException("Invalid transport: " + transport); } /** * Put the stack in a state where it can receive data on three UDP/TCP ports * (2 for clear communication, 1 for TLS). That is to say create the related * JAIN-SIP <tt>ListeningPoint</tt>s and <tt>SipProvider</tt>s. */ private void startListening() throws OperationFailedException { try { int bindRetriesValue = getBindRetriesValue(); this.createProvider(this.getPreferredClearPort() , bindRetriesValue, false); this.createProvider(this.getPreferredSecurePort() , bindRetriesValue, true); this.stack.start(); logger.trace("started listening"); } catch(Exception ex) { logger.error("An unexpected error happened while creating the" + "SipProviders and ListeningPoints."); throw new OperationFailedException("An unexpected error hapenned" + "while initializing the SIP stack" , OperationFailedException.INTERNAL_ERROR , ex); } } /** * Attach JAIN-SIP <tt>SipProvider</tt> and <tt>ListeningPoint</tt> to the * stack either for clear communications or TLS. Clear UDP and TCP * <tt>ListeningPoint</tt>s are not handled separately as the former is a * fallback for the latter (depending on the size of the data transmitted). * Both <tt>ListeningPoint</tt>s must be bound to the same address and port * in order for the related <tt>SipProvider</tt> to be created. If a UDP or * TCP <tt>ListeningPoint</tt> cannot bind, retry for both on another port. * * @param preferredPort which port to try first to bind. * @param retries how many times should we try to find a free port to bind * @param secure whether to create the TLS SipProvider. * or the clear UDP/TCP one. */ private void createProvider(int preferredPort, int retries, boolean secure) throws TransportNotSupportedException , InvalidArgumentException , ObjectInUseException , TransportAlreadySupportedException , TooManyListenersException { String context = (secure ? "TLS: " : "clear UDP/TCP: "); if(retries < 0) { // very unlikely to happen with the default 50 retries logger.error(context + "couldn't find free ports to listen on."); return; } ListeningPoint tlsLP = null; ListeningPoint udpLP = null; ListeningPoint tcpLP = null; try { if(secure) { tlsLP = this.stack.createListeningPoint( NetworkUtils.IN_ADDR_ANY , preferredPort , ListeningPoint.TLS); logger.trace("TLS secure ListeningPoint has been created."); this.secureJainSipProvider = this.stack.createSipProvider(tlsLP); this.secureJainSipProvider.addSipListener(this); } else { udpLP = this.stack.createListeningPoint( NetworkUtils.IN_ADDR_ANY , preferredPort , ListeningPoint.UDP); tcpLP = this.stack.createListeningPoint( NetworkUtils.IN_ADDR_ANY , preferredPort , ListeningPoint.TCP); logger.trace("UDP and TCP clear ListeningPoints have " + "been created."); this.clearJainSipProvider = this.stack.createSipProvider(udpLP); this.clearJainSipProvider. addListeningPoint(tcpLP); this.clearJainSipProvider.addSipListener(this); } logger.trace(context + "SipProvider has been created."); } catch(InvalidArgumentException ex) { // makes sure we didn't leave an open listener // as both UDP and TCP listener have to bind to the same port if(tlsLP != null) this.stack.deleteListeningPoint(tlsLP); if(udpLP != null) this.stack.deleteListeningPoint(udpLP); if(tcpLP != null) this.stack.deleteListeningPoint(tcpLP); // FIXME: "Address already in use" is not working // as ex.getMessage() displays in the locale language in SC // (getMessage() is always supposed to be English though) // this should be a temporary workaround //if (ex.getMessage().indexOf("Address already in use") != -1) // another software is probably using the port if(ex.getCause() instanceof java.io.IOException) { logger.debug("Port " + preferredPort + " seems in use for either TCP or UDP."); // tries again on a new random port int currentlyTriedPort = NetworkUtils.getRandomPortNumber(); logger.debug("Retrying bind on port " + currentlyTriedPort); this.createProvider(currentlyTriedPort, retries-1, secure); } else throw ex; } } /** * Put the JAIN-SIP stack in a state where it cannot receive any data and * frees the network ports used. That is to say remove JAIN-SIP * <tt>ListeningPoint</tt>s and <tt>SipProvider</tt>s. */ private void stopListening() { try { this.secureJainSipProvider.removeSipListener(this); this.stack.deleteSipProvider(this.secureJainSipProvider); this.secureJainSipProvider = null; this.clearJainSipProvider.removeSipListener(this); this.stack.deleteSipProvider(this.clearJainSipProvider); this.clearJainSipProvider = null; Iterator<ListeningPoint> it = this.stack.getListeningPoints(); while(it.hasNext()) { this.stack.deleteListeningPoint(it.next()); it = this.stack.getListeningPoints(); } this.stack.stop(); logger.trace("stopped listening"); } catch(ObjectInUseException ex) { logger.fatal("Failed to stop listening", ex); } } /** * Returns the JAIN-SIP <tt>SipProvider</tt> in charge of this * <tt>transport</tt>. * * @param transport a <tt>String</tt> like "TCP", "UDP" or "TLS" * @return the corresponding <tt>SipProvider</tt> */ public SipProvider getJainSipProvider(String transport) { SipProvider sp = null; if(transport.equalsIgnoreCase(ListeningPoint.UDP) || transport.equalsIgnoreCase(ListeningPoint.TCP)) sp = this.clearJainSipProvider; else if(transport.equalsIgnoreCase(ListeningPoint.TLS)) sp = this.secureJainSipProvider; if(sp == null) throw new IllegalArgumentException("invalid transport"); return sp; } /** * Fetches the preferred UDP and TCP port for clear communications in the * user preferences or fallback on a default value. * * @return the preferred network port for clear communications. */ private int getPreferredClearPort() { return SipActivator.getConfigurationService().getInt( PREFERRED_CLEAR_PORT_PROPERTY_NAME, ListeningPoint.PORT_5060); } /** * Fetches the preferred TLS (TCP) port for secure communications in the * user preferences or fallback on a default value. * * @return the preferred network port for secure communications. */ private int getPreferredSecurePort() { return SipActivator.getConfigurationService().getInt( PREFERRED_SECURE_PORT_PROPERTY_NAME, ListeningPoint.PORT_5061); } /** * Fetches the number of times to retry when the binding of a JAIN-SIP * <tt>ListeningPoint</tt> fails. Looks in the user preferences or * fallbacks on a default value. * * @return the number of times to retry a failed bind. */ private int getBindRetriesValue() { return SipActivator.getConfigurationService().getInt( ProtocolProviderService.BIND_RETRIES_PROPERTY_NAME, ProtocolProviderService.BIND_RETRIES_DEFAULT_VALUE); } /** * Dispatches the event received from a JAIN-SIP <tt>SipProvider</tt> to one * of our "candidate recipient" listeners. * * @param event the event received for a * <tt>SipProvider</tt>. */ public void processDialogTerminated(DialogTerminatedEvent event) { try { ProtocolProviderServiceSipImpl recipient = (ProtocolProviderServiceSipImpl) SipApplicationData .getApplicationData(event.getDialog(), SipApplicationData.KEY_SERVICE); if(recipient == null) { logger.error("Dialog wasn't marked, please report this to " + "dev@sip-communicator.dev.java.net"); } else { logger.trace("service was found with dialog data"); recipient.processDialogTerminated(event); } } catch(Throwable exc) { //any exception thrown within our code should be caught here //so that we could log it rather than interrupt stack activity with //it. this.logApplicationException(DialogTerminatedEvent.class, exc); } } /** * Dispatches the event received from a JAIN-SIP <tt>SipProvider</tt> to one * of our "candidate recipient" listeners. * * @param event the event received for a <tt>SipProvider</tt>. */ public void processIOException(IOExceptionEvent event) { try { logger.trace(event); // impossible to dispatch, log here logger.debug("@todo implement processIOException()"); } catch(Throwable exc) { //any exception thrown within our code should be caught here //so that we could log it rather than interrupt stack activity with //it. this.logApplicationException(DialogTerminatedEvent.class, exc); } } /** * Dispatches the event received from a JAIN-SIP <tt>SipProvider</tt> to one * of our "candidate recipient" listeners. * * @param event the event received for a <tt>SipProvider</tt>. */ public void processRequest(RequestEvent event) { try { logger.trace("received request: " + event.getRequest().getMethod()); Request request = event.getRequest(); // Create the transaction if it doesn't exist yet. If it is a // dialog-creating request, the dialog will also be automatically // created by the stack. if (event.getServerTransaction() == null) { try { SipProvider source = (SipProvider) event.getSource(); ServerTransaction transaction = source.getNewServerTransaction(request); // Update the event, otherwise getServerTransaction() and // getDialog() will still return their previous value. event = new RequestEvent(source, transaction, transaction.getDialog(), request); } catch (SipException ex) { logger.error("couldn't create transaction, please report " + "this to dev@sip-communicator.dev.java.net", ex); } } ProtocolProviderServiceSipImpl service = getServiceData(event.getServerTransaction()); if (service != null) { service.processRequest(event); } else { service = findTargetFor(request); if (service == null) { logger.error("couldn't find a ProtocolProviderServiceSipImpl " +"to dispatch to"); } else { // Mark the dialog for the dispatching of later in-dialog // requests. If there is no dialog, we need to mark the request // to dispatch a possible timeout when sending the response. if (event.getDialog() != null) { SipApplicationData.setApplicationData(event.getDialog(), SipApplicationData.KEY_SERVICE, service); } else { SipApplicationData.setApplicationData(request, SipApplicationData.KEY_SERVICE, service); } service.processRequest(event); } } } catch(Throwable exc) { //any exception thrown within our code should be caught here //so that we could log it rather than interrupt stack activity with //it. this.logApplicationException(DialogTerminatedEvent.class, exc); } } /** * Dispatches the event received from a JAIN-SIP <tt>SipProvider</tt> to one * of our "candidate recipient" listeners. * * @param responseEvent the event received for a <tt>SipProvider</tt>. */ public void processResponse(ResponseEvent event) { try { // we don't have to accept the transaction since we //created the request ClientTransaction transaction = event.getClientTransaction(); logger.trace("received response: " + event.getResponse().getStatusCode() + " " + event.getResponse().getReasonPhrase()); ProtocolProviderServiceSipImpl service = getServiceData(transaction); if (service != null) { // Mark the dialog for the dispatching of later in-dialog // responses. If there is no dialog then the initial request // sure is marked otherwise we won't have found the service with // getServiceData(). The request has to be marked in case we // receive one more response in an out-of-dialog transaction. if (event.getDialog() != null) { SipApplicationData.setApplicationData(event.getDialog(), SipApplicationData.KEY_SERVICE, service); } service.processResponse(event); } else { logger.error("We received a response which " + "wasn't marked, please report this to " + "dev@sip-communicator.dev.java.net"); } } catch(Throwable exc) { //any exception thrown within our code should be caught here //so that we could log it rather than interrupt stack activity with //it. this.logApplicationException(DialogTerminatedEvent.class, exc); } } /** * Dispatches the event received from a JAIN-SIP <tt>SipProvider</tt> to one * of our "candidate recipient" listeners. * * @param timeoutEvent the event received for a <tt>SipProvider</tt>. */ public void processTimeout(TimeoutEvent event) { try { Transaction transaction; if (event.isServerTransaction()) { transaction = event.getServerTransaction(); } else { transaction = event.getClientTransaction(); } ProtocolProviderServiceSipImpl recipient = getServiceData(transaction); if (recipient == null) { logger.error("We received a timeout which wasn't " + "marked, please report this to " + "dev@sip-communicator.dev.java.net"); } else { recipient.processTimeout(event); } } catch(Throwable exc) { //any exception thrown within our code should be caught here //so that we could log it rather than interrupt stack activity with //it. this.logApplicationException(DialogTerminatedEvent.class, exc); } } /** * Dispatches the event received from a JAIN-SIP <tt>SipProvider</tt> to one * of our "candidate recipient" listeners. * * @param event the event received for a * <tt>SipProvider</tt>. */ public void processTransactionTerminated(TransactionTerminatedEvent event) { try { Transaction transaction; if (event.isServerTransaction()) transaction = event.getServerTransaction(); else transaction = event.getClientTransaction(); ProtocolProviderServiceSipImpl recipient = getServiceData(transaction); if (recipient == null) { logger.error("We received a transaction terminated which wasn't" + " marked, please report this to" + " dev@sip-communicator.dev.java.net"); } else { recipient.processTransactionTerminated(event); } } catch(Throwable exc) { //any exception thrown within our code should be caught here //so that we could log it rather than interrupt stack activity with //it. this.logApplicationException(DialogTerminatedEvent.class, exc); } } /** * Find the <tt>ProtocolProviderServiceSipImpl</tt> (one of our * "candidate recipient" listeners) which this <tt>request</tt> should be * dispatched to. The strategy is to look first at the request URI, and * then at the To field to find a matching candidate for dispatching. * Note that this method takes a <tt>Request</tt> as param, and not a * <tt>ServerTransaction</tt>, because sometimes <tt>RequestEvent</tt>s * have no associated <tt>ServerTransaction</tt>. * * @param request the <tt>Request</tt> to find a recipient for. * @return a suitable <tt>ProtocolProviderServiceSipImpl</tt>. */ private ProtocolProviderServiceSipImpl findTargetFor(Request request) { if(request == null) { logger.error("request shouldn't be null."); return null; } Set<ProtocolProviderServiceSipImpl> currentListeners = this.getSipListeners(); if(currentListeners.size() == 0) { logger.error("no listeners"); return null; } URI requestURI = request.getRequestURI(); if(requestURI.isSipURI()) { String requestUser = ((SipURI) requestURI).getUser(); List<ProtocolProviderServiceSipImpl> candidates = new ArrayList<ProtocolProviderServiceSipImpl>(); // check if the Request-URI username is // one of ours usernames for(ProtocolProviderServiceSipImpl listener : currentListeners) { String ourUserID = (String) listener.getAccountID().getUserID(); //logger.trace(ourUserID + " *** " + requestUser); if(ourUserID.equals(requestUser)) { logger.trace("suitable candidate found: " + listener.getAccountID()); candidates.add(listener); } } // the perfect match // every other case is approximation if(candidates.size() == 1) { ProtocolProviderServiceSipImpl perfectMatch = candidates.get(0); logger.trace("Will dispatch to \"" + perfectMatch.getAccountID() + "\""); return perfectMatch; } // more than one account match if(candidates.size() > 1) { // check if a custom param exists in the contact // address (set for registrar accounts) for (ProtocolProviderServiceSipImpl candidate : candidates) { String hostValue = ((SipURI) requestURI).getParameter( SipStackSharing.CONTACT_ADDRESS_CUSTOM_PARAM_NAME); if (hostValue == null) continue; if (hostValue.equals(candidate .getContactAddressCustomParamValue())) { logger.trace("Will dispatch to \"" + candidate.getAccountID() + "\" because " + "\" the custom param was set"); return candidate; } } // Past this point, our guess is not reliable. We try to find // the "least worst" match based on parameters like the To field // check if the To header field host part // matches any of our SIP hosts for(ProtocolProviderServiceSipImpl candidate : candidates) { URI fromURI = ((FromHeader) request .getHeader(FromHeader.NAME)).getAddress().getURI(); if(fromURI.isSipURI() == false) continue; SipURI ourURI = (SipURI) candidate .getOurSipAddress((SipURI) fromURI).getURI(); String ourHost = ourURI.getHost(); URI toURI = ((ToHeader) request .getHeader(ToHeader.NAME)).getAddress().getURI(); if(toURI.isSipURI() == false) continue; String toHost = ((SipURI) toURI).getHost(); //logger.trace(toHost + "***" + ourHost); if(toHost.equals(ourHost)) { logger.trace("Will dispatch to \"" + candidate.getAccountID() + "\" because " + "host in the To: is the same as in our AOR"); return candidate; } } // fallback on the first candidate ProtocolProviderServiceSipImpl target = candidates.iterator().next(); logger.warn("Will randomly dispatch to \"" + target.getAccountID() + "\" because there is ambiguity on the username from" + " the Request-URI"); logger.trace("\n" + request); return target; } // fallback on any account ProtocolProviderServiceSipImpl target = currentListeners.iterator().next(); logger.info("Will randomly dispatch to \"" + target.getAccountID() + "\" because the username in the Request-URI " + "is unknown or empty"); logger.trace("\n" + request); return target; } else { logger.error("Request-URI is not a SIP URI, dropping"); } return null; } /** * Retrieves and returns that ProtocolProviderService that this transaction * belongs to, or <tt>null</tt> if we couldn't associate it with a provider * based on neither the request nor the transaction itself. * * @param transaction the transaction that we'd like to determine a provider * for. * * @return a reference to the <tt>ProtocolProviderServiceSipImpl</tt> that * <tt>transaction</tt> was associated with or <tt>null</tt> if we couldn't * determine which one it is. */ private ProtocolProviderServiceSipImpl getServiceData(Transaction transaction) { ProtocolProviderServiceSipImpl service = (ProtocolProviderServiceSipImpl) SipApplicationData .getApplicationData(transaction.getRequest(), SipApplicationData.KEY_SERVICE); if (service != null) { logger.trace("service was found in request data"); return service; } service = (ProtocolProviderServiceSipImpl) SipApplicationData .getApplicationData(transaction.getDialog(), SipApplicationData.KEY_SERVICE); if (service != null) { logger.trace("service was found in dialog data"); } return service; } /** * Logs exceptions that have occurred in the application while processing * events originating from the stack. * * @param eventClass the class of the jain-sip event that we were handling * when the exception was thrown. * @param exc the exception that we need to log. */ private void logApplicationException(Class eventClass, Throwable exc) { logger.error("An error occurred while processing event of type: " + eventClass.getName()); logger.error(exc); } }