blob: 1788bbe8131515e5babfa7a45c849fa4f0bc278e [file] [log] [blame]
/*
* 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);
}
}