/* * Conditions Of Use * * This software was developed by employees of the National Institute of * Standards and Technology (NIST), an agency of the Federal Government. * Pursuant to title 15 Untied States Code Section 105, works of NIST * employees are not subject to copyright protection in the United States * and are considered to be in the public domain. As a result, a formal * license is not needed to use the software. * * This software is provided by NIST as a service and is expressly * provided "AS IS." NIST MAKES NO WARRANTY OF ANY KIND, EXPRESS, IMPLIED * OR STATUTORY, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTY OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT * AND DATA ACCURACY. NIST does not warrant or make any representations * regarding the use of the software or the results thereof, including but * not limited to the correctness, accuracy, reliability or usefulness of * the software. * * Permission to use this software is contingent upon your acceptance * of the terms of this agreement * * . * */ package gov.nist.javax.sip.stack; import gov.nist.core.Host; import gov.nist.core.HostPort; import gov.nist.core.ServerLogger; import gov.nist.core.StackLogger; import gov.nist.core.ThreadAuditor; import gov.nist.core.net.AddressResolver; import gov.nist.core.net.DefaultNetworkLayer; import gov.nist.core.net.NetworkLayer; import gov.nist.javax.sip.DefaultAddressResolver; import gov.nist.javax.sip.ListeningPointImpl; import gov.nist.javax.sip.LogRecordFactory; import gov.nist.javax.sip.SIPConstants; import gov.nist.javax.sip.SipListenerExt; import gov.nist.javax.sip.SipProviderImpl; import gov.nist.javax.sip.SipStackImpl; import gov.nist.javax.sip.header.Event; import gov.nist.javax.sip.header.Via; import gov.nist.javax.sip.header.extensions.JoinHeader; import gov.nist.javax.sip.header.extensions.ReplacesHeader; import gov.nist.javax.sip.message.SIPMessage; import gov.nist.javax.sip.message.SIPRequest; import gov.nist.javax.sip.message.SIPResponse; import java.io.IOException; import java.net.InetAddress; import java.net.SocketAddress; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.Set; import java.util.Timer; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import javax.sip.ClientTransaction; import javax.sip.Dialog; import javax.sip.DialogState; import javax.sip.DialogTerminatedEvent; import javax.sip.ServerTransaction; import javax.sip.SipException; import javax.sip.SipListener; import javax.sip.TransactionState; import javax.sip.TransactionTerminatedEvent; import javax.sip.address.Hop; import javax.sip.address.Router; import javax.sip.header.CallIdHeader; import javax.sip.header.EventHeader; import javax.sip.message.Request; import javax.sip.message.Response; /* * Jeff Keyser : architectural suggestions and contributions. Pierre De Rop and Thomas Froment : * Bug reports. Jeyashankher < jai@lucent.com > : bug reports. Jeroen van Bemmel : Bug fixes. * * */ /** * * This is the sip stack. It is essentially a management interface. It manages the resources for * the JAIN-SIP implementation. This is the structure that is wrapped by the SipStackImpl. * * @see gov.nist.javax.sip.SipStackImpl * * @author M. Ranganathan <br/> * * @version 1.2 $Revision: 1.141 $ $Date: 2009/12/17 23:38:27 $ */ public abstract class SIPTransactionStack implements SIPTransactionEventListener, SIPDialogEventListener { /* * Number of milliseconds between timer ticks (500). */ public static final int BASE_TIMER_INTERVAL = 500; /* * Connection linger time (seconds) this is the time (in seconds) for which we linger the TCP * connection before closing it. */ public static final int CONNECTION_LINGER_TIME = 8; /* * Table of retransmission Alert timers. */ protected ConcurrentHashMap<String, SIPServerTransaction> retransmissionAlertTransactions; // Table of early dialogs ( to keep identity mapping ) protected ConcurrentHashMap<String, SIPDialog> earlyDialogTable; // Table of dialogs. protected ConcurrentHashMap<String, SIPDialog> dialogTable; // A set of methods that result in dialog creations. protected static final Set<String> dialogCreatingMethods = new HashSet<String>(); // Global timer. Use this for all timer tasks. private Timer timer; // List of pending server transactions private ConcurrentHashMap<String, SIPServerTransaction> pendingTransactions; // hashtable for fast lookup private ConcurrentHashMap<String, SIPClientTransaction> clientTransactionTable; // Set to false if you want hiwat and lowat to be consulted. protected boolean unlimitedServerTransactionTableSize = true; // Set to false if you want unlimited size of client trnansactin table. protected boolean unlimitedClientTransactionTableSize = true; // High water mark for ServerTransaction Table // after which requests are dropped. protected int serverTransactionTableHighwaterMark = 5000; // Low water mark for Server Tx table size after which // requests are selectively dropped protected int serverTransactionTableLowaterMark = 4000; // Hiwater mark for client transaction table. These defaults can be // overriden by stack // configuration. protected int clientTransactionTableHiwaterMark = 1000; // Low water mark for client tx table. protected int clientTransactionTableLowaterMark = 800; private AtomicInteger activeClientTransactionCount = new AtomicInteger(0); // Hashtable for server transactions. private ConcurrentHashMap<String, SIPServerTransaction> serverTransactionTable; // A table of ongoing transactions indexed by mergeId ( for detecting merged // requests. private ConcurrentHashMap<String, SIPServerTransaction> mergeTable; private ConcurrentHashMap<String,SIPServerTransaction> terminatedServerTransactionsPendingAck; private ConcurrentHashMap<String,SIPClientTransaction> forkedClientTransactionTable; /* * A wrapper around differnt logging implementations (log4j, commons logging, slf4j, ...) to help log debug. */ private StackLogger stackLogger; /* * ServerLog is used just for logging stack message tracecs. */ protected ServerLogger serverLogger; /* * We support UDP on this stack. */ boolean udpFlag; /* * Internal router. Use this for all sip: request routing. * */ protected DefaultRouter defaultRouter; /* * Global flag that turns logging off */ protected boolean needsLogging; /* * Flag used for testing TI, bypasses filtering of ACK to non-2xx */ private boolean non2XXAckPassedToListener; /* * Class that handles caching of TCP/TLS connections. */ protected IOHandler ioHandler; /* * Flag that indicates that the stack is active. */ protected boolean toExit; /* * Name of the stack. */ protected String stackName; /* * IP address of stack -- this can be re-written by stun. * * @deprecated */ protected String stackAddress; /* * INET address of stack (cached to avoid repeated lookup) * * @deprecated */ protected InetAddress stackInetAddress; /* * Request factory interface (to be provided by the application) */ protected StackMessageFactory sipMessageFactory; /* * Router to determine where to forward the request. */ protected javax.sip.address.Router router; /* * Number of pre-allocated threads for processing udp messages. -1 means no preallocated * threads ( dynamically allocated threads). */ protected int threadPoolSize; /* * max number of simultaneous connections. */ protected int maxConnections; /* * Close accept socket on completion. */ protected boolean cacheServerConnections; /* * Close connect socket on Tx termination. */ protected boolean cacheClientConnections; /* * Use the user supplied router for all out of dialog requests. */ protected boolean useRouterForAll; /* * Max size of message that can be read from a TCP connection. */ protected int maxContentLength; /* * Max # of headers that a SIP message can contain. */ protected int maxMessageSize; /* * A collection of message processors. */ private Collection<MessageProcessor> messageProcessors; /* * Read timeout on TCP incoming sockets -- defines the time between reads for after delivery * of first byte of message. */ protected int readTimeout; /* * The socket factory. Can be overriden by applications that want direct access to the * underlying socket. */ protected NetworkLayer networkLayer; /* * Outbound proxy String ( to be handed to the outbound proxy class on creation). */ protected String outboundProxy; protected String routerPath; // Flag to indicate whether the stack will provide dialog // support. protected boolean isAutomaticDialogSupportEnabled; // The set of events for which subscriptions can be forked. protected HashSet<String> forkedEvents; // Generate a timestamp header for retransmitted requests. protected boolean generateTimeStampHeader; protected AddressResolver addressResolver; // Max time that the listener is allowed to take to respond to a // request. Default is "infinity". This property allows // containers to defend against buggy clients (that do not // want to respond to requests). protected int maxListenerResponseTime; // A flag that indicates whether or not RFC 2543 clients are fully supported. // If this is set to true, then To tag checking on the Dialog layer is // disabled in a few places - resulting in possible breakage of forked dialogs. protected boolean rfc2543Supported = true; // / Provides a mechanism for applications to check the health of threads in // the stack protected ThreadAuditor threadAuditor = new ThreadAuditor(); protected LogRecordFactory logRecordFactory; // Set to true if the client CANCEL transaction should be checked before sending // it out. protected boolean cancelClientTransactionChecked = true; // Is to tag reassignment allowed. protected boolean remoteTagReassignmentAllowed = true; protected boolean logStackTraceOnMessageSend = true; // Receive UDP buffer size protected int receiveUdpBufferSize; // Send UDP buffer size protected int sendUdpBufferSize; protected boolean stackDoesCongestionControl = true; protected boolean isBackToBackUserAgent = false; protected boolean checkBranchId; protected boolean isAutomaticDialogErrorHandlingEnabled = true; protected boolean isDialogTerminatedEventDeliveredForNullDialog = false; // Max time for a forked response to arrive. After this time, the original dialog // is not tracked. If you want to track the original transaction you need to specify // the max fork time with a stack init property. protected int maxForkTime = 0; // / Timer to regularly ping the thread auditor (on behalf of the timer // thread) class PingTimer extends SIPStackTimerTask { // / Timer thread handle ThreadAuditor.ThreadHandle threadHandle; // / Constructor public PingTimer(ThreadAuditor.ThreadHandle a_oThreadHandle) { threadHandle = a_oThreadHandle; } protected void runTask() { // Check if we still have a timer (it may be null after shutdown) if (getTimer() != null) { // Register the timer task if we haven't done so if (threadHandle == null) { // This happens only once since the thread handle is passed // to the next scheduled ping timer threadHandle = getThreadAuditor().addCurrentThread(); } // Let the thread auditor know that the timer task is alive threadHandle.ping(); // Schedule the next ping getTimer().schedule(new PingTimer(threadHandle), threadHandle.getPingIntervalInMillisecs()); } } } class RemoveForkedTransactionTimerTask extends SIPStackTimerTask { private SIPClientTransaction clientTransaction; public RemoveForkedTransactionTimerTask(SIPClientTransaction sipClientTransaction ) { this.clientTransaction = sipClientTransaction; } @Override protected void runTask() { forkedClientTransactionTable.remove(clientTransaction.getTransactionId()); } } static { // Standard set of methods that create dialogs. dialogCreatingMethods.add(Request.REFER); dialogCreatingMethods.add(Request.INVITE); dialogCreatingMethods.add(Request.SUBSCRIBE); } /** * Default constructor. */ protected SIPTransactionStack() { this.toExit = false; this.forkedEvents = new HashSet<String>(); // set of events for which subscriptions can be forked. // Set an infinite thread pool size. this.threadPoolSize = -1; // Close response socket after infinte time. // for max performance this.cacheServerConnections = true; // Close the request socket after infinite time. // for max performance this.cacheClientConnections = true; // Max number of simultaneous connections. this.maxConnections = -1; // Array of message processors. messageProcessors = new ArrayList<MessageProcessor>(); // Handle IO for this process. this.ioHandler = new IOHandler(this); // The read time out is infinite. this.readTimeout = -1; this.maxListenerResponseTime = -1; // The default (identity) address lookup scheme this.addressResolver = new DefaultAddressResolver(); // Notify may or may not create a dialog. This is handled in // the code. // Create the transaction collections // Dialog dable. this.dialogTable = new ConcurrentHashMap<String, SIPDialog>(); this.earlyDialogTable = new ConcurrentHashMap<String, SIPDialog>(); clientTransactionTable = new ConcurrentHashMap<String, SIPClientTransaction>(); serverTransactionTable = new ConcurrentHashMap<String, SIPServerTransaction>(); this.terminatedServerTransactionsPendingAck = new ConcurrentHashMap<String, SIPServerTransaction>(); mergeTable = new ConcurrentHashMap<String, SIPServerTransaction>(); retransmissionAlertTransactions = new ConcurrentHashMap<String, SIPServerTransaction>(); // Start the timer event thread. this.timer = new Timer(); this.pendingTransactions = new ConcurrentHashMap<String, SIPServerTransaction>(); this.forkedClientTransactionTable = new ConcurrentHashMap<String,SIPClientTransaction>(); if (getThreadAuditor().isEnabled()) { // Start monitoring the timer thread timer.schedule(new PingTimer(null), 0); } } /** * Re Initialize the stack instance. */ protected void reInit() { if (stackLogger.isLoggingEnabled()) stackLogger.logDebug("Re-initializing !"); // Array of message processors. messageProcessors = new ArrayList<MessageProcessor>(); // Handle IO for this process. this.ioHandler = new IOHandler(this); // clientTransactions = new ConcurrentLinkedQueue(); // serverTransactions = new ConcurrentLinkedQueue(); pendingTransactions = new ConcurrentHashMap<String, SIPServerTransaction>(); clientTransactionTable = new ConcurrentHashMap<String, SIPClientTransaction>(); serverTransactionTable = new ConcurrentHashMap<String, SIPServerTransaction>(); retransmissionAlertTransactions = new ConcurrentHashMap<String, SIPServerTransaction>(); mergeTable = new ConcurrentHashMap<String, SIPServerTransaction>(); // Dialog dable. this.dialogTable = new ConcurrentHashMap<String, SIPDialog>(); this.earlyDialogTable = new ConcurrentHashMap<String, SIPDialog>(); this.terminatedServerTransactionsPendingAck = new ConcurrentHashMap<String,SIPServerTransaction>(); this.forkedClientTransactionTable = new ConcurrentHashMap<String,SIPClientTransaction>(); this.timer = new Timer(); this.activeClientTransactionCount = new AtomicInteger(0); } /** * Creates and binds, if necessary, a socket connected to the specified * destination address and port and then returns 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 localPort the port that we'd like our socket to bind to (0 for a * random port). * * @return the SocketAddress that this handler would use when connecting to * the specified destination address and port. * * @throws IOException */ public SocketAddress obtainLocalAddress(InetAddress dst, int dstPort, InetAddress localAddress, int localPort) throws IOException { return this.ioHandler.obtainLocalAddress( dst, dstPort, localAddress, localPort); } /** * For debugging -- allows you to disable logging or enable logging selectively. * * */ public void disableLogging() { this.getStackLogger().disableLogging(); } /** * Globally enable message logging ( for debugging) * */ public void enableLogging() { this.getStackLogger().enableLogging(); } /** * Print the dialog table. * */ public void printDialogTable() { if (isLoggingEnabled()) { this.getStackLogger().logDebug("dialog table = " + this.dialogTable); System.out.println("dialog table = " + this.dialogTable); } } /** * Retrieve a transaction from our table of transactions with pending retransmission alerts. * * @param dialogId * @return -- the RetransmissionAlert enabled transaction corresponding to the given dialog * ID. */ public SIPServerTransaction getRetransmissionAlertTransaction(String dialogId) { return (SIPServerTransaction) this.retransmissionAlertTransactions.get(dialogId); } /** * Return true if extension is supported. * * @return true if extension is supported and false otherwise. */ public static boolean isDialogCreated(String method) { return dialogCreatingMethods.contains(method); } /** * Add an extension method. * * @param extensionMethod -- extension method to support for dialog creation */ public void addExtensionMethod(String extensionMethod) { if (extensionMethod.equals(Request.NOTIFY)) { if (stackLogger.isLoggingEnabled()) stackLogger.logDebug("NOTIFY Supported Natively"); } else { dialogCreatingMethods.add(extensionMethod.trim().toUpperCase()); } } /** * Put a dialog into the dialog table. * * @param dialog -- dialog to put into the dialog table. * */ public void putDialog(SIPDialog dialog) { String dialogId = dialog.getDialogId(); if (dialogTable.containsKey(dialogId)) { if (stackLogger.isLoggingEnabled()) { stackLogger.logDebug("putDialog: dialog already exists" + dialogId + " in table = " + dialogTable.get(dialogId)); } return; } if (stackLogger.isLoggingEnabled()) { stackLogger.logDebug("putDialog dialogId=" + dialogId + " dialog = " + dialog); } dialog.setStack(this); if (stackLogger.isLoggingEnabled()) stackLogger.logStackTrace(); dialogTable.put(dialogId, dialog); } /** * Create a dialog and add this transaction to it. * * @param transaction -- tx to add to the dialog. * @return the newly created Dialog. */ public SIPDialog createDialog(SIPTransaction transaction) { SIPDialog retval = null; if (transaction instanceof SIPClientTransaction) { String dialogId = ((SIPRequest) transaction.getRequest()).getDialogId(false); if (this.earlyDialogTable.get(dialogId) != null) { SIPDialog dialog = this.earlyDialogTable.get(dialogId); if (dialog.getState() == null || dialog.getState() == DialogState.EARLY) { retval = dialog; } else { retval = new SIPDialog(transaction); this.earlyDialogTable.put(dialogId, retval); } } else { retval = new SIPDialog(transaction); this.earlyDialogTable.put(dialogId, retval); } } else { retval = new SIPDialog(transaction); } return retval; } /** * Create a Dialog given a client tx and response. * * @param transaction * @param sipResponse * @return */ public SIPDialog createDialog(SIPClientTransaction transaction, SIPResponse sipResponse) { String dialogId = ((SIPRequest) transaction.getRequest()).getDialogId(false); SIPDialog retval = null; if (this.earlyDialogTable.get(dialogId) != null) { retval = this.earlyDialogTable.get(dialogId); if (sipResponse.isFinalResponse()) { this.earlyDialogTable.remove(dialogId); } } else { retval = new SIPDialog(transaction, sipResponse); } return retval; } /** * Create a Dialog given a sip provider and response. * * @param sipProvider * @param sipResponse * @return */ public SIPDialog createDialog(SipProviderImpl sipProvider, SIPResponse sipResponse) { return new SIPDialog(sipProvider, sipResponse); } /** * Remove the dialog from the dialog table. * * @param dialog -- dialog to remove. */ public void removeDialog(SIPDialog dialog) { String id = dialog.getDialogId(); String earlyId = dialog.getEarlyDialogId(); if (earlyId != null) { this.earlyDialogTable.remove(earlyId); this.dialogTable.remove(earlyId); } if (id != null) { // FHT: Remove dialog from table only if its associated dialog is the same as the one // specified Object old = this.dialogTable.get(id); if (old == dialog) { this.dialogTable.remove(id); } // We now deliver DTE even when the dialog is not originally present in the Dialog // Table // This happens before the dialog state is assigned. if (!dialog.testAndSetIsDialogTerminatedEventDelivered()) { DialogTerminatedEvent event = new DialogTerminatedEvent(dialog.getSipProvider(), dialog); // Provide notification to the listener that the dialog has // ended. dialog.getSipProvider().handleEvent(event, null); } } else if ( this.isDialogTerminatedEventDeliveredForNullDialog ) { if (!dialog.testAndSetIsDialogTerminatedEventDelivered()) { DialogTerminatedEvent event = new DialogTerminatedEvent(dialog.getSipProvider(), dialog); // Provide notification to the listener that the dialog has // ended. dialog.getSipProvider().handleEvent(event, null); } } } /** * Return the dialog for a given dialog ID. If compatibility is enabled then we do not assume * the presence of tags and hence need to add a flag to indicate whether this is a server or * client transaction. * * @param dialogId is the dialog id to check. */ public SIPDialog getDialog(String dialogId) { SIPDialog sipDialog = (SIPDialog) dialogTable.get(dialogId); if (stackLogger.isLoggingEnabled()) { stackLogger.logDebug("getDialog(" + dialogId + ") : returning " + sipDialog); } return sipDialog; } /** * Remove the dialog given its dialog id. This is used for dialog id re-assignment only. * * @param dialogId is the dialog Id to remove. */ public void removeDialog(String dialogId) { if (stackLogger.isLoggingEnabled()) { stackLogger.logWarning("Silently removing dialog from table"); } dialogTable.remove(dialogId); } /** * Find a matching client SUBSCRIBE to the incoming notify. NOTIFY requests are matched to * such SUBSCRIBE requests if they contain the same "Call-ID", a "To" header "tag" parameter * which matches the "From" header "tag" parameter of the SUBSCRIBE, and the same "Event" * header field. Rules for comparisons of the "Event" headers are described in section 7.2.1. * If a matching NOTIFY request contains a "Subscription-State" of "active" or "pending", it * creates a new subscription and a new dialog (unless they have already been created by a * matching response, as described above). * * @param notifyMessage * @return -- the matching ClientTransaction with semaphore aquired or null if no such client * transaction can be found. */ public SIPClientTransaction findSubscribeTransaction(SIPRequest notifyMessage, ListeningPointImpl listeningPoint) { SIPClientTransaction retval = null; try { Iterator it = clientTransactionTable.values().iterator(); if (stackLogger.isLoggingEnabled()) stackLogger.logDebug("ct table size = " + clientTransactionTable.size()); String thisToTag = notifyMessage.getTo().getTag(); if (thisToTag == null) { return retval; } Event eventHdr = (Event) notifyMessage.getHeader(EventHeader.NAME); if (eventHdr == null) { if (stackLogger.isLoggingEnabled()) { stackLogger.logDebug("event Header is null -- returning null"); } return retval; } while (it.hasNext()) { SIPClientTransaction ct = (SIPClientTransaction) it.next(); if (!ct.getMethod().equals(Request.SUBSCRIBE)) continue; // if ( sipProvider.getListeningPoint(transport) == null) String fromTag = ct.from.getTag(); Event hisEvent = ct.event; // Event header is mandatory but some slopply clients // dont include it. if (hisEvent == null) continue; if (stackLogger.isLoggingEnabled()) { stackLogger.logDebug("ct.fromTag = " + fromTag); stackLogger.logDebug("thisToTag = " + thisToTag); stackLogger.logDebug("hisEvent = " + hisEvent); stackLogger.logDebug("eventHdr " + eventHdr); } if ( fromTag.equalsIgnoreCase(thisToTag) && hisEvent != null && eventHdr.match(hisEvent) && notifyMessage.getCallId().getCallId().equalsIgnoreCase( ct.callId.getCallId())) { if (ct.acquireSem()) retval = ct; return retval; } } return retval; } finally { if (stackLogger.isLoggingEnabled()) stackLogger.logDebug("findSubscribeTransaction : returning " + retval); } } /** * Add entry to "Transaction Pending ACK" table. * * @param serverTransaction */ public void addTransactionPendingAck(SIPServerTransaction serverTransaction) { String branchId = ((SIPRequest)serverTransaction.getRequest()).getTopmostVia().getBranch(); if ( branchId != null ) { this.terminatedServerTransactionsPendingAck.put(branchId, serverTransaction); } } /** * Get entry in the server transaction pending ACK table corresponding to an ACK. * * @param ackMessage * @return */ public SIPServerTransaction findTransactionPendingAck(SIPRequest ackMessage) { return this.terminatedServerTransactionsPendingAck.get(ackMessage.getTopmostVia().getBranch()); } /** * Remove entry from "Transaction Pending ACK" table. * * @param serverTransaction * @return */ public boolean removeTransactionPendingAck(SIPServerTransaction serverTransaction) { String branchId = ((SIPRequest)serverTransaction.getRequest()).getTopmostVia().getBranch(); if ( branchId != null && this.terminatedServerTransactionsPendingAck.containsKey(branchId) ) { this.terminatedServerTransactionsPendingAck.remove(branchId); return true; } else { return false; } } /** * Check if this entry exists in the "Transaction Pending ACK" table. * * @param serverTransaction * @return */ public boolean isTransactionPendingAck(SIPServerTransaction serverTransaction) { String branchId = ((SIPRequest)serverTransaction.getRequest()).getTopmostVia().getBranch(); return this.terminatedServerTransactionsPendingAck.contains(branchId); } /** * Find the transaction corresponding to a given request. * * @param sipMessage request for which to retrieve the transaction. * * @param isServer search the server transaction table if true. * * @return the transaction object corresponding to the request or null if no such mapping * exists. */ public SIPTransaction findTransaction(SIPMessage sipMessage, boolean isServer) { SIPTransaction retval = null; try { if (isServer) { Via via = sipMessage.getTopmostVia(); if (via.getBranch() != null) { String key = sipMessage.getTransactionId(); retval = (SIPTransaction) serverTransactionTable.get(key); if (stackLogger.isLoggingEnabled()) getStackLogger().logDebug( "serverTx: looking for key " + key + " existing=" + serverTransactionTable); if (key.startsWith(SIPConstants.BRANCH_MAGIC_COOKIE_LOWER_CASE)) { return retval; } } // Need to scan the table for old style transactions (RFC 2543 // style) Iterator<SIPServerTransaction> it = serverTransactionTable.values().iterator(); while (it.hasNext()) { SIPServerTransaction sipServerTransaction = (SIPServerTransaction) it.next(); if (sipServerTransaction.isMessagePartOfTransaction(sipMessage)) { retval = sipServerTransaction; return retval; } } } else { Via via = sipMessage.getTopmostVia(); if (via.getBranch() != null) { String key = sipMessage.getTransactionId(); if (stackLogger.isLoggingEnabled()) getStackLogger().logDebug("clientTx: looking for key " + key); retval = (SIPTransaction) clientTransactionTable.get(key); if (key.startsWith(SIPConstants.BRANCH_MAGIC_COOKIE_LOWER_CASE)) { return retval; } } // Need to scan the table for old style transactions (RFC 2543 // style). This is terribly slow but we need to do this // for backasswords compatibility. Iterator<SIPClientTransaction> it = clientTransactionTable.values().iterator(); while (it.hasNext()) { SIPClientTransaction clientTransaction = (SIPClientTransaction) it.next(); if (clientTransaction.isMessagePartOfTransaction(sipMessage)) { retval = clientTransaction; return retval; } } } } finally { if ( this.getStackLogger().isLoggingEnabled()) { this.getStackLogger().logDebug("findTransaction: returning : " + retval); } } return retval; } /** * Get the transaction to cancel. Search the server transaction table for a transaction that * matches the given transaction. */ public SIPTransaction findCancelTransaction(SIPRequest cancelRequest, boolean isServer) { if (stackLogger.isLoggingEnabled()) { stackLogger.logDebug("findCancelTransaction request= \n" + cancelRequest + "\nfindCancelRequest isServer=" + isServer); } if (isServer) { Iterator<SIPServerTransaction> li = this.serverTransactionTable.values().iterator(); while (li.hasNext()) { SIPTransaction transaction = (SIPTransaction) li.next(); SIPServerTransaction sipServerTransaction = (SIPServerTransaction) transaction; if (sipServerTransaction.doesCancelMatchTransaction(cancelRequest)) return sipServerTransaction; } } else { Iterator<SIPClientTransaction> li = this.clientTransactionTable.values().iterator(); while (li.hasNext()) { SIPTransaction transaction = (SIPTransaction) li.next(); SIPClientTransaction sipClientTransaction = (SIPClientTransaction) transaction; if (sipClientTransaction.doesCancelMatchTransaction(cancelRequest)) return sipClientTransaction; } } if (stackLogger.isLoggingEnabled()) stackLogger.logDebug("Could not find transaction for cancel request"); return null; } /** * Construcor for the stack. Registers the request and response factories for the stack. * * @param messageFactory User-implemented factory for processing messages. */ protected SIPTransactionStack(StackMessageFactory messageFactory) { this(); this.sipMessageFactory = messageFactory; } /** * Finds a pending server transaction. Since each request may be handled either statefully or * statelessly, we keep a map of pending transactions so that a duplicate transaction is not * created if a second request is recieved while the first one is being processed. * * @param requestReceived * @return -- the pending transaction or null if no such transaction exists. */ public SIPServerTransaction findPendingTransaction(SIPRequest requestReceived) { if (this.stackLogger.isLoggingEnabled()) { this.stackLogger.logDebug("looking for pending tx for :" + requestReceived.getTransactionId()); } return (SIPServerTransaction) pendingTransactions.get(requestReceived.getTransactionId()); } /** * See if there is a pending transaction with the same Merge ID as the Merge ID obtained from * the SIP Request. The Merge table is for handling the following condition: If the request * has no tag in the To header field, the UAS core MUST check the request against ongoing * transactions. If the From tag, Call-ID, and CSeq exactly match those associated with an * ongoing transaction, but the request does not match that transaction (based on the matching * rules in Section 17.2.3), the UAS core SHOULD generate a 482 (Loop Detected) response and * pass it to the server transaction. */ public SIPServerTransaction findMergedTransaction(SIPRequest sipRequest) { if (! sipRequest.getMethod().equals(Request.INVITE)) { /* * Dont need to worry about request merging for Non-INVITE transactions. */ return null; } String mergeId = sipRequest.getMergeId(); SIPServerTransaction mergedTransaction = (SIPServerTransaction) this.mergeTable.get(mergeId); if (mergeId == null ) { return null; } else if (mergedTransaction != null && !mergedTransaction.isMessagePartOfTransaction(sipRequest) ) { return mergedTransaction; } else { /* * Check the server transactions that have resulted in dialogs. */ for (Dialog dialog: this.dialogTable.values() ) { SIPDialog sipDialog = (SIPDialog) dialog ; if (sipDialog.getFirstTransaction() != null && sipDialog.getFirstTransaction() instanceof ServerTransaction) { SIPServerTransaction serverTransaction = ((SIPServerTransaction) sipDialog.getFirstTransaction()); SIPRequest transactionRequest = ((SIPServerTransaction) sipDialog.getFirstTransaction()).getOriginalRequest(); if ( (! serverTransaction.isMessagePartOfTransaction(sipRequest)) && sipRequest.getMergeId().equals(transactionRequest.getMergeId())) { return (SIPServerTransaction) sipDialog.getFirstTransaction(); } } } return null; } } /** * Remove a pending Server transaction from the stack. This is called after the user code has * completed execution in the listener. * * @param tr -- pending transaction to remove. */ public void removePendingTransaction(SIPServerTransaction tr) { if (this.stackLogger.isLoggingEnabled()) { this.stackLogger.logDebug("removePendingTx: " + tr.getTransactionId()); } this.pendingTransactions.remove(tr.getTransactionId()); } /** * Remove a transaction from the merge table. * * @param tr -- the server transaction to remove from the merge table. * */ public void removeFromMergeTable(SIPServerTransaction tr) { if (stackLogger.isLoggingEnabled()) { this.stackLogger.logDebug("Removing tx from merge table "); } String key = ((SIPRequest) tr.getRequest()).getMergeId(); if (key != null) { this.mergeTable.remove(key); } } /** * Put this into the merge request table. * * @param sipTransaction -- transaction to put into the merge table. * */ public void putInMergeTable(SIPServerTransaction sipTransaction, SIPRequest sipRequest) { String mergeKey = sipRequest.getMergeId(); if (mergeKey != null) { this.mergeTable.put(mergeKey, sipTransaction); } } /** * Map a Server transaction (possibly sending out a 100 if the server tx is an INVITE). This * actually places it in the hash table and makes it known to the stack. * * @param transaction -- the server transaction to map. */ public void mapTransaction(SIPServerTransaction transaction) { if (transaction.isMapped) return; addTransactionHash(transaction); // transaction.startTransactionTimer(); transaction.isMapped = true; } /** * Handles a new SIP request. It finds a server transaction to handle this message. If none * exists, it creates a new transaction. * * @param requestReceived Request to handle. * @param requestMessageChannel Channel that received message. * * @return A server transaction. */ public ServerRequestInterface newSIPServerRequest(SIPRequest requestReceived, MessageChannel requestMessageChannel) { // Iterator through all server transactions Iterator<SIPServerTransaction> transactionIterator; // Next transaction in the set SIPServerTransaction nextTransaction; // Transaction to handle this request SIPServerTransaction currentTransaction; String key = requestReceived.getTransactionId(); requestReceived.setMessageChannel(requestMessageChannel); currentTransaction = (SIPServerTransaction) serverTransactionTable.get(key); // Got to do this for bacasswards compatibility. if (currentTransaction == null || !currentTransaction.isMessagePartOfTransaction(requestReceived)) { // Loop through all server transactions transactionIterator = serverTransactionTable.values().iterator(); currentTransaction = null; if (!key.toLowerCase().startsWith(SIPConstants.BRANCH_MAGIC_COOKIE_LOWER_CASE)) { while (transactionIterator.hasNext() && currentTransaction == null) { nextTransaction = (SIPServerTransaction) transactionIterator.next(); // If this transaction should handle this request, if (nextTransaction.isMessagePartOfTransaction(requestReceived)) { // Mark this transaction as the one // to handle this message currentTransaction = nextTransaction; } } } // If no transaction exists to handle this message if (currentTransaction == null) { currentTransaction = findPendingTransaction(requestReceived); if (currentTransaction != null) { // Associate the tx with the received request. requestReceived.setTransaction(currentTransaction); if (currentTransaction != null && currentTransaction.acquireSem()) return currentTransaction; else return null; } // Creating a new server tx. May fail under heavy load. currentTransaction = createServerTransaction(requestMessageChannel); if (currentTransaction != null) { // currentTransaction.setPassToListener(); currentTransaction.setOriginalRequest(requestReceived); // Associate the tx with the received request. requestReceived.setTransaction(currentTransaction); } } } // Set ths transaction's encapsulated request // interface from the superclass if (stackLogger.isLoggingEnabled()) { stackLogger.logDebug("newSIPServerRequest( " + requestReceived.getMethod() + ":" + requestReceived.getTopmostVia().getBranch() + "):" + currentTransaction); } if (currentTransaction != null) currentTransaction.setRequestInterface(sipMessageFactory.newSIPServerRequest( requestReceived, currentTransaction)); if (currentTransaction != null && currentTransaction.acquireSem()) { return currentTransaction; } else if (currentTransaction != null) { try { /* * Already processing a message for this transaction. * SEND a trying ( message already being processed ). */ if (currentTransaction.isMessagePartOfTransaction(requestReceived) && currentTransaction.getMethod().equals(requestReceived.getMethod())) { SIPResponse trying = requestReceived.createResponse(Response.TRYING); trying.removeContent(); currentTransaction.getMessageChannel().sendMessage(trying); } } catch (Exception ex) { if (isLoggingEnabled()) stackLogger.logError("Exception occured sending TRYING"); } return null; } else { return null; } } /** * Handles a new SIP response. It finds a client transaction to handle this message. If none * exists, it sends the message directly to the superclass. * * @param responseReceived Response to handle. * @param responseMessageChannel Channel that received message. * * @return A client transaction. */ public ServerResponseInterface newSIPServerResponse(SIPResponse responseReceived, MessageChannel responseMessageChannel) { // Iterator through all client transactions Iterator<SIPClientTransaction> transactionIterator; // Next transaction in the set SIPClientTransaction nextTransaction; // Transaction to handle this request SIPClientTransaction currentTransaction; String key = responseReceived.getTransactionId(); // Note that for RFC 3261 compliant operation, this lookup will // return a tx if one exists and hence no need to search through // the table. currentTransaction = (SIPClientTransaction) clientTransactionTable.get(key); if (currentTransaction == null || (!currentTransaction.isMessagePartOfTransaction(responseReceived) && !key .startsWith(SIPConstants.BRANCH_MAGIC_COOKIE_LOWER_CASE))) { // Loop through all client transactions transactionIterator = clientTransactionTable.values().iterator(); currentTransaction = null; while (transactionIterator.hasNext() && currentTransaction == null) { nextTransaction = (SIPClientTransaction) transactionIterator.next(); // If this transaction should handle this request, if (nextTransaction.isMessagePartOfTransaction(responseReceived)) { // Mark this transaction as the one to // handle this message currentTransaction = nextTransaction; } } // If no transaction exists to handle this message, if (currentTransaction == null) { // JvB: Need to log before passing the response to the client // app, it // gets modified! if (this.stackLogger.isLoggingEnabled(StackLogger.TRACE_INFO)) { responseMessageChannel.logResponse(responseReceived, System .currentTimeMillis(), "before processing"); } // Pass the message directly to the TU return sipMessageFactory.newSIPServerResponse(responseReceived, responseMessageChannel); } } // Aquire the sem -- previous request may still be processing. boolean acquired = currentTransaction.acquireSem(); // Set ths transaction's encapsulated response interface // from the superclass if (this.stackLogger.isLoggingEnabled(StackLogger.TRACE_INFO)) { currentTransaction.logResponse(responseReceived, System.currentTimeMillis(), "before processing"); } if (acquired) { ServerResponseInterface sri = sipMessageFactory.newSIPServerResponse( responseReceived, currentTransaction); if (sri != null) { currentTransaction.setResponseInterface(sri); } else { if (this.stackLogger.isLoggingEnabled()) { this.stackLogger.logDebug("returning null - serverResponseInterface is null!"); } currentTransaction.releaseSem(); return null; } } else { if (stackLogger.isLoggingEnabled()) this.stackLogger.logDebug("Could not aquire semaphore !!"); } if (acquired) return currentTransaction; else return null; } /** * Creates a client transaction to handle a new request. Gets the real message channel from * the superclass, and then creates a new client transaction wrapped around this channel. * * @param nextHop Hop to create a channel to contact. */ public MessageChannel createMessageChannel(SIPRequest request, MessageProcessor mp, Hop nextHop) throws IOException { // New client transaction to return SIPTransaction returnChannel; // Create a new client transaction around the // superclass' message channel // Create the host/port of the target hop Host targetHost = new Host(); targetHost.setHostname(nextHop.getHost()); HostPort targetHostPort = new HostPort(); targetHostPort.setHost(targetHost); targetHostPort.setPort(nextHop.getPort()); MessageChannel mc = mp.createMessageChannel(targetHostPort); // Superclass will return null if no message processor // available for the transport. if (mc == null) return null; returnChannel = createClientTransaction(request, mc); ((SIPClientTransaction) returnChannel).setViaPort(nextHop.getPort()); ((SIPClientTransaction) returnChannel).setViaHost(nextHop.getHost()); addTransactionHash(returnChannel); // clientTransactionTable.put(returnChannel.getTransactionId(), // returnChannel); // Add the transaction timer for the state machine. // returnChannel.startTransactionTimer(); return returnChannel; } /** * Creates a client transaction that encapsulates a MessageChannel. Useful for implementations * that want to subclass the standard * * @param encapsulatedMessageChannel Message channel of the transport layer. */ public SIPClientTransaction createClientTransaction(SIPRequest sipRequest, MessageChannel encapsulatedMessageChannel) { SIPClientTransaction ct = new SIPClientTransaction(this, encapsulatedMessageChannel); ct.setOriginalRequest(sipRequest); return ct; } /** * Creates a server transaction that encapsulates a MessageChannel. Useful for implementations * that want to subclass the standard * * @param encapsulatedMessageChannel Message channel of the transport layer. */ public SIPServerTransaction createServerTransaction(MessageChannel encapsulatedMessageChannel) { // Issue 256 : be consistent with createClientTransaction, if unlimitedServerTransactionTableSize is true, // a new Server Transaction is created no matter what if (unlimitedServerTransactionTableSize) { return new SIPServerTransaction(this, encapsulatedMessageChannel); } else { float threshold = ((float) (serverTransactionTable.size() - serverTransactionTableLowaterMark)) / ((float) (serverTransactionTableHighwaterMark - serverTransactionTableLowaterMark)); boolean decision = Math.random() > 1.0 - threshold; if (decision) { return null; } else { return new SIPServerTransaction(this, encapsulatedMessageChannel); } } } /** * Get the size of the client transaction table. * * @return -- size of the ct table. */ public int getClientTransactionTableSize() { return this.clientTransactionTable.size(); } /** * Get the size of the server transaction table. * * @return -- size of the server table. */ public int getServerTransactionTableSize() { return this.serverTransactionTable.size(); } /** * Add a new client transaction to the set of existing transactions. Add it to the top of the * list so an incoming response has less work to do in order to find the transaction. * * @param clientTransaction -- client transaction to add to the set. */ public void addTransaction(SIPClientTransaction clientTransaction) { if (stackLogger.isLoggingEnabled()) stackLogger.logDebug("added transaction " + clientTransaction); addTransactionHash(clientTransaction); } /** * Remove transaction. This actually gets the tx out of the search structures which the stack * keeps around. When the tx */ public void removeTransaction(SIPTransaction sipTransaction) { if (stackLogger.isLoggingEnabled()) { stackLogger.logDebug("Removing Transaction = " + sipTransaction.getTransactionId() + " transaction = " + sipTransaction); } if (sipTransaction instanceof SIPServerTransaction) { if (stackLogger.isLoggingEnabled()) stackLogger.logStackTrace(); String key = sipTransaction.getTransactionId(); Object removed = serverTransactionTable.remove(key); String method = sipTransaction.getMethod(); this.removePendingTransaction((SIPServerTransaction) sipTransaction); this.removeTransactionPendingAck((SIPServerTransaction) sipTransaction); if (method.equalsIgnoreCase(Request.INVITE)) { this.removeFromMergeTable((SIPServerTransaction) sipTransaction); } // Send a notification to the listener. SipProviderImpl sipProvider = (SipProviderImpl) sipTransaction.getSipProvider(); if (removed != null && sipTransaction.testAndSetTransactionTerminatedEvent()) { TransactionTerminatedEvent event = new TransactionTerminatedEvent(sipProvider, (ServerTransaction) sipTransaction); sipProvider.handleEvent(event, sipTransaction); } } else { String key = sipTransaction.getTransactionId(); Object removed = clientTransactionTable.remove(key); if (stackLogger.isLoggingEnabled()) { stackLogger.logDebug("REMOVED client tx " + removed + " KEY = " + key); if ( removed != null ) { SIPClientTransaction clientTx = (SIPClientTransaction)removed; if ( clientTx.getMethod().equals(Request.INVITE) && this.maxForkTime != 0 ) { RemoveForkedTransactionTimerTask ttask = new RemoveForkedTransactionTimerTask(clientTx); this.timer.schedule(ttask, this.maxForkTime * 1000); } } } // Send a notification to the listener. if (removed != null && sipTransaction.testAndSetTransactionTerminatedEvent()) { SipProviderImpl sipProvider = (SipProviderImpl) sipTransaction.getSipProvider(); TransactionTerminatedEvent event = new TransactionTerminatedEvent(sipProvider, (ClientTransaction) sipTransaction); sipProvider.handleEvent(event, sipTransaction); } } } /** * Add a new server transaction to the set of existing transactions. Add it to the top of the * list so an incoming ack has less work to do in order to find the transaction. * * @param serverTransaction -- server transaction to add to the set. */ public void addTransaction(SIPServerTransaction serverTransaction) throws IOException { if (stackLogger.isLoggingEnabled()) stackLogger.logDebug("added transaction " + serverTransaction); serverTransaction.map(); addTransactionHash(serverTransaction); } /** * Hash table for quick lookup of transactions. Here we wait for room if needed. */ private void addTransactionHash(SIPTransaction sipTransaction) { SIPRequest sipRequest = sipTransaction.getOriginalRequest(); if (sipTransaction instanceof SIPClientTransaction) { if (!this.unlimitedClientTransactionTableSize) { if (this.activeClientTransactionCount.get() > clientTransactionTableHiwaterMark) { try { synchronized (this.clientTransactionTable) { this.clientTransactionTable.wait(); this.activeClientTransactionCount.incrementAndGet(); } } catch (Exception ex) { if (stackLogger.isLoggingEnabled()) { stackLogger.logError("Exception occured while waiting for room", ex); } } } } else { this.activeClientTransactionCount.incrementAndGet(); } String key = sipRequest.getTransactionId(); clientTransactionTable.put(key, (SIPClientTransaction) sipTransaction); if (stackLogger.isLoggingEnabled()) { stackLogger.logDebug(" putTransactionHash : " + " key = " + key); } } else { String key = sipRequest.getTransactionId(); if (stackLogger.isLoggingEnabled()) { stackLogger.logDebug(" putTransactionHash : " + " key = " + key); } serverTransactionTable.put(key, (SIPServerTransaction) sipTransaction); } } /** * This method is called when a client tx transitions to the Completed or Terminated state. * */ protected void decrementActiveClientTransactionCount() { if (this.activeClientTransactionCount.decrementAndGet() <= this.clientTransactionTableLowaterMark && !this.unlimitedClientTransactionTableSize) { synchronized (this.clientTransactionTable) { clientTransactionTable.notify(); } } } /** * Remove the transaction from transaction hash. */ protected void removeTransactionHash(SIPTransaction sipTransaction) { SIPRequest sipRequest = sipTransaction.getOriginalRequest(); if (sipRequest == null) return; if (sipTransaction instanceof SIPClientTransaction) { String key = sipTransaction.getTransactionId(); if (stackLogger.isLoggingEnabled()) { stackLogger.logStackTrace(); stackLogger.logDebug("removing client Tx : " + key); } clientTransactionTable.remove(key); } else if (sipTransaction instanceof SIPServerTransaction) { String key = sipTransaction.getTransactionId(); serverTransactionTable.remove(key); if (stackLogger.isLoggingEnabled()) { stackLogger.logDebug("removing server Tx : " + key); } } } /** * Invoked when an error has ocurred with a transaction. * * @param transactionErrorEvent Error event. */ public synchronized void transactionErrorEvent(SIPTransactionErrorEvent transactionErrorEvent) { SIPTransaction transaction = (SIPTransaction) transactionErrorEvent.getSource(); if (transactionErrorEvent.getErrorID() == SIPTransactionErrorEvent.TRANSPORT_ERROR) { // Kill scanning of this transaction. transaction.setState(SIPTransaction.TERMINATED_STATE); if (transaction instanceof SIPServerTransaction) { // let the reaper get him ((SIPServerTransaction) transaction).collectionTime = 0; } transaction.disableTimeoutTimer(); transaction.disableRetransmissionTimer(); // Send a IO Exception to the Listener. } } /* * (non-Javadoc) * @see gov.nist.javax.sip.stack.SIPDialogEventListener#dialogErrorEvent(gov.nist.javax.sip.stack.SIPDialogErrorEvent) */ public synchronized void dialogErrorEvent(SIPDialogErrorEvent dialogErrorEvent) { SIPDialog sipDialog = (SIPDialog) dialogErrorEvent.getSource(); SipListener sipListener = ((SipStackImpl)this).getSipListener(); // if the app is not implementing the SipListenerExt interface we delete the dialog to avoid leaks if(sipDialog != null && !(sipListener instanceof SipListenerExt)) { sipDialog.delete(); } } /** * Stop stack. Clear all the timer stuff. Make the stack close all accept connections and * return. This is useful if you want to start/stop the stack several times from your * application. Caution : use of this function could cause peculiar bugs as messages are * prcessed asynchronously by the stack. */ public void stopStack() { // Prevent NPE on two concurrent stops if (this.timer != null) this.timer.cancel(); // JvB: set it to null, SIPDialog tries to schedule things after stop timer = null; this.pendingTransactions.clear(); this.toExit = true; synchronized (this) { this.notifyAll(); } synchronized (this.clientTransactionTable) { clientTransactionTable.notifyAll(); } synchronized (this.messageProcessors) { // Threads must periodically check this flag. MessageProcessor[] processorList; processorList = getMessageProcessors(); for (int processorIndex = 0; processorIndex < processorList.length; processorIndex++) { removeMessageProcessor(processorList[processorIndex]); } this.ioHandler.closeAll(); // Let the processing complete. } try { Thread.sleep(1000); } catch (InterruptedException ex) { } this.clientTransactionTable.clear(); this.serverTransactionTable.clear(); this.dialogTable.clear(); this.serverLogger.closeLogFile(); } /** * Put a transaction in the pending transaction list. This is to avoid a race condition when a * duplicate may arrive when the application is deciding whether to create a transaction or * not. */ public void putPendingTransaction(SIPServerTransaction tr) { if (stackLogger.isLoggingEnabled()) stackLogger.logDebug("putPendingTransaction: " + tr); this.pendingTransactions.put(tr.getTransactionId(), tr); } /** * Return the network layer (i.e. the interface for socket creation or the socket factory for * the stack). * * @return -- the registered Network Layer. */ public NetworkLayer getNetworkLayer() { if (networkLayer == null) { return DefaultNetworkLayer.SINGLETON; } else { return networkLayer; } } /** * Return true if logging is enabled for this stack. * * @return true if logging is enabled for this stack instance. */ public boolean isLoggingEnabled() { return this.stackLogger == null ? false : this.stackLogger.isLoggingEnabled(); } /** * Get the logger. * * @return --the logger for the sip stack. Each stack has its own logger instance. */ public StackLogger getStackLogger() { return this.stackLogger; } /** * Server log is the place where we log messages for the signaling trace viewer. * * @return -- the log file where messages are logged for viewing by the trace viewer. */ public ServerLogger getServerLogger() { return this.serverLogger; } /** * Maximum size of a single TCP message. Limiting the size of a single TCP message prevents * flooding attacks. * * @return the size of a single TCP message. */ public int getMaxMessageSize() { return this.maxMessageSize; } /** * Set the flag that instructs the stack to only start a single thread for sequentially * processing incoming udp messages (thus serializing the processing). Same as setting thread * pool size to 1. */ public void setSingleThreaded() { this.threadPoolSize = 1; } /** * Set the thread pool size for processing incoming UDP messages. Limit the total number of * threads for processing udp messages. * * @param size -- the thread pool size. * */ public void setThreadPoolSize(int size) { this.threadPoolSize = size; } /** * Set the max # of simultaneously handled TCP connections. * * @param nconnections -- the number of connections to handle. */ public void setMaxConnections(int nconnections) { this.maxConnections = nconnections; } /** * Get the default route string. * * @param sipRequest is the request for which we want to compute the next hop. * @throws SipException */ public Hop getNextHop(SIPRequest sipRequest) throws SipException { if (this.useRouterForAll) { // Use custom router to route all messages. if (router != null) return router.getNextHop(sipRequest); else return null; } else { // Also non-SIP request containing Route headers goes to the default // router if (sipRequest.getRequestURI().isSipURI() || sipRequest.getRouteHeaders() != null) { return defaultRouter.getNextHop(sipRequest); } else if (router != null) { return router.getNextHop(sipRequest); } else return null; } } /** * Set the descriptive name of the stack. * * @param stackName -- descriptive name of the stack. */ public void setStackName(String stackName) { this.stackName = stackName; } /** * Set my address. * * @param stackAddress -- A string containing the stack address. */ protected void setHostAddress(String stackAddress) throws UnknownHostException { if (stackAddress.indexOf(':') != stackAddress.lastIndexOf(':') && stackAddress.trim().charAt(0) != '[') this.stackAddress = '[' + stackAddress + ']'; else this.stackAddress = stackAddress; this.stackInetAddress = InetAddress.getByName(stackAddress); } /** * Get my address. * * @return hostAddress - my host address or null if no host address is defined. * @deprecated */ public String getHostAddress() { // JvB: for 1.2 this may return null... return this.stackAddress; } /** * Set the router algorithm. This is meant for routing messages out of dialog or for non-sip * uri's. * * @param router A class that implements the Router interface. */ protected void setRouter(Router router) { this.router = router; } /** * Get the router algorithm. * * @return Router router */ public Router getRouter(SIPRequest request) { if (request.getRequestLine() == null) { return this.defaultRouter; } else if (this.useRouterForAll) { return this.router; } else { if (request.getRequestURI().getScheme().equals("sip") || request.getRequestURI().getScheme().equals("sips")) { return this.defaultRouter; } else { if (this.router != null) return this.router; else return defaultRouter; } } } /* * (non-Javadoc) * * @see javax.sip.SipStack#getRouter() */ public Router getRouter() { return this.router; } /** * return the status of the toExit flag. * * @return true if the stack object is alive and false otherwise. */ public boolean isAlive() { return !toExit; } /** * Adds a new MessageProcessor to the list of running processors for this SIPStack and starts * it. You can use this method for dynamic stack configuration. */ protected void addMessageProcessor(MessageProcessor newMessageProcessor) throws IOException { synchronized (messageProcessors) { // Suggested changes by Jeyashankher, jai@lucent.com // newMessageProcessor.start() can fail // because a local port is not available // This throws an IOException. // We should not add the message processor to the // local list of processors unless the start() // call is successful. // newMessageProcessor.start(); messageProcessors.add(newMessageProcessor); } } /** * Removes a MessageProcessor from this SIPStack. * * @param oldMessageProcessor */ protected void removeMessageProcessor(MessageProcessor oldMessageProcessor) { synchronized (messageProcessors) { if (messageProcessors.remove(oldMessageProcessor)) { oldMessageProcessor.stop(); } } } /** * Gets an array of running MessageProcessors on this SIPStack. Acknowledgement: Jeff Keyser * suggested that applications should have access to the running message processors and * contributed this code. * * @return an array of running message processors. */ protected MessageProcessor[] getMessageProcessors() { synchronized (messageProcessors) { return (MessageProcessor[]) messageProcessors.toArray(new MessageProcessor[0]); } } /** * Creates the equivalent of a JAIN listening point and attaches to the stack. * * @param ipAddress -- ip address for the listening point. * @param port -- port for the listening point. * @param transport -- transport for the listening point. */ protected MessageProcessor createMessageProcessor(InetAddress ipAddress, int port, String transport) throws java.io.IOException { if (transport.equalsIgnoreCase("udp")) { UDPMessageProcessor udpMessageProcessor = new UDPMessageProcessor(ipAddress, this, port); this.addMessageProcessor(udpMessageProcessor); this.udpFlag = true; return udpMessageProcessor; } else if (transport.equalsIgnoreCase("tcp")) { TCPMessageProcessor tcpMessageProcessor = new TCPMessageProcessor(ipAddress, this, port); this.addMessageProcessor(tcpMessageProcessor); // this.tcpFlag = true; return tcpMessageProcessor; } else if (transport.equalsIgnoreCase("tls")) { TLSMessageProcessor tlsMessageProcessor = new TLSMessageProcessor(ipAddress, this, port); this.addMessageProcessor(tlsMessageProcessor); // this.tlsFlag = true; return tlsMessageProcessor; } else if (transport.equalsIgnoreCase("sctp")) { // Need Java 7 for this, so these classes are packaged in a separate jar // Try to load it indirectly, if fails report an error try { Class<?> mpc = ClassLoader.getSystemClassLoader().loadClass( "gov.nist.javax.sip.stack.sctp.SCTPMessageProcessor" ); MessageProcessor mp = (MessageProcessor) mpc.newInstance(); mp.initialize( ipAddress, port, this ); this.addMessageProcessor(mp); return mp; } catch (ClassNotFoundException e) { throw new IllegalArgumentException("SCTP not supported (needs Java 7 and SCTP jar in classpath)"); } catch ( InstantiationException ie ) { throw new IllegalArgumentException("Error initializing SCTP", ie); } catch ( IllegalAccessException ie ) { throw new IllegalArgumentException("Error initializing SCTP", ie); } } else { throw new IllegalArgumentException("bad transport"); } } /** * Set the message factory. * * @param messageFactory -- messageFactory to set. */ protected void setMessageFactory(StackMessageFactory messageFactory) { this.sipMessageFactory = messageFactory; } /** * Creates a new MessageChannel for a given Hop. * * @param sourceIpAddress - Ip address of the source of this message. * * @param sourcePort - source port of the message channel to be created. * * @param nextHop Hop to create a MessageChannel to. * * @return A MessageChannel to the specified Hop, or null if no MessageProcessors support * contacting that Hop. * * @throws UnknownHostException If the host in the Hop doesn't exist. */ public MessageChannel createRawMessageChannel(String sourceIpAddress, int sourcePort, Hop nextHop) throws UnknownHostException { Host targetHost; HostPort targetHostPort; Iterator processorIterator; MessageProcessor nextProcessor; MessageChannel newChannel; // Create the host/port of the target hop targetHost = new Host(); targetHost.setHostname(nextHop.getHost()); targetHostPort = new HostPort(); targetHostPort.setHost(targetHost); targetHostPort.setPort(nextHop.getPort()); // Search each processor for the correct transport newChannel = null; processorIterator = messageProcessors.iterator(); while (processorIterator.hasNext() && newChannel == null) { nextProcessor = (MessageProcessor) processorIterator.next(); // If a processor that supports the correct // transport is found, if (nextHop.getTransport().equalsIgnoreCase(nextProcessor.getTransport()) && sourceIpAddress.equals(nextProcessor.getIpAddress().getHostAddress()) && sourcePort == nextProcessor.getPort()) { try { // Create a channel to the target // host/port newChannel = nextProcessor.createMessageChannel(targetHostPort); } catch (UnknownHostException ex) { if (stackLogger.isLoggingEnabled()) stackLogger.logException(ex); throw ex; } catch (IOException e) { if (stackLogger.isLoggingEnabled()) stackLogger.logException(e); // Ignore channel creation error - // try next processor } } } // Return the newly-created channel return newChannel; } /** * Return true if a given event can result in a forked subscription. The stack is configured * with a set of event names that can result in forked subscriptions. * * @param ename -- event name to check. * */ public boolean isEventForked(String ename) { if (stackLogger.isLoggingEnabled()) { stackLogger.logDebug("isEventForked: " + ename + " returning " + this.forkedEvents.contains(ename)); } return this.forkedEvents.contains(ename); } /** * get the address resolver interface. * * @return -- the registered address resolver. */ public AddressResolver getAddressResolver() { return this.addressResolver; } /** * Set the address resolution interface * * @param addressResolver -- the address resolver to set. */ public void setAddressResolver(AddressResolver addressResolver) { this.addressResolver = addressResolver; } /** * Set the logger factory. * * @param logRecordFactory -- the log record factory to set. */ public void setLogRecordFactory(LogRecordFactory logRecordFactory) { this.logRecordFactory = logRecordFactory; } /** * get the thread auditor object * * @return -- the thread auditor of the stack */ public ThreadAuditor getThreadAuditor() { return this.threadAuditor; } // / // / Stack Audit methods // / /** * Audits the SIP Stack for leaks * * @return Audit report, null if no leaks were found */ public String auditStack(Set activeCallIDs, long leakedDialogTimer, long leakedTransactionTimer) { String auditReport = null; String leakedDialogs = auditDialogs(activeCallIDs, leakedDialogTimer); String leakedServerTransactions = auditTransactions(serverTransactionTable, leakedTransactionTimer); String leakedClientTransactions = auditTransactions(clientTransactionTable, leakedTransactionTimer); if (leakedDialogs != null || leakedServerTransactions != null || leakedClientTransactions != null) { auditReport = "SIP Stack Audit:\n" + (leakedDialogs != null ? leakedDialogs : "") + (leakedServerTransactions != null ? leakedServerTransactions : "") + (leakedClientTransactions != null ? leakedClientTransactions : ""); } return auditReport; } /** * Audits SIP dialogs for leaks - Compares the dialogs in the dialogTable with a list of Call * IDs passed by the application. - Dialogs that are not known by the application are leak * suspects. - Kill the dialogs that are still around after the timer specified. * * @return Audit report, null if no dialog leaks were found */ private String auditDialogs(Set activeCallIDs, long leakedDialogTimer) { String auditReport = " Leaked dialogs:\n"; int leakedDialogs = 0; long currentTime = System.currentTimeMillis(); // Make a shallow copy of the dialog list. // This copy will remain intact as leaked dialogs are removed by the // stack. LinkedList dialogs; synchronized (dialogTable) { dialogs = new LinkedList(dialogTable.values()); } // Iterate through the dialogDialog, get the callID of each dialog and // check if it's in the // list of active calls passed by the application. If it isn't, start // the timer on it. // If the timer has expired, kill the dialog. Iterator it = dialogs.iterator(); while (it.hasNext()) { // Get the next dialog SIPDialog itDialog = (SIPDialog) it.next(); // Get the call id associated with this dialog CallIdHeader callIdHeader = (itDialog != null ? itDialog.getCallId() : null); String callID = (callIdHeader != null ? callIdHeader.getCallId() : null); // Check if the application knows about this call id if (itDialog != null && callID != null && !activeCallIDs.contains(callID)) { // Application doesn't know anything about this dialog... if (itDialog.auditTag == 0) { // Mark this dialog as suspect itDialog.auditTag = currentTime; } else { // We already audited this dialog before. Check if his // time's up. if (currentTime - itDialog.auditTag >= leakedDialogTimer) { // Leaked dialog found leakedDialogs++; // Generate report DialogState dialogState = itDialog.getState(); String dialogReport = "dialog id: " + itDialog.getDialogId() + ", dialog state: " + (dialogState != null ? dialogState.toString() : "null"); auditReport += " " + dialogReport + "\n"; // Kill it itDialog.setState(SIPDialog.TERMINATED_STATE); if (stackLogger.isLoggingEnabled()) stackLogger.logDebug("auditDialogs: leaked " + dialogReport); } } } } // Return final report if (leakedDialogs > 0) { auditReport += " Total: " + Integer.toString(leakedDialogs) + " leaked dialogs detected and removed.\n"; } else { auditReport = null; } return auditReport; } /** * Audits SIP transactions for leaks * * @return Audit report, null if no transaction leaks were found */ private String auditTransactions(ConcurrentHashMap transactionsMap, long a_nLeakedTransactionTimer) { String auditReport = " Leaked transactions:\n"; int leakedTransactions = 0; long currentTime = System.currentTimeMillis(); // Make a shallow copy of the transaction list. // This copy will remain intact as leaked transactions are removed by // the stack. LinkedList transactionsList = new LinkedList(transactionsMap.values()); // Iterate through our copy Iterator it = transactionsList.iterator(); while (it.hasNext()) { SIPTransaction sipTransaction = (SIPTransaction) it.next(); if (sipTransaction != null) { if (sipTransaction.auditTag == 0) { // First time we see this transaction. Mark it as audited. sipTransaction.auditTag = currentTime; } else { // We've seen this transaction before. Check if his time's // up. if (currentTime - sipTransaction.auditTag >= a_nLeakedTransactionTimer) { // Leaked transaction found leakedTransactions++; // Generate some report TransactionState transactionState = sipTransaction.getState(); SIPRequest origRequest = sipTransaction.getOriginalRequest(); String origRequestMethod = (origRequest != null ? origRequest.getMethod() : null); String transactionReport = sipTransaction.getClass().getName() + ", state: " + (transactionState != null ? transactionState.toString() : "null") + ", OR: " + (origRequestMethod != null ? origRequestMethod : "null"); auditReport += " " + transactionReport + "\n"; // Kill it removeTransaction(sipTransaction); if (isLoggingEnabled()) stackLogger.logDebug("auditTransactions: leaked " + transactionReport); } } } } // Return final report if (leakedTransactions > 0) { auditReport += " Total: " + Integer.toString(leakedTransactions) + " leaked transactions detected and removed.\n"; } else { auditReport = null; } return auditReport; } public void setNon2XXAckPassedToListener(boolean passToListener) { this.non2XXAckPassedToListener = passToListener; } /** * @return the non2XXAckPassedToListener */ public boolean isNon2XXAckPassedToListener() { return non2XXAckPassedToListener; } /** * Get the count of client transactions that is not in the completed or terminated state. * * @return the activeClientTransactionCount */ public int getActiveClientTransactionCount() { return activeClientTransactionCount.get(); } public boolean isRfc2543Supported() { return this.rfc2543Supported; } public boolean isCancelClientTransactionChecked() { return this.cancelClientTransactionChecked; } public boolean isRemoteTagReassignmentAllowed() { return this.remoteTagReassignmentAllowed; } /** * This method is slated for addition to the next spec revision. * * * @return -- the collection of dialogs that is being managed by the stack. */ public Collection<Dialog> getDialogs() { HashSet<Dialog> dialogs = new HashSet<Dialog>(); dialogs.addAll(this.dialogTable.values()); dialogs.addAll(this.earlyDialogTable.values()); return dialogs; } /** * * @return -- the collection of dialogs matching the state that is being managed by the stack. */ public Collection<Dialog> getDialogs(DialogState state) { HashSet<Dialog> matchingDialogs = new HashSet<Dialog>(); if (DialogState.EARLY.equals(state)) { matchingDialogs.addAll(this.earlyDialogTable.values()); } else { Collection<SIPDialog> dialogs = dialogTable.values(); for (SIPDialog dialog : dialogs) { if (dialog.getState() != null && dialog.getState().equals(state)) { matchingDialogs.add(dialog); } } } return matchingDialogs; } /** * Get the Replaced Dialog from the stack. * * @param replacesHeader -- the header that references the dialog being replaced. */ public Dialog getReplacesDialog(ReplacesHeader replacesHeader) { String cid = replacesHeader.getCallId(); String fromTag = replacesHeader.getFromTag(); String toTag = replacesHeader.getToTag(); StringBuffer dialogId = new StringBuffer(cid); // retval.append(COLON).append(to.getUserAtHostPort()); if (toTag != null) { dialogId.append(":"); dialogId.append(toTag); } // retval.append(COLON).append(from.getUserAtHostPort()); if (fromTag != null) { dialogId.append(":"); dialogId.append(fromTag); } String did = dialogId.toString().toLowerCase(); if (stackLogger.isLoggingEnabled()) stackLogger.logDebug("Looking for dialog " + did); /* * Check if we can find this dialog in our dialog table. */ Dialog replacesDialog = this.dialogTable.get(did); /* * This could be a forked dialog. Search for it. */ if ( replacesDialog == null ) { for ( SIPClientTransaction ctx : this.clientTransactionTable.values()) { if ( ctx.getDialog(did) != null ) { replacesDialog = ctx.getDialog(did); break; } } } return replacesDialog; } /** * Get the Join Dialog from the stack. * * @param joinHeader -- the header that references the dialog being joined. */ public Dialog getJoinDialog(JoinHeader joinHeader) { String cid = joinHeader.getCallId(); String fromTag = joinHeader.getFromTag(); String toTag = joinHeader.getToTag(); StringBuffer retval = new StringBuffer(cid); // retval.append(COLON).append(to.getUserAtHostPort()); if (toTag != null) { retval.append(":"); retval.append(toTag); } // retval.append(COLON).append(from.getUserAtHostPort()); if (fromTag != null) { retval.append(":"); retval.append(fromTag); } return this.dialogTable.get(retval.toString().toLowerCase()); } /** * @param timer the timer to set */ public void setTimer(Timer timer) { this.timer = timer; } /** * @return the timer */ public Timer getTimer() { return timer; } /** * Size of the receive UDP buffer. This property affects performance under load. Bigger buffer * is better under load. * * @return */ public int getReceiveUdpBufferSize() { return receiveUdpBufferSize; } /** * Size of the receive UDP buffer. This property affects performance under load. Bigger buffer * is better under load. * * @return */ public void setReceiveUdpBufferSize(int receiveUdpBufferSize) { this.receiveUdpBufferSize = receiveUdpBufferSize; } /** * Size of the send UDP buffer. This property affects performance under load. Bigger buffer * is better under load. * * @return */ public int getSendUdpBufferSize() { return sendUdpBufferSize; } /** * Size of the send UDP buffer. This property affects performance under load. Bigger buffer * is better under load. * * @return */ public void setSendUdpBufferSize(int sendUdpBufferSize) { this.sendUdpBufferSize = sendUdpBufferSize; } /** * @param stackLogger the stackLogger to set */ public void setStackLogger(StackLogger stackLogger) { this.stackLogger = stackLogger; } /** * Flag that reqests checking of branch IDs on responses. * * @return */ public boolean checkBranchId() { return this.checkBranchId; } /** * @param logStackTraceOnMessageSend the logStackTraceOnMessageSend to set */ public void setLogStackTraceOnMessageSend(boolean logStackTraceOnMessageSend) { this.logStackTraceOnMessageSend = logStackTraceOnMessageSend; } /** * @return the logStackTraceOnMessageSend */ public boolean isLogStackTraceOnMessageSend() { return logStackTraceOnMessageSend; } public void setDeliverDialogTerminatedEventForNullDialog() { this.isDialogTerminatedEventDeliveredForNullDialog = true; } public void addForkedClientTransaction(SIPClientTransaction clientTransaction) { this.forkedClientTransactionTable.put(clientTransaction.getTransactionId(), clientTransaction ); } public SIPClientTransaction getForkedTransaction(String transactionId) { return this.forkedClientTransactionTable.get(transactionId); } }