2493 lines
89 KiB
Java
2493 lines
89 KiB
Java
/*
|
|
* 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);
|
|
}
|
|
|
|
|
|
}
|