Пример #1
0
/**
 * 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, NetworkConfigurationChangeListener {
  /**
   * 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:[email protected]: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. */
  private 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.
   *
   * @throws OperationFailedException if creating the stack fails.
   */
  SipStackSharing() throws OperationFailedException {
    // init of the stack
    try {
      SipFactory sipFactory = SipFactory.getInstance();
      sipFactory.setPathName("org.jitsi.gov.nist");

      Properties sipStackProperties = new SipStackProperties();

      // Create SipStack object
      this.stack = sipFactory.createSipStack(sipStackProperties);
      if (logger.isTraceEnabled()) logger.trace("Created stack: " + this.stack);

      // set our custom address resolver managing SRV records
      AddressResolverImpl addressResolver = new AddressResolverImpl();
      ((SIPTransactionStack) this.stack).setAddressResolver(addressResolver);

      SipActivator.getNetworkAddressManagerService().addNetworkConfigurationChangeListener(this);
    } 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.
   * @throws OperationFailedException if creating one of the underlying <tt>SipProvider</tt>s fails
   *     for whatever reason.
   */
  public void addSipListener(ProtocolProviderServiceSipImpl listener)
      throws OperationFailedException {
    synchronized (this.listeners) {
      if (this.listeners.size() == 0) startListening();
      this.listeners.add(listener);
      if (logger.isTraceEnabled()) 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();
      if (logger.isTraceEnabled()) 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.
   */
  @SuppressWarnings("unchecked") // jain-sip legacy code
  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.
   *
   * @throws OperationFailedException if creating one of the underlying <tt>SipProvider</tt>s fails
   *     for whatever reason.
   */
  private void startListening() throws OperationFailedException {
    try {
      int bindRetriesValue = getBindRetriesValue();

      this.createProvider(this.getPreferredClearPort(), bindRetriesValue, false);
      this.createProvider(this.getPreferredSecurePort(), bindRetriesValue, true);
      this.stack.start();
      if (logger.isTraceEnabled()) 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.
   * @throws TransportNotSupportedException in case we try to create a provider for a transport not
   *     currently supported by jain-sip
   * @throws InvalidArgumentException if we try binding to an illegal port (which we won't)
   * @throws ObjectInUseException if another <tt>SipProvider</tt> is already associated with this
   *     <tt>ListeningPoint</tt>.
   * @throws TransportAlreadySupportedException if there is already a ListeningPoint associated to
   *     this <tt>SipProvider</tt> with the same transport of the <tt>ListeningPoint</tt>.
   * @throws TooManyListenersException if we try to add a new <tt>SipListener</tt> with a
   *     <tt>SipProvider</tt> when one was already registered.
   */
  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);
        if (logger.isTraceEnabled()) 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);
        if (logger.isTraceEnabled())
          logger.trace("UDP and TCP clear ListeningPoints have " + "been created.");

        this.clearJainSipProvider = this.stack.createSipProvider(udpLP);
        this.clearJainSipProvider.addListeningPoint(tcpLP);
        this.clearJainSipProvider.addSipListener(this);
      }

      if (logger.isTraceEnabled()) 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) {
        if (logger.isDebugEnabled())
          logger.debug("Port " + preferredPort + " seems in use for either TCP or UDP.");

        // tries again on a new random port
        int currentlyTriedPort = NetworkUtils.getRandomPortNumber();
        if (logger.isDebugEnabled()) 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.
   */
  @SuppressWarnings("unchecked") // jain-sip legacy code
  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();
      Vector<ListeningPoint> lpointsToRemove = new Vector<ListeningPoint>();
      while (it.hasNext()) {
        lpointsToRemove.add(it.next());
      }

      it = lpointsToRemove.iterator();
      while (it.hasNext()) {
        this.stack.deleteListeningPoint(it.next());
      }

      this.stack.stop();
      if (logger.isTraceEnabled()) 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
   * search is default value set in settings or fallback on a default value.
   *
   * @return the preferred network port for clear communications.
   */
  private int getPreferredClearPort() {

    int preferredPort =
        SipActivator.getConfigurationService().getInt(PREFERRED_CLEAR_PORT_PROPERTY_NAME, -1);

    if (preferredPort <= 1) {
      // check for default value
      preferredPort =
          SipActivator.getResources().getSettingsInt(PREFERRED_CLEAR_PORT_PROPERTY_NAME);
    }

    if (preferredPort <= 1) return ListeningPoint.PORT_5060;
    else return preferredPort;
  }

  /**
   * Fetches the preferred TLS (TCP) port for secure communications in the user preferences or
   * search is default value set in settings or fallback on a default value.
   *
   * @return the preferred network port for secure communications.
   */
  private int getPreferredSecurePort() {
    int preferredPort =
        SipActivator.getConfigurationService().getInt(PREFERRED_SECURE_PORT_PROPERTY_NAME, -1);

    if (preferredPort <= 1) {
      // check for default value
      preferredPort =
          SipActivator.getResources().getSettingsInt(PREFERRED_SECURE_PORT_PROPERTY_NAME);
    }

    if (preferredPort <= 1) return ListeningPoint.PORT_5061;
    else return preferredPort;
  }

  /**
   * 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 " + "*****@*****.**");
      } else {
        if (logger.isTraceEnabled()) 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 {
      if (logger.isTraceEnabled()) logger.trace(event);

      // impossible to dispatch, log here
      if (logger.isDebugEnabled()) 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 {
      Request request = event.getRequest();
      if (logger.isTraceEnabled()) logger.trace("received request: " + request.getMethod());

      /*
       * 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 {
          // apply some hacks if needed on incoming request
          // to be compliant with some servers/clients
          // if needed stop further processing.
          if (applyNonConformanceHacks(event)) return;

          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 [email protected]",
              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");
          if (event.getServerTransaction() != null) event.getServerTransaction().terminate();
        } 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.
           */
          Object container = event.getDialog();
          if (container == null) container = request;
          SipApplicationData.setApplicationData(container, 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);

      // Unfortunately, death can hardly be ignored.
      if (exc instanceof ThreadDeath) throw (ThreadDeath) 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 processResponse(ResponseEvent event) {
    try {
      // we don't have to accept the transaction since we
      // created the request
      ClientTransaction transaction = event.getClientTransaction();
      if (logger.isTraceEnabled())
        logger.trace(
            "received response: "
                + event.getResponse().getStatusCode()
                + " "
                + event.getResponse().getReasonPhrase());

      if (transaction == null) {
        logger.warn("Transaction is null, probably already expired!");
        return;
      }

      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 "
                + "*****@*****.**");
      }
    } 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 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 "
                + "*****@*****.**");
      } 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"
                + " [email protected]");
      } 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;
    }

    List<ProtocolProviderServiceSipImpl> currentListenersCopy =
        new ArrayList<ProtocolProviderServiceSipImpl>(this.getSipListeners());

    // Let's first narrow down candidate choice by comparing
    // addresses and ports (no point in delivering to a provider with a
    // non matching IP address  since they will reject it anyway).
    filterByAddress(currentListenersCopy, request);

    if (currentListenersCopy.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 : currentListenersCopy) {
        String ourUserID = listener.getAccountID().getUserID();
        // logger.trace(ourUserID + " *** " + requestUser);
        if (ourUserID.equals(requestUser)) {
          if (logger.isTraceEnabled())
            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);

        if (logger.isTraceEnabled())
          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())) {
            if (logger.isTraceEnabled())
              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)) {
            if (logger.isTraceEnabled())
              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.info(
            "Will randomly dispatch to \""
                + target.getAccountID()
                + "\" because there is ambiguity on the username from"
                + " the Request-URI");
        if (logger.isTraceEnabled()) logger.trace("\n" + request);
        return target;
      }

      // fallback on any account
      ProtocolProviderServiceSipImpl target = currentListenersCopy.iterator().next();
      if (logger.isDebugEnabled())
        logger.debug(
            "Will randomly dispatch to \""
                + target.getAccountID()
                + "\" because the username in the Request-URI "
                + "is unknown or empty");
      if (logger.isTraceEnabled()) logger.trace("\n" + request);
      return target;
    } else {
      logger.error("Request-URI is not a SIP URI, dropping");
    }
    return null;
  }

  /**
   * Removes from the specified list of candidates providers connected to a registrar that does not
   * match the IP address that we are receiving a request from.
   *
   * @param candidates the list of providers we've like to filter.
   * @param request the request that we are currently dispatching
   */
  private void filterByAddress(List<ProtocolProviderServiceSipImpl> candidates, Request request) {
    Iterator<ProtocolProviderServiceSipImpl> iterPP = candidates.iterator();
    while (iterPP.hasNext()) {
      ProtocolProviderServiceSipImpl candidate = iterPP.next();

      if (candidate.getRegistrarConnection() == null) {
        // RegistrarLess connections are ok
        continue;
      }

      if (!candidate.getRegistrarConnection().isRegistrarless()
          && !candidate.getRegistrarConnection().isRequestFromSameConnection(request)) {
        iterPP.remove();
      }
    }
  }

  /**
   * 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) {
      if (logger.isTraceEnabled()) logger.trace("service was found in request data");
      return service;
    }

    service =
        (ProtocolProviderServiceSipImpl)
            SipApplicationData.getApplicationData(
                transaction.getDialog(), SipApplicationData.KEY_SERVICE);
    if (service != null) {
      if (logger.isTraceEnabled()) 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<DialogTerminatedEvent> eventClass, Throwable exc) {
    String message = "An error occurred while processing event of type: " + eventClass.getName();

    logger.error(message, exc);
    if (logger.isDebugEnabled()) logger.debug(message, exc);
  }

  /**
   * Safely returns the transaction from the event if already exists. If not a new transaction is
   * created.
   *
   * @param event the request event
   * @return the server transaction
   * @throws javax.sip.TransactionAlreadyExistsException if transaction exists
   * @throws javax.sip.TransactionUnavailableException if unavailable
   */
  public static ServerTransaction getOrCreateServerTransaction(RequestEvent event)
      throws TransactionAlreadyExistsException, TransactionUnavailableException {
    ServerTransaction serverTransaction = event.getServerTransaction();

    if (serverTransaction == null) {
      SipProvider jainSipProvider = (SipProvider) event.getSource();

      serverTransaction = jainSipProvider.getNewServerTransaction(event.getRequest());
    }
    return serverTransaction;
  }

  /**
   * Returns a local address to use with the specified TCP destination. The method forces the
   * JAIN-SIP stack to create s and binds (if necessary) and return a socket connected to the
   * specified destination address and port and then return its local address.
   *
   * @param dst the destination address that the socket would need to connect to.
   * @param dstPort the port number that the connection would be established with.
   * @param localAddress the address that we would like to bind on (null for the "any" address).
   * @param transport the transport that will be used TCP ot TLS
   * @return the SocketAddress that this handler would use when connecting to the specified
   *     destination address and port.
   * @throws IOException if we fail binding the local socket
   */
  public java.net.InetSocketAddress getLocalAddressForDestination(
      java.net.InetAddress dst, int dstPort, java.net.InetAddress localAddress, String transport)
      throws IOException {
    if (ListeningPoint.TLS.equalsIgnoreCase(transport))
      return (java.net.InetSocketAddress)
          (((SipStackImpl) this.stack).getLocalAddressForTlsDst(dst, dstPort, localAddress));
    else
      return (java.net.InetSocketAddress)
          (((SipStackImpl) this.stack).getLocalAddressForTcpDst(dst, dstPort, localAddress, 0));
  }

  /**
   * Place to put some hacks if needed on incoming requests.
   *
   * @param event the incoming request event.
   * @return status <code>true</code> if we don't need to process this message, just discard it and
   *     <code>false</code> otherwise.
   */
  private boolean applyNonConformanceHacks(RequestEvent event) {
    Request request = event.getRequest();
    try {
      /*
       * Max-Forwards is required, yet there are UAs which do not
       * place it. SipProvider#getNewServerTransaction(Request)
       * will throw an exception in the case of a missing
       * Max-Forwards header and this method will eventually just
       * log it thus ignoring the whole event.
       */
      if (request.getHeader(MaxForwardsHeader.NAME) == null) {
        // it appears that some buggy providers do send requests
        // with no Max-Forwards headers, as we are at application level
        // and we know there will be no endless loops
        // there is no problem of adding headers and process normally
        // this messages
        MaxForwardsHeader maxForwards =
            SipFactory.getInstance().createHeaderFactory().createMaxForwardsHeader(70);
        request.setHeader(maxForwards);
      }
    } catch (Throwable ex) {
      logger.warn("Cannot apply incoming request modification!", ex);
    }

    try {
      // using asterisk voice mail initial notify for messages
      // is ok, but on the fly received messages their notify comes
      // without subscription-state, so we add it in order to be able to
      // process message.
      if (request.getMethod().equals(Request.NOTIFY)
          && request.getHeader(EventHeader.NAME) != null
          && ((EventHeader) request.getHeader(EventHeader.NAME))
              .getEventType()
              .equals(OperationSetMessageWaitingSipImpl.EVENT_PACKAGE)
          && request.getHeader(SubscriptionStateHeader.NAME) == null) {
        request.addHeader(
            new HeaderFactoryImpl().createSubscriptionStateHeader(SubscriptionStateHeader.ACTIVE));
      }
    } catch (Throwable ex) {
      logger.warn("Cannot apply incoming request modification!", ex);
    }

    try {
      // receiving notify message without subscription state
      // used for keep-alive pings, they have done their job
      // and are no more need. Skip processing them to avoid
      // filling logs with unneeded exceptions.
      if (request.getMethod().equals(Request.NOTIFY)
          && request.getHeader(SubscriptionStateHeader.NAME) == null) {
        return true;
      }
    } catch (Throwable ex) {
      logger.warn("Cannot apply incoming request modification!", ex);
    }

    return false;
  }

  /** List of currently waiting timers that will monitor the protocol provider */
  Map<String, TimerTask> resetListeningPointsTimers = new HashMap<String, TimerTask>();

  /**
   * Listens for network changes and if we have a down interface and we have a tcp/tls provider
   * which is staying for 20 seconds in unregistering state, it cannot unregister cause its using
   * the old address which is currently down, and we must recreate its listening points so it can
   * further reconnect.
   *
   * @param event the change event.
   */
  public void configurationChanged(ChangeEvent event) {
    if (event.isInitial()) return;

    if (event.getType() == ChangeEvent.ADDRESS_DOWN) {
      for (final ProtocolProviderServiceSipImpl pp : listeners) {
        if (pp.getRegistrarConnection().getTransport() != null
            && (pp.getRegistrarConnection().getTransport().equals(ListeningPoint.TCP)
                || pp.getRegistrarConnection().getTransport().equals(ListeningPoint.TLS))) {
          ResetListeningPoint reseter;
          synchronized (resetListeningPointsTimers) {
            // we do this only once for transport
            if (resetListeningPointsTimers.containsKey(pp.getRegistrarConnection().getTransport()))
              continue;

            reseter = new ResetListeningPoint(pp);
            resetListeningPointsTimers.put(pp.getRegistrarConnection().getTransport(), reseter);
          }
          pp.addRegistrationStateChangeListener(reseter);
        }
      }
    }
  }

  /**
   * If a tcp(tls) provider stays unregistering for a long time after connection changed most
   * probably it won't get registered after unregistering fails, cause underlying listening point
   * are conncted to wrong interfaces. So we will replace them.
   */
  private class ResetListeningPoint extends TimerTask implements RegistrationStateChangeListener {
    /** The time we wait before checking is the provider still unregistering. */
    private static final int TIME_FOR_PP_TO_UNREGISTER = 20000;

    /** The protocol provider we are checking. */
    private final ProtocolProviderServiceSipImpl protocolProvider;

    /**
     * Constructs this task.
     *
     * @param pp
     */
    ResetListeningPoint(ProtocolProviderServiceSipImpl pp) {
      this.protocolProvider = pp;
    }

    /**
     * Notified when registration state changed for a provider.
     *
     * @param evt
     */
    public void registrationStateChanged(RegistrationStateChangeEvent evt) {
      if (evt.getNewState() == RegistrationState.UNREGISTERING) {
        new Timer().schedule(this, TIME_FOR_PP_TO_UNREGISTER);
      } else {
        protocolProvider.removeRegistrationStateChangeListener(this);
        resetListeningPointsTimers.remove(protocolProvider.getRegistrarConnection().getTransport());
      }
    }

    /** The real task work, replace listening point. */
    public void run() {
      // if the provider is still unregistering it most probably won't
      // successes until we re-init the LP
      if (protocolProvider.getRegistrationState() == RegistrationState.UNREGISTERING) {
        String transport = protocolProvider.getRegistrarConnection().getTransport();

        ListeningPoint old = getLP(transport);

        try {
          stack.deleteListeningPoint(old);
        } catch (Throwable t) {
          logger.warn("Error replacing ListeningPoint for " + transport, t);
        }

        try {
          ListeningPoint tcpLP =
              stack.createListeningPoint(
                  NetworkUtils.IN_ADDR_ANY,
                  transport.equals(ListeningPoint.TCP)
                      ? getPreferredClearPort()
                      : getPreferredSecurePort(),
                  transport);
          clearJainSipProvider.addListeningPoint(tcpLP);
        } catch (Throwable t) {
          logger.warn(
              "Error replacing ListeningPoint for "
                  + protocolProvider.getRegistrarConnection().getTransport(),
              t);
        }
      }

      resetListeningPointsTimers.remove(protocolProvider.getRegistrarConnection().getTransport());
    }
  }
}
Пример #2
0
/**
 * Checks the <tt>protocolProvider</tt>'s account properties for configured custom headers and add
 * them to the outgoing requests. The header account properties are of form:
 * ConfigHeader.N.Name=HeaderName ConfigHeader.N.Value=HeaderValue
 * ConfigHeader.N.Method=SIP_MethodName (optional) Where N is the index of the header, multiple
 * custom headers can be added. Name is the header name to use and Value is its value. The optional
 * property is whether to use a specific request method to attach headers to or if missing we will
 * attach it to all requests.
 *
 * @author Damian Minkov
 */
public class ConfigHeaders {
  /** The logger for this class */
  public static final Logger logger = Logger.getLogger(ConfigHeaders.class);

  /** The account property holding all the custom pre-configured headers. */
  private static final String ACC_PROPERTY_CONFIG_HEADER = "ConfigHeader";

  /** The config property suffix for custom header name. */
  private static final String ACC_PROPERTY_CONFIG_HEADER_NAME = "Name";

  /** The config property suffix for custom header value. */
  private static final String ACC_PROPERTY_CONFIG_HEADER_VALUE = "Value";

  /** The config property suffix for custom header method. */
  private static final String ACC_PROPERTY_CONFIG_HEADER_METHOD = "Method";

  /**
   * Attach any custom headers pre configured for the account. Added only to message Requests. The
   * header account properties are of form: ConfigHeader.N.Name=HeaderName
   * ConfigHeader.N.Value=HeaderValue ConfigHeader.N.Method=SIP_MethodName (optional) Where N is the
   * index of the header, multiple custom headers can be added. Name is the header name to use and
   * Value is its value. The optional property is whether to use a specific request method to attach
   * headers to or if missing we will attach it to all requests.
   *
   * @param message the message that we'd like to attach custom headers to.
   * @param protocolProvider the protocol provider to check for configured custom headers.
   */
  static void attachConfigHeaders(
      Message message, ProtocolProviderServiceSipImpl protocolProvider) {
    if (message instanceof Response) return;

    Request request = (Request) message;

    Map<String, String> props = protocolProvider.getAccountID().getAccountProperties();

    Map<String, Map<String, String>> headers = new HashMap<String, Map<String, String>>();

    // parse the properties into the map where the index is the key
    for (Map.Entry<String, String> entry : props.entrySet()) {
      String pName = entry.getKey();
      String prefStr = entry.getValue();
      String name;
      String ix;

      if (!pName.startsWith(ACC_PROPERTY_CONFIG_HEADER) || prefStr == null) continue;

      prefStr = prefStr.trim();

      if (pName.contains(".")) {
        pName = pName.replaceAll(ACC_PROPERTY_CONFIG_HEADER + ".", "");
        name = pName.substring(pName.lastIndexOf('.') + 1).trim();

        if (!pName.contains(".")) continue;

        ix = pName.substring(0, pName.lastIndexOf('.')).trim();
      } else continue;

      Map<String, String> headerValues = headers.get(ix);

      if (headerValues == null) {
        headerValues = new HashMap<String, String>();
        headers.put(ix, headerValues);
      }

      headerValues.put(name, prefStr);
    }

    // process the found custom headers
    for (Map<String, String> headerValues : headers.values()) {
      String method = headerValues.get(ACC_PROPERTY_CONFIG_HEADER_METHOD);

      // if there is a method setting and is different from
      // current request method skip this header
      // if any of the required properties are missing skip (Name & Value)
      if ((method != null && !request.getMethod().equalsIgnoreCase(method))
          || !headerValues.containsKey(ACC_PROPERTY_CONFIG_HEADER_NAME)
          || !headerValues.containsKey(ACC_PROPERTY_CONFIG_HEADER_VALUE)) continue;

      try {
        String name = headerValues.get(ACC_PROPERTY_CONFIG_HEADER_NAME);
        String value = processParams(headerValues.get(ACC_PROPERTY_CONFIG_HEADER_VALUE), request);

        Header h = request.getHeader(name);

        // makes possible overriding already created headers which
        // are not custom one
        // RouteHeader is used by ProxyRouter/DefaultRouter and we
        // cannot use it as custom header if we want to add it
        if ((h != null && !(h instanceof CustomHeader)) || name.equals(SIPHeaderNames.ROUTE)) {
          request.setHeader(protocolProvider.getHeaderFactory().createHeader(name, value));
        } else request.addHeader(new CustomHeaderList(name, value));
      } catch (Exception e) {
        logger.error("Cannot create custom header", e);
      }
    }
  }

  /**
   * Checks for certain params existence in the value, and replace them with real values obtained
   * from <tt>request</tt>.
   *
   * @param value the value of the header param
   * @param request the request we are processing
   * @return the value with replaced params
   */
  private static String processParams(String value, Request request) {
    if (value.indexOf("${from.address}") != -1) {
      FromHeader fromHeader = (FromHeader) request.getHeader(FromHeader.NAME);

      if (fromHeader != null) {
        value = value.replace("${from.address}", fromHeader.getAddress().getURI().toString());
      }
    }

    if (value.indexOf("${from.userID}") != -1) {
      FromHeader fromHeader = (FromHeader) request.getHeader(FromHeader.NAME);

      if (fromHeader != null) {
        URI fromURI = fromHeader.getAddress().getURI();
        String fromAddr = fromURI.toString();

        // strips sip: or sips:
        if (fromURI.isSipURI()) {
          fromAddr = fromAddr.replaceFirst(fromURI.getScheme() + ":", "");
        }

        // take the userID part
        int index = fromAddr.indexOf('@');
        if (index > -1) fromAddr = fromAddr.substring(0, index);

        value = value.replace("${from.userID}", fromAddr);
      }
    }

    if (value.indexOf("${to.address}") != -1) {
      ToHeader toHeader = (ToHeader) request.getHeader(ToHeader.NAME);

      if (toHeader != null) {
        value = value.replace("${to.address}", toHeader.getAddress().getURI().toString());
      }
    }

    if (value.indexOf("${to.userID}") != -1) {
      ToHeader toHeader = (ToHeader) request.getHeader(ToHeader.NAME);

      if (toHeader != null) {
        URI toURI = toHeader.getAddress().getURI();
        String toAddr = toURI.toString();

        // strips sip: or sips:
        if (toURI.isSipURI()) {
          toAddr = toAddr.replaceFirst(toURI.getScheme() + ":", "");
        }

        // take the userID part
        int index = toAddr.indexOf('@');
        if (index > -1) toAddr = toAddr.substring(0, index);

        value = value.replace("${to.userID}", toAddr);
      }
    }

    return value;
  }
}