/* * 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.InternalErrorHandler; import gov.nist.javax.sip.SIPConstants; import gov.nist.javax.sip.ServerTransactionExt; import gov.nist.javax.sip.SipProviderImpl; import gov.nist.javax.sip.Utils; import gov.nist.javax.sip.header.Expires; import gov.nist.javax.sip.header.ParameterNames; import gov.nist.javax.sip.header.RSeq; import gov.nist.javax.sip.header.Via; import gov.nist.javax.sip.header.ViaList; 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.text.ParseException; import java.util.TimerTask; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import javax.sip.Dialog; import javax.sip.DialogState; import javax.sip.DialogTerminatedEvent; import javax.sip.ObjectInUseException; import javax.sip.SipException; import javax.sip.Timeout; import javax.sip.TimeoutEvent; import javax.sip.TransactionState; import javax.sip.address.Hop; import javax.sip.header.ContactHeader; import javax.sip.header.ExpiresHeader; import javax.sip.header.RSeqHeader; import javax.sip.message.Request; import javax.sip.message.Response; /* * Bug fixes / enhancements:Emil Ivov, Antonis Karydas, Daniel J. Martinez Manzano, Daniel, Hagai * Sela, Vazques-Illa, Bill Roome, Thomas Froment and Pierre De Rop, Christophe Anzille and Jeroen * van Bemmel, Frank Reif. * Carolyn Beeton ( Avaya ). * */ /** * Represents a server transaction. Implements the following state machines. * *
* * * * |INVITE * |pass INV to TU * INVITE V send 100 if TU won't in 200ms * send response+-----------+ * +--------| |--------+101-199 from TU * | | Proceeding| |send response * +------->| |<-------+ * | | Transport Err. * | | Inform TU * | |--------------->+ * +-----------+ | * 300-699 from TU | |2xx from TU | * send response | |send response | * | +------------------>+ * | | * INVITE V Timer G fires | * send response+-----------+ send response | * +--------| |--------+ | * | | Completed | | | * +------->| |<-------+ | * +-----------+ | * | | | * ACK | | | * - | +------------------>+ * | Timer H fires | * V or Transport Err.| * +-----------+ Inform TU | * | | | * | Confirmed | | * | | | * +-----------+ | * | | * |Timer I fires | * |- | * | | * V | * +-----------+ | * | | | * | Terminated|<---------------+ * | | * +-----------+ * * Figure 7: INVITE server transaction * Request received * |pass to TU * * V * +-----------+ * | | * | Trying |-------------+ * | | | * +-----------+ |200-699 from TU * | |send response * |1xx from TU | * |send response | * | | * Request V 1xx from TU | * send response+-----------+send response| * +--------| |--------+ | * | | Proceeding| | | * +------->| |<-------+ | * +<--------------| | | * |Trnsprt Err +-----------+ | * |Inform TU | | * | | | * | |200-699 from TU | * | |send response | * | Request V | * | send response+-----------+ | * | +--------| | | * | | | Completed |<------------+ * | +------->| | * +<--------------| | * |Trnsprt Err +-----------+ * |Inform TU | * | |Timer J fires * | |- * | | * | V * | +-----------+ * | | | * +-------------->| Terminated| * | | * +-----------+ * * * * * ** * @version 1.2 $Revision: 1.118 $ $Date: 2010/01/10 00:13:14 $ * @author M. Ranganathan * */ public class SIPServerTransaction extends SIPTransaction implements ServerRequestInterface, javax.sip.ServerTransaction, ServerTransactionExt { // force the listener to see transaction private int rseqNumber; // private LinkedList pendingRequests; // Real RequestInterface to pass messages to private transient ServerRequestInterface requestOf; private SIPDialog dialog; // the unacknowledged SIPResponse private SIPResponse pendingReliableResponse; // The pending reliable Response Timer private ProvisionalResponseTask provisionalResponseTask; private boolean retransmissionAlertEnabled; private RetransmissionAlertTimerTask retransmissionAlertTimerTask; protected boolean isAckSeen; private SIPClientTransaction pendingSubscribeTransaction; private SIPServerTransaction inviteTransaction; private Semaphore provisionalResponseSem = new Semaphore(1); /** * This timer task is used for alerting the application to send retransmission alerts. * * */ class RetransmissionAlertTimerTask extends SIPStackTimerTask { String dialogId; int ticks; int ticksLeft; public RetransmissionAlertTimerTask(String dialogId) { this.ticks = SIPTransaction.T1; this.ticksLeft = this.ticks; } protected void runTask() { SIPServerTransaction serverTransaction = SIPServerTransaction.this; ticksLeft--; if (ticksLeft == -1) { serverTransaction.fireRetransmissionTimer(); this.ticksLeft = 2 * ticks; } } } class ProvisionalResponseTask extends SIPStackTimerTask { int ticks; int ticksLeft; public ProvisionalResponseTask() { this.ticks = SIPTransaction.T1; this.ticksLeft = this.ticks; } protected void runTask() { SIPServerTransaction serverTransaction = SIPServerTransaction.this; /* * The reliable provisional response is passed to the transaction layer periodically * with an interval that starts at T1 seconds and doubles for each retransmission (T1 * is defined in Section 17 of RFC 3261). Once passed to the server transaction, it is * added to an internal list of unacknowledged reliable provisional responses. The * transaction layer will forward each retransmission passed from the UAS core. * * This differs from retransmissions of 2xx responses, whose intervals cap at T2 * seconds. This is because retransmissions of ACK are triggered on receipt of a 2xx, * but retransmissions of PRACK take place independently of reception of 1xx. */ // If the transaction has terminated, if (serverTransaction.isTerminated()) { this.cancel(); } else { ticksLeft--; if (ticksLeft == -1) { serverTransaction.fireReliableResponseRetransmissionTimer(); this.ticksLeft = 2 * ticks; this.ticks = this.ticksLeft; // timer H MUST be set to fire in 64*T1 seconds for all transports. Timer H // determines when the server // transaction abandons retransmitting the response if (this.ticksLeft >= SIPTransaction.TIMER_H) { this.cancel(); setState(TERMINATED_STATE); fireTimeoutTimer(); } } } } } /** * This timer task will terminate the transaction if the listener does not respond in a * pre-determined time period. This helps prevent buggy listeners (who fail to respond) from * causing memory leaks. This allows a container to protect itself from buggy code ( that * fails to respond to a server transaction). * */ class ListenerExecutionMaxTimer extends SIPStackTimerTask { SIPServerTransaction serverTransaction = SIPServerTransaction.this; ListenerExecutionMaxTimer() { } protected void runTask() { try { if (serverTransaction.getState() == null) { serverTransaction.terminate(); SIPTransactionStack sipStack = serverTransaction.getSIPStack(); sipStack.removePendingTransaction(serverTransaction); sipStack.removeTransaction(serverTransaction); } } catch (Exception ex) { sipStack.getStackLogger().logError("unexpected exception", ex); } } } /** * This timer task is for INVITE server transactions. It will send a trying in 200 ms. if the * TU does not do so. * */ class SendTrying extends SIPStackTimerTask { protected SendTrying() { if (sipStack.isLoggingEnabled()) sipStack.getStackLogger().logDebug("scheduled timer for " + SIPServerTransaction.this); } protected void runTask() { SIPServerTransaction serverTransaction = SIPServerTransaction.this; TransactionState realState = serverTransaction.getRealState(); if (realState == null || TransactionState.TRYING == realState) { if (sipStack.isLoggingEnabled()) sipStack.getStackLogger().logDebug(" sending Trying current state = " + serverTransaction.getRealState()); try { serverTransaction.sendMessage(serverTransaction.getOriginalRequest() .createResponse(100, "Trying")); if (sipStack.isLoggingEnabled()) sipStack.getStackLogger().logDebug(" trying sent " + serverTransaction.getRealState()); } catch (IOException ex) { if (sipStack.isLoggingEnabled()) sipStack.getStackLogger().logError("IO error sending TRYING"); } } } } class TransactionTimer extends SIPStackTimerTask { public TransactionTimer() { if (sipStack.isLoggingEnabled()) { sipStack.getStackLogger().logDebug("TransactionTimer() : " + getTransactionId()); } } protected void runTask() { // If the transaction has terminated, if (isTerminated()) { // Keep the transaction hanging around in the transaction table // to catch the incoming ACK -- this is needed for tcp only. // Note that the transaction record is actually removed in // the connection linger timer. try { this.cancel(); } catch (IllegalStateException ex) { if (!sipStack.isAlive()) return; } // Oneshot timer that garbage collects the SeverTransaction // after a scheduled amount of time. The linger timer allows // the client side of the tx to use the same connection to // send an ACK and prevents a race condition for creation // of new server tx TimerTask myTimer = new LingerTimer(); sipStack.getTimer().schedule(myTimer, SIPTransactionStack.CONNECTION_LINGER_TIME * 1000); } else { // Add to the fire list -- needs to be moved // outside the synchronized block to prevent // deadlock. fireTimer(); } } } /** * Send a response. * * @param transactionResponse -- the response to send * */ private void sendResponse(SIPResponse transactionResponse) throws IOException { try { // RFC18.2.2. Sending Responses // The server transport uses the value of the top Via header field // in // order // to determine where to send a response. // It MUST follow the following process: // If the "sent-protocol" is a reliable transport // protocol such as TCP or SCTP, // or TLS over those, the response MUST be // sent using the existing connection // to the source of the original request // that created the transaction, if that connection is still open. if (isReliable()) { getMessageChannel().sendMessage(transactionResponse); // TODO If that connection attempt fails, the server SHOULD // use SRV 3263 procedures // for servers in order to determine the IP address // and port to open the connection and send the response to. } else { Via via = transactionResponse.getTopmostVia(); String transport = via.getTransport(); if (transport == null) throw new IOException("missing transport!"); // @@@ hagai Symmetric NAT support int port = via.getRPort(); if (port == -1) port = via.getPort(); if (port == -1) { if (transport.equalsIgnoreCase("TLS")) port = 5061; else port = 5060; } // Otherwise, if the Via header field value contains a // "maddr" parameter, the response MUST be forwarded to // the address listed there, using the port indicated in // "sent-by", // or port 5060 if none is present. If the address is a // multicast // address, the response SHOULD be sent using // the TTL indicated in the "ttl" parameter, or with a // TTL of 1 if that parameter is not present. String host = null; if (via.getMAddr() != null) { host = via.getMAddr(); } else { // Otherwise (for unreliable unicast transports), // if the top Via has a "received" parameter, the response // MUST // be sent to the // address in the "received" parameter, using the port // indicated // in the // "sent-by" value, or using port 5060 if none is specified // explicitly. host = via.getParameter(Via.RECEIVED); if (host == null) { // Otherwise, if it is not receiver-tagged, the response // MUST be // sent to the address indicated by the "sent-by" value, // using the procedures in Section 5 // RFC 3263 PROCEDURE TO BE DONE HERE host = via.getHost(); } } Hop hop = sipStack.addressResolver.resolveAddress(new HopImpl(host, port, transport)); MessageChannel messageChannel = ((SIPTransactionStack) getSIPStack()) .createRawMessageChannel(this.getSipProvider().getListeningPoint( hop.getTransport()).getIPAddress(), this.getPort(), hop); if (messageChannel != null) messageChannel.sendMessage(transactionResponse); else throw new IOException("Could not create a message channel for " + hop); } } finally { this.startTransactionTimer(); } } /** * Creates a new server transaction. * * @param sipStack Transaction stack this transaction belongs to. * @param newChannelToUse Channel to encapsulate. */ protected SIPServerTransaction(SIPTransactionStack sipStack, MessageChannel newChannelToUse) { super(sipStack, newChannelToUse); if (sipStack.maxListenerResponseTime != -1) { sipStack.getTimer().schedule(new ListenerExecutionMaxTimer(), sipStack.maxListenerResponseTime * 1000); } this.rseqNumber = (int) (Math.random() * 1000); // Only one outstanding request for a given server tx. if (sipStack.isLoggingEnabled()) { sipStack.getStackLogger().logDebug("Creating Server Transaction" + this.getBranchId()); sipStack.getStackLogger().logStackTrace(); } } /** * Sets the real RequestInterface this transaction encapsulates. * * @param newRequestOf RequestInterface to send messages to. */ public void setRequestInterface(ServerRequestInterface newRequestOf) { requestOf = newRequestOf; } /** * Returns this transaction. */ public MessageChannel getResponseChannel() { return this; } /** * Determines if the message is a part of this transaction. * * @param messageToTest Message to check if it is part of this transaction. * * @return True if the message is part of this transaction, false if not. */ public boolean isMessagePartOfTransaction(SIPMessage messageToTest) { // List of Via headers in the message to test ViaList viaHeaders; // Topmost Via header in the list Via topViaHeader; // Branch code in the topmost Via header String messageBranch; // Flags whether the select message is part of this transaction boolean transactionMatches; transactionMatches = false; String method = messageToTest.getCSeq().getMethod(); // Invite Server transactions linger in the terminated state in the // transaction // table and are matched to compensate for // http://bugs.sipit.net/show_bug.cgi?id=769 if ((method.equals(Request.INVITE) || !isTerminated())) { // Get the topmost Via header and its branch parameter viaHeaders = messageToTest.getViaHeaders(); if (viaHeaders != null) { topViaHeader = (Via) viaHeaders.getFirst(); messageBranch = topViaHeader.getBranch(); if (messageBranch != null) { // If the branch parameter exists but // does not start with the magic cookie, if (!messageBranch.toLowerCase().startsWith( SIPConstants.BRANCH_MAGIC_COOKIE_LOWER_CASE)) { // Flags this as old // (RFC2543-compatible) client // version messageBranch = null; } } // If a new branch parameter exists, if (messageBranch != null && this.getBranch() != null) { if (method.equals(Request.CANCEL)) { // Cancel is handled as a special case because it // shares the same same branch id of the invite // that it is trying to cancel. transactionMatches = this.getMethod().equals(Request.CANCEL) && getBranch().equalsIgnoreCase(messageBranch) && topViaHeader.getSentBy().equals( ((Via) getOriginalRequest().getViaHeaders().getFirst()) .getSentBy()); } else { // Matching server side transaction with only the // branch parameter. transactionMatches = getBranch().equalsIgnoreCase(messageBranch) && topViaHeader.getSentBy().equals( ((Via) getOriginalRequest().getViaHeaders().getFirst()) .getSentBy()); } } else { // This is an RFC2543-compliant message; this code is here // for backwards compatibility. // It is a weak check. // If RequestURI, To tag, From tag, CallID, CSeq number, and // top Via headers are the same, the // SIPMessage matches this transaction. An exception is for // a CANCEL request, which is not deemed // to be part of an otherwise-matching INVITE transaction. String originalFromTag = super.fromTag; String thisFromTag = messageToTest.getFrom().getTag(); boolean skipFrom = (originalFromTag == null || thisFromTag == null); String originalToTag = super.toTag; String thisToTag = messageToTest.getTo().getTag(); boolean skipTo = (originalToTag == null || thisToTag == null); boolean isResponse = (messageToTest instanceof SIPResponse); // Issue #96: special case handling for a CANCEL request - // the CSeq method of the original request must // be CANCEL for it to have a chance at matching. if (messageToTest.getCSeq().getMethod().equalsIgnoreCase(Request.CANCEL) && !getOriginalRequest().getCSeq().getMethod().equalsIgnoreCase( Request.CANCEL)) { transactionMatches = false; } else if ((isResponse || getOriginalRequest().getRequestURI().equals( ((SIPRequest) messageToTest).getRequestURI())) && (skipFrom || originalFromTag != null && originalFromTag.equalsIgnoreCase(thisFromTag)) && (skipTo || originalToTag != null && originalToTag.equalsIgnoreCase(thisToTag)) && getOriginalRequest().getCallId().getCallId().equalsIgnoreCase( messageToTest.getCallId().getCallId()) && getOriginalRequest().getCSeq().getSeqNumber() == messageToTest .getCSeq().getSeqNumber() && ((!messageToTest.getCSeq().getMethod().equals(Request.CANCEL)) || getOriginalRequest() .getMethod().equals(messageToTest.getCSeq().getMethod())) && topViaHeader.equals(getOriginalRequest().getViaHeaders() .getFirst())) { transactionMatches = true; } } } } return transactionMatches; } /** * Send out a trying response (only happens when the transaction is mapped). Otherwise the * transaction is not known to the stack. */ protected void map() { // note that TRYING is a pseudo-state for invite transactions TransactionState realState = getRealState(); if (realState == null || realState == TransactionState.TRYING) { // JvB: Removed the condition 'dialog!=null'. Trying should also // be // sent by intermediate proxies. This fixes some TCK tests // null check added as the stack may be stopped. if (isInviteTransaction() && !this.isMapped && sipStack.getTimer() != null) { this.isMapped = true; // Schedule a timer to fire in 200 ms if the // TU did not send a trying in that time. sipStack.getTimer().schedule(new SendTrying(), 200); } else { isMapped = true; } } // Pull it out of the pending transactions list. sipStack.removePendingTransaction(this); } /** * Return true if the transaction is known to stack. */ public boolean isTransactionMapped() { return this.isMapped; } /** * Process a new request message through this transaction. If necessary, this message will * also be passed onto the TU. * * @param transactionRequest Request to process. * @param sourceChannel Channel that received this message. */ public void processRequest(SIPRequest transactionRequest, MessageChannel sourceChannel) { boolean toTu = false; // Can only process a single request directed to the // transaction at a time. For a given server transaction // the listener sees only one event at a time. if (sipStack.isLoggingEnabled()) { sipStack.getStackLogger().logDebug("processRequest: " + transactionRequest.getFirstLine()); sipStack.getStackLogger().logDebug("tx state = " + this.getRealState()); } try { // If this is the first request for this transaction, if (getRealState() == null) { // Save this request as the one this // transaction is handling setOriginalRequest(transactionRequest); this.setState(TransactionState.TRYING); toTu = true; this.setPassToListener(); // Rsends the TRYING on retransmission of the request. if (isInviteTransaction() && this.isMapped) { // JvB: also // proxies need // to do this // Has side-effect of setting // state to "Proceeding" sendMessage(transactionRequest.createResponse(100, "Trying")); } // If an invite transaction is ACK'ed while in // the completed state, } else if (isInviteTransaction() && TransactionState.COMPLETED == getRealState() && transactionRequest.getMethod().equals(Request.ACK)) { // @jvB bug fix this.setState(TransactionState.CONFIRMED); disableRetransmissionTimer(); if (!isReliable()) { enableTimeoutTimer(TIMER_I); } else { this.setState(TransactionState.TERMINATED); } // JvB: For the purpose of testing a TI, added a property to // pass it anyway if (sipStack.isNon2XXAckPassedToListener()) { // This is useful for test applications that want to see // all messages. requestOf.processRequest(transactionRequest, this); } else { // According to RFC3261 Application should not Ack in // CONFIRMED state if (sipStack.isLoggingEnabled()) { sipStack.getStackLogger().logDebug("ACK received for server Tx " + this.getTransactionId() + " not delivering to application!"); } this.semRelease(); } return; // If we receive a retransmission of the original // request, } else if (transactionRequest.getMethod().equals(getOriginalRequest().getMethod())) { if (TransactionState.PROCEEDING == getRealState() || TransactionState.COMPLETED == getRealState()) { this.semRelease(); // Resend the last response to // the client if (lastResponse != null) { // Send the message to the client super.sendMessage(lastResponse); } } else if (transactionRequest.getMethod().equals(Request.ACK)) { // This is passed up to the TU to suppress // retransmission of OK if (requestOf != null) requestOf.processRequest(transactionRequest, this); else this.semRelease(); } if (sipStack.isLoggingEnabled()) sipStack.getStackLogger().logDebug("completed processing retransmitted request : " + transactionRequest.getFirstLine() + this + " txState = " + this.getState() + " lastResponse = " + this.getLastResponse()); return; } // Pass message to the TU if (TransactionState.COMPLETED != getRealState() && TransactionState.TERMINATED != getRealState() && requestOf != null) { if (getOriginalRequest().getMethod().equals(transactionRequest.getMethod())) { // Only send original request to TU once! if (toTu) { requestOf.processRequest(transactionRequest, this); } else this.semRelease(); } else { if (requestOf != null) requestOf.processRequest(transactionRequest, this); else this.semRelease(); } } else { // This seems like a common bug so I am allowing it through! if (((SIPTransactionStack) getSIPStack()).isDialogCreated(getOriginalRequest() .getMethod()) && getRealState() == TransactionState.TERMINATED && transactionRequest.getMethod().equals(Request.ACK) && requestOf != null) { SIPDialog thisDialog = (SIPDialog) this.dialog; if (thisDialog == null || !thisDialog.ackProcessed) { // Filter out duplicate acks if (thisDialog != null) { thisDialog.ackReceived(transactionRequest); thisDialog.ackProcessed = true; } requestOf.processRequest(transactionRequest, this); } else { this.semRelease(); } } else if (transactionRequest.getMethod().equals(Request.CANCEL)) { if (sipStack.isLoggingEnabled()) sipStack.getStackLogger().logDebug("Too late to cancel Transaction"); this.semRelease(); // send OK and just ignore the CANCEL. try { this.sendMessage(transactionRequest.createResponse(Response.OK)); } catch (IOException ex) { // Transaction is already terminated // just ignore the IOException. } } if (sipStack.isLoggingEnabled()) sipStack.getStackLogger().logDebug("Dropping request " + getRealState()); } } catch (IOException e) { if (sipStack.isLoggingEnabled()) sipStack.getStackLogger().logError("IOException " ,e); this.semRelease(); this.raiseIOExceptionEvent(); } } /** * Send a response message through this transactionand onto the client. The response drives * the state machine. * * @param messageToSend Response to process and send. */ public void sendMessage(SIPMessage messageToSend) throws IOException { try { // Message typecast as a response SIPResponse transactionResponse; // Status code of the response being sent to the client int statusCode; // Get the status code from the response transactionResponse = (SIPResponse) messageToSend; statusCode = transactionResponse.getStatusCode(); try { // Provided we have set the banch id for this we set the BID for // the // outgoing via. if (this.getOriginalRequest().getTopmostVia().getBranch() != null) transactionResponse.getTopmostVia().setBranch(this.getBranch()); else transactionResponse.getTopmostVia().removeParameter(ParameterNames.BRANCH); // Make the topmost via headers match identically for the // transaction rsponse. if (!this.getOriginalRequest().getTopmostVia().hasPort()) transactionResponse.getTopmostVia().removePort(); } catch (ParseException ex) { ex.printStackTrace(); } // Method of the response does not match the request used to // create the transaction - transaction state does not change. if (!transactionResponse.getCSeq().getMethod().equals( getOriginalRequest().getMethod())) { sendResponse(transactionResponse); return; } // If the TU sends a provisional response while in the // trying state, if (getRealState() == TransactionState.TRYING) { if (statusCode / 100 == 1) { this.setState(TransactionState.PROCEEDING); } else if (200 <= statusCode && statusCode <= 699) { // INVITE ST has TRYING as a Pseudo state // (See issue 76). We are using the TRYING // pseudo state invite Transactions // to signal if the application // has sent trying or not and hence this // check is necessary. if (!isInviteTransaction()) { if (!isReliable()) { // Linger in the completed state to catch // retransmissions if the transport is not // reliable. this.setState(TransactionState.COMPLETED); // Note that Timer J is only set for Unreliable // transports -- see Issue 75. /* * From RFC 3261 Section 17.2.2 (non-invite server transaction) * * When the server transaction enters the "Completed" state, it MUST * set Timer J to fire in 64*T1 seconds for unreliable transports, and * zero seconds for reliable transports. While in the "Completed" * state, the server transaction MUST pass the final response to the * transport layer for retransmission whenever a retransmission of the * request is received. Any other final responses passed by the TU to * the server transaction MUST be discarded while in the "Completed" * state. The server transaction remains in this state until Timer J * fires, at which point it MUST transition to the "Terminated" state. */ enableTimeoutTimer(TIMER_J); } else { this.setState(TransactionState.TERMINATED); } } else { // This is the case for INVITE server transactions. // essentially, it duplicates the code in the // PROCEEDING case below. There is no TRYING state for INVITE // transactions in the RFC. We are using it to signal whether the // application has sent a provisional response or not. Hence // this is treated the same as as Proceeding. if (statusCode / 100 == 2) { // Status code is 2xx means that the // transaction transitions to TERMINATED // for both Reliable as well as unreliable // transports. Note that the dialog layer // takes care of retransmitting 2xx final // responses. /* * RFC 3261 Section 13.3.1.4 Note, however, that the INVITE server * transaction will be destroyed as soon as it receives this final * response and passes it to the transport. Therefore, it is necessary * to periodically pass the response directly to the transport until * the ACK arrives. The 2xx response is passed to the transport with * an interval that starts at T1 seconds and doubles for each * retransmission until it reaches T2 seconds (T1 and T2 are defined * in Section 17). Response retransmissions cease when an ACK request * for the response is received. This is independent of whatever * transport protocols are used to send the response. */ this.disableRetransmissionTimer(); this.disableTimeoutTimer(); this.collectionTime = TIMER_J; this.setState(TransactionState.TERMINATED); if (this.dialog != null) this.dialog.setRetransmissionTicks(); } else { // This an error final response. this.setState(TransactionState.COMPLETED); if (!isReliable()) { /* * RFC 3261 * * While in the "Proceeding" state, if the TU passes a response * with status code from 300 to 699 to the server transaction, the * response MUST be passed to the transport layer for * transmission, and the state machine MUST enter the "Completed" * state. For unreliable transports, timer G is set to fire in T1 * seconds, and is not set to fire for reliable transports. */ enableRetransmissionTimer(); } enableTimeoutTimer(TIMER_H); } } } // If the transaction is in the proceeding state, } else if (getRealState() == TransactionState.PROCEEDING) { if (isInviteTransaction()) { // If the response is a failure message, if (statusCode / 100 == 2) { // Set up to catch returning ACKs // The transaction lingers in the // terminated state for some time // to catch retransmitted INVITEs this.disableRetransmissionTimer(); this.disableTimeoutTimer(); this.collectionTime = TIMER_J; this.setState(TransactionState.TERMINATED); if (this.dialog != null) this.dialog.setRetransmissionTicks(); } else if (300 <= statusCode && statusCode <= 699) { // Set up to catch returning ACKs this.setState(TransactionState.COMPLETED); if (!isReliable()) { /* * While in the "Proceeding" state, if the TU passes a response with * status code from 300 to 699 to the server transaction, the response * MUST be passed to the transport layer for transmission, and the * state machine MUST enter the "Completed" state. For unreliable * transports, timer G is set to fire in T1 seconds, and is not set to * fire for reliable transports. */ enableRetransmissionTimer(); } enableTimeoutTimer(TIMER_H); } // If the transaction is not an invite transaction // and this is a final response, } else if (200 <= statusCode && statusCode <= 699) { // This is for Non-invite server transactions. // Set up to retransmit this response, // or terminate the transaction this.setState(TransactionState.COMPLETED); if (!isReliable()) { disableRetransmissionTimer(); enableTimeoutTimer(TIMER_J); } else { this.setState(TransactionState.TERMINATED); } } // If the transaction has already completed, } else if (TransactionState.COMPLETED == this.getRealState()) { return; } try { // Send the message to the client. // Record the last message sent out. if (sipStack.isLoggingEnabled()) { sipStack.getStackLogger().logDebug( "sendMessage : tx = " + this + " getState = " + this.getState()); } lastResponse = transactionResponse; this.sendResponse(transactionResponse); } catch (IOException e) { this.setState(TransactionState.TERMINATED); this.collectionTime = 0; throw e; } } finally { this.startTransactionTimer(); } } public String getViaHost() { return getMessageChannel().getViaHost(); } public int getViaPort() { return getMessageChannel().getViaPort(); } /** * Called by the transaction stack when a retransmission timer fires. This retransmits the * last response when the retransmission filter is enabled. */ protected void fireRetransmissionTimer() { try { if (sipStack.isLoggingEnabled()) { sipStack.getStackLogger().logDebug("fireRetransmissionTimer() -- "); } // Resend the last response sent by this transaction if (isInviteTransaction() && lastResponse != null) { // null can happen if this is terminating when the timer fires. if (!this.retransmissionAlertEnabled || sipStack.isTransactionPendingAck(this) ) { // Retransmit last response until ack. if (lastResponse.getStatusCode() / 100 > 2 && !this.isAckSeen) super.sendMessage(lastResponse); } else { // alert the application to retransmit the last response SipProviderImpl sipProvider = (SipProviderImpl) this.getSipProvider(); TimeoutEvent txTimeout = new TimeoutEvent(sipProvider, this, Timeout.RETRANSMIT); sipProvider.handleEvent(txTimeout, this); } } } catch (IOException e) { if (sipStack.isLoggingEnabled()) sipStack.getStackLogger().logException(e); raiseErrorEvent(SIPTransactionErrorEvent.TRANSPORT_ERROR); } } private void fireReliableResponseRetransmissionTimer() { try { super.sendMessage(this.pendingReliableResponse); } catch (IOException e) { if (sipStack.isLoggingEnabled()) sipStack.getStackLogger().logException(e); this.setState(TransactionState.TERMINATED); raiseErrorEvent(SIPTransactionErrorEvent.TRANSPORT_ERROR); } } /** * Called by the transaction stack when a timeout timer fires. */ protected void fireTimeoutTimer() { if (sipStack.isLoggingEnabled()) sipStack.getStackLogger().logDebug("SIPServerTransaction.fireTimeoutTimer this = " + this + " current state = " + this.getRealState() + " method = " + this.getOriginalRequest().getMethod()); if ( this.getMethod().equals(Request.INVITE) && sipStack.removeTransactionPendingAck(this) ) { if ( sipStack.isLoggingEnabled() ) { sipStack.getStackLogger().logDebug("Found tx pending ACK - returning"); } return; } SIPDialog dialog = (SIPDialog) this.dialog; if (((SIPTransactionStack) getSIPStack()).isDialogCreated(this.getOriginalRequest() .getMethod()) && (TransactionState.CALLING == this.getRealState() || TransactionState.TRYING == this .getRealState())) { dialog.setState(SIPDialog.TERMINATED_STATE); } else if (getOriginalRequest().getMethod().equals(Request.BYE)) { if (dialog != null && dialog.isTerminatedOnBye()) dialog.setState(SIPDialog.TERMINATED_STATE); } if (TransactionState.COMPLETED == this.getRealState() && isInviteTransaction()) { raiseErrorEvent(SIPTransactionErrorEvent.TIMEOUT_ERROR); this.setState(TransactionState.TERMINATED); sipStack.removeTransaction(this); } else if (TransactionState.COMPLETED == this.getRealState() && !isInviteTransaction()) { this.setState(TransactionState.TERMINATED); sipStack.removeTransaction(this); } else if (TransactionState.CONFIRMED == this.getRealState() && isInviteTransaction()) { // TIMER_I should not generate a timeout // exception to the application when the // Invite transaction is in Confirmed state. // Just transition to Terminated state. this.setState(TransactionState.TERMINATED); sipStack.removeTransaction(this); } else if (!isInviteTransaction() && (TransactionState.COMPLETED == this.getRealState() || TransactionState.CONFIRMED == this .getRealState())) { this.setState(TransactionState.TERMINATED); } else if (isInviteTransaction() && TransactionState.TERMINATED == this.getRealState()) { // This state could be reached when retransmitting raiseErrorEvent(SIPTransactionErrorEvent.TIMEOUT_ERROR); if (dialog != null) dialog.setState(SIPDialog.TERMINATED_STATE); } } /** * Get the last response. */ public SIPResponse getLastResponse() { return this.lastResponse; } /** * Set the original request. */ public void setOriginalRequest(SIPRequest originalRequest) { super.setOriginalRequest(originalRequest); } /* * (non-Javadoc) * * @see javax.sip.ServerTransaction#sendResponse(javax.sip.message.Response) */ public void sendResponse(Response response) throws SipException { SIPResponse sipResponse = (SIPResponse) response; SIPDialog dialog = this.dialog; if (response == null) throw new NullPointerException("null response"); try { sipResponse.checkHeaders(); } catch (ParseException ex) { throw new SipException(ex.getMessage()); } // check for meaningful response. if (!sipResponse.getCSeq().getMethod().equals(this.getMethod())) { throw new SipException( "CSeq method does not match Request method of request that created the tx."); } /* * 200-class responses to SUBSCRIBE requests also MUST contain an "Expires" header. The * period of time in the response MAY be shorter but MUST NOT be longer than specified in * the request. */ if (this.getMethod().equals(Request.SUBSCRIBE) && response.getStatusCode() / 100 == 2) { if (response.getHeader(ExpiresHeader.NAME) == null) { throw new SipException("Expires header is mandatory in 2xx response of SUBSCRIBE"); } else { Expires requestExpires = (Expires) this.getOriginalRequest().getExpires(); Expires responseExpires = (Expires) response.getExpires(); /* * If no "Expires" header is present in a SUBSCRIBE request, the implied default * is defined by the event package being used. */ if (requestExpires != null && responseExpires.getExpires() > requestExpires.getExpires()) { throw new SipException( "Response Expires time exceeds request Expires time : See RFC 3265 3.1.1"); } } } // Check for mandatory header. if (sipResponse.getStatusCode() == 200 && sipResponse.getCSeq().getMethod().equals(Request.INVITE) && sipResponse.getHeader(ContactHeader.NAME) == null) throw new SipException("Contact Header is mandatory for the OK to the INVITE"); if (!this.isMessagePartOfTransaction((SIPMessage) response)) { throw new SipException("Response does not belong to this transaction."); } // Fix up the response if the dialog has already been established. try { /* * The UAS MAY send a final response to the initial request before * having received PRACKs for all unacknowledged reliable provisional responses, * unless the final response is 2xx and any of the unacknowledged reliable provisional * responses contained a session description. In that case, it MUST NOT send a final * response until those provisional responses are acknowledged. */ if (this.pendingReliableResponse != null && this.getDialog() != null && this.getState() != TransactionState.TERMINATED && ((SIPResponse)response).getContentTypeHeader() != null && response.getStatusCode() / 100 == 2 && ((SIPResponse)response).getContentTypeHeader().getContentType() .equalsIgnoreCase("application") && ((SIPResponse)response).getContentTypeHeader().getContentSubType() .equalsIgnoreCase("sdp")) { try { boolean acquired = this.provisionalResponseSem.tryAcquire(1,TimeUnit.SECONDS); if (!acquired ) { throw new SipException("cannot send response -- unacked povisional"); } } catch (Exception ex) { this.sipStack.getStackLogger().logError("Could not acquire PRACK sem ", ex); } } else { // Sending the final response cancels the // pending response task. if (this.pendingReliableResponse != null && sipResponse.isFinalResponse()) { this.provisionalResponseTask.cancel(); this.provisionalResponseTask = null; } } // Dialog checks. These make sure that the response // being sent makes sense. if (dialog != null) { if (sipResponse.getStatusCode() / 100 == 2 && sipStack.isDialogCreated(sipResponse.getCSeq().getMethod())) { if (dialog.getLocalTag() == null && sipResponse.getTo().getTag() == null) { // Trying to send final response and user forgot to set // to // tag on the response -- be nice and assign the tag for // the user. sipResponse.getTo().setTag(Utils.getInstance().generateTag()); } else if (dialog.getLocalTag() != null && sipResponse.getToTag() == null) { sipResponse.setToTag(dialog.getLocalTag()); } else if (dialog.getLocalTag() != null && sipResponse.getToTag() != null && !dialog.getLocalTag().equals(sipResponse.getToTag())) { throw new SipException("Tag mismatch dialogTag is " + dialog.getLocalTag() + " responseTag is " + sipResponse.getToTag()); } } if (!sipResponse.getCallId().getCallId().equals(dialog.getCallId().getCallId())) { throw new SipException("Dialog mismatch!"); } } // Backward compatibility slippery slope.... // Only set the from tag in the response when the // incoming request has a from tag. String fromTag = ((SIPRequest) this.getRequest()).getFrom().getTag(); if (fromTag != null && sipResponse.getFromTag() != null && !sipResponse.getFromTag().equals(fromTag)) { throw new SipException("From tag of request does not match response from tag"); } else if (fromTag != null) { sipResponse.getFrom().setTag(fromTag); } else { if (sipStack.isLoggingEnabled()) sipStack.getStackLogger().logDebug("WARNING -- Null From tag in request!!"); } // See if the dialog needs to be inserted into the dialog table // or if the state of the dialog needs to be changed. if (dialog != null && response.getStatusCode() != 100) { dialog.setResponseTags(sipResponse); DialogState oldState = dialog.getState(); dialog.setLastResponse(this, (SIPResponse) response); if (oldState == null && dialog.getState() == DialogState.TERMINATED) { DialogTerminatedEvent event = new DialogTerminatedEvent(dialog .getSipProvider(), dialog); // Provide notification to the listener that the dialog has // ended. dialog.getSipProvider().handleEvent(event, this); } } else if (dialog == null && this.getMethod().equals(Request.INVITE) && this.retransmissionAlertEnabled && this.retransmissionAlertTimerTask == null && response.getStatusCode() / 100 == 2) { String dialogId = ((SIPResponse) response).getDialogId(true); this.retransmissionAlertTimerTask = new RetransmissionAlertTimerTask(dialogId); sipStack.retransmissionAlertTransactions.put(dialogId, this); sipStack.getTimer().schedule(this.retransmissionAlertTimerTask, 0, SIPTransactionStack.BASE_TIMER_INTERVAL); } // Send message after possibly inserting the Dialog // into the dialog table to avoid a possible race condition. this.sendMessage((SIPResponse) response); if ( dialog != null ) { dialog.startRetransmitTimer(this, (SIPResponse)response); } } catch (IOException ex) { if (sipStack.isLoggingEnabled()) sipStack.getStackLogger().logException(ex); this.setState(TransactionState.TERMINATED); raiseErrorEvent(SIPTransactionErrorEvent.TRANSPORT_ERROR); throw new SipException(ex.getMessage()); } catch (java.text.ParseException ex1) { if (sipStack.isLoggingEnabled()) sipStack.getStackLogger().logException(ex1); this.setState(TransactionState.TERMINATED); throw new SipException(ex1.getMessage()); } } /** * Return the book-keeping information that we actually use. */ private TransactionState getRealState() { return super.getState(); } /** * Return the current transaction state according to the RFC 3261 transaction state machine. * Invite transactions do not have a trying state. We just use this as a pseudo state for * processing requests. * * @return the state of the transaction. */ public TransactionState getState() { // Trying is a pseudo state for INVITE transactions. if (this.isInviteTransaction() && TransactionState.TRYING == super.getState()) return TransactionState.PROCEEDING; else return super.getState(); } /** * Sets a timeout after which the connection is closed (provided the server does not use the * connection for outgoing requests in this time period) and calls the superclass to set * state. */ public void setState(TransactionState newState) { // Set this timer for connection caching // of incoming connections. if (newState == TransactionState.TERMINATED && this.isReliable() && (!getSIPStack().cacheServerConnections)) { // Set a time after which the connection // is closed. this.collectionTime = TIMER_J; } super.setState(newState); } /** * Start the timer task. */ protected void startTransactionTimer() { if (this.transactionTimerStarted.compareAndSet(false, true)) { if (sipStack.getTimer() != null) { // The timer is set to null when the Stack is // shutting down. TimerTask myTimer = new TransactionTimer(); sipStack.getTimer().schedule(myTimer, BASE_TIMER_INTERVAL, BASE_TIMER_INTERVAL); } } } public boolean equals(Object other) { if (!other.getClass().equals(this.getClass())) { return false; } SIPServerTransaction sst = (SIPServerTransaction) other; return this.getBranch().equalsIgnoreCase(sst.getBranch()); } /* * (non-Javadoc) * * @see gov.nist.javax.sip.stack.SIPTransaction#getDialog() */ public Dialog getDialog() { return this.dialog; } /* * (non-Javadoc) * * @see gov.nist.javax.sip.stack.SIPTransaction#setDialog(gov.nist.javax.sip.stack.SIPDialog, * gov.nist.javax.sip.message.SIPMessage) */ public void setDialog(SIPDialog sipDialog, String dialogId) { if (sipStack.isLoggingEnabled()) sipStack.getStackLogger().logDebug("setDialog " + this + " dialog = " + sipDialog); this.dialog = sipDialog; if (dialogId != null) this.dialog.setAssigned(); if (this.retransmissionAlertEnabled && this.retransmissionAlertTimerTask != null) { this.retransmissionAlertTimerTask.cancel(); if (this.retransmissionAlertTimerTask.dialogId != null) { sipStack.retransmissionAlertTransactions .remove(this.retransmissionAlertTimerTask.dialogId); } this.retransmissionAlertTimerTask = null; } this.retransmissionAlertEnabled = false; } /* * (non-Javadoc) * * @see javax.sip.Transaction#terminate() */ public void terminate() throws ObjectInUseException { this.setState(TransactionState.TERMINATED); if (this.retransmissionAlertTimerTask != null) { this.retransmissionAlertTimerTask.cancel(); if (retransmissionAlertTimerTask.dialogId != null) { this.sipStack.retransmissionAlertTransactions .remove(retransmissionAlertTimerTask.dialogId); } this.retransmissionAlertTimerTask = null; } } protected void sendReliableProvisionalResponse(Response relResponse) throws SipException { /* * After the first reliable provisional response for a request has been acknowledged, the * UAS MAY send additional reliable provisional responses. The UAS MUST NOT send a second * reliable provisional response until the first is acknowledged. */ if (this.pendingReliableResponse != null) { throw new SipException("Unacknowledged response"); } else this.pendingReliableResponse = (SIPResponse) relResponse; /* * In addition, it MUST contain a Require header field containing the option tag 100rel, * and MUST include an RSeq header field. */ RSeq rseq = (RSeq) relResponse.getHeader(RSeqHeader.NAME); if (relResponse.getHeader(RSeqHeader.NAME) == null) { rseq = new RSeq(); relResponse.setHeader(rseq); } try { this.rseqNumber++; rseq.setSeqNumber(this.rseqNumber); // start the timer task which will retransmit the reliable response // until the PRACK is received this.lastResponse = (SIPResponse) relResponse; if ( this.getDialog() != null ) { boolean acquired = this.provisionalResponseSem.tryAcquire(1, TimeUnit.SECONDS); if (!acquired) { throw new SipException("Unacknowledged response"); } } this.sendMessage((SIPMessage) relResponse); this.provisionalResponseTask = new ProvisionalResponseTask(); this.sipStack.getTimer().schedule(provisionalResponseTask, 0, SIPTransactionStack.BASE_TIMER_INTERVAL); } catch (Exception ex) { InternalErrorHandler.handleException(ex); } } public SIPResponse getReliableProvisionalResponse() { return this.pendingReliableResponse; } /** * Cancel the retransmit timer for the provisional response task. * * @return true if the tx has seen the prack for the first time and false otherwise. * */ public boolean prackRecieved() { if (this.pendingReliableResponse == null) return false; if(provisionalResponseTask != null) this.provisionalResponseTask.cancel(); this.pendingReliableResponse = null; this.provisionalResponseSem.release(); return true; } /* * (non-Javadoc) * * @see javax.sip.ServerTransaction#enableRetransmissionAlerts() */ public void enableRetransmissionAlerts() throws SipException { if (this.getDialog() != null) throw new SipException("Dialog associated with tx"); else if (!this.getMethod().equals(Request.INVITE)) throw new SipException("Request Method must be INVITE"); this.retransmissionAlertEnabled = true; } public boolean isRetransmissionAlertEnabled() { return this.retransmissionAlertEnabled; } /** * Disable retransmission Alerts and cancel associated timers. * */ public void disableRetransmissionAlerts() { if (this.retransmissionAlertTimerTask != null && this.retransmissionAlertEnabled) { this.retransmissionAlertTimerTask.cancel(); this.retransmissionAlertEnabled = false; String dialogId = this.retransmissionAlertTimerTask.dialogId; if (dialogId != null) { sipStack.retransmissionAlertTransactions.remove(dialogId); } this.retransmissionAlertTimerTask = null; } } /** * This is book-keeping for retransmission filter management. */ public void setAckSeen() { this.isAckSeen = true; } /** * This is book-keeping for retransmission filter management. */ public boolean ackSeen() { return this.isAckSeen; } public void setMapped(boolean b) { this.isMapped = true; } public void setPendingSubscribe(SIPClientTransaction pendingSubscribeClientTx) { this.pendingSubscribeTransaction = pendingSubscribeClientTx; } public void releaseSem() { if (this.pendingSubscribeTransaction != null) { /* * When a notify is being processed we take a lock on the subscribe to avoid racing * with the OK of the subscribe. */ pendingSubscribeTransaction.releaseSem(); } else if (this.inviteTransaction != null && this.getMethod().equals(Request.CANCEL)) { /* * When a CANCEL is being processed we take a nested lock on the associated INVITE * server tx. */ this.inviteTransaction.releaseSem(); } super.releaseSem(); } /** * The INVITE Server Transaction corresponding to a CANCEL Server Transaction. * * @param st -- the invite server tx corresponding to the cancel server transaction. */ public void setInviteTransaction(SIPServerTransaction st) { this.inviteTransaction = st; } /** * TODO -- this method has to be added to the api. * * @return */ public SIPServerTransaction getCanceledInviteTransaction() { return this.inviteTransaction; } public void scheduleAckRemoval() throws IllegalStateException { if (this.getMethod() == null || !this.getMethod().equals(Request.ACK)) { throw new IllegalStateException("Method is null[" + (getMethod() == null) + "] or method is not ACK[" + this.getMethod() + "]"); } this.startTransactionTimer(); } }