/* * 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.core.NameValueList; import gov.nist.javax.sip.SIPConstants; import gov.nist.javax.sip.Utils; import gov.nist.javax.sip.address.AddressImpl; import gov.nist.javax.sip.header.Contact; import gov.nist.javax.sip.header.RecordRoute; import gov.nist.javax.sip.header.RecordRouteList; import gov.nist.javax.sip.header.Route; import gov.nist.javax.sip.header.RouteList; import gov.nist.javax.sip.header.TimeStamp; import gov.nist.javax.sip.header.To; 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.security.cert.X509Certificate; import java.text.ParseException; import java.util.ListIterator; import java.util.TimerTask; import java.util.concurrent.ConcurrentHashMap; import javax.net.ssl.SSLPeerUnverifiedException; import javax.sip.Dialog; import javax.sip.DialogState; import javax.sip.InvalidArgumentException; 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.address.SipURI; import javax.sip.header.ExpiresHeader; import javax.sip.header.RouteHeader; import javax.sip.header.TimeStampHeader; import javax.sip.message.Request; /* * Jeff Keyser -- initial. Daniel J. Martinez Manzano --Added support for TLS message channel. * Emil Ivov -- bug fixes. Chris Beardshear -- bug fix. Andreas Bystrom -- bug fixes. Matt Keller * (Motorolla) -- bug fix. */ /** * Represents a client transaction. Implements the following state machines. (From RFC 3261) * * <pre> * * * * * * * |INVITE from TU * Timer A fires |INVITE sent * Reset A, V Timer B fires * INVITE sent +-----------+ or Transport Err. * +---------| |---------------+inform TU * | | Calling | | * +-------->| |-------------->| * +-----------+ 2xx | * | | 2xx to TU | * | |1xx | * 300-699 +---------------+ |1xx to TU | * ACK sent | | | * resp. to TU | 1xx V | * | 1xx to TU -----------+ | * | +---------| | | * | | |Proceeding |-------------->| * | +-------->| | 2xx | * | +-----------+ 2xx to TU | * | 300-699 | | * | ACK sent, | | * | resp. to TU| | * | | | NOTE: * | 300-699 V | * | ACK sent +-----------+Transport Err. | transitions * | +---------| |Inform TU | labeled with * | | | Completed |-------------->| the event * | +-------->| | | over the action * | +-----------+ | to take * | ˆ | | * | | | Timer D fires | * +--------------+ | - | * | | * V | * +-----------+ | * | | | * | Terminated|<--------------+ * | | * +-----------+ * * Figure 5: INVITE client transaction * * * |Request from TU * |send request * Timer E V * send request +-----------+ * +---------| |-------------------+ * | | Trying | Timer F | * +-------->| | or Transport Err.| * +-----------+ inform TU | * 200-699 | | | * resp. to TU | |1xx | * +---------------+ |resp. to TU | * | | | * | Timer E V Timer F | * | send req +-----------+ or Transport Err. | * | +---------| | inform TU | * | | |Proceeding |------------------>| * | +-------->| |-----+ | * | +-----------+ |1xx | * | | ˆ |resp to TU | * | 200-699 | +--------+ | * | resp. to TU | | * | | | * | V | * | +-----------+ | * | | | | * | | Completed | | * | | | | * | +-----------+ | * | ˆ | | * | | | Timer K | * +--------------+ | - | * | | * V | * NOTE: +-----------+ | * | | | * transitions | Terminated|<------------------+ * labeled with | | * the event +-----------+ * over the action * to take * * Figure 6: non-INVITE client transaction * * * * * * * </pre> * * * @author M. Ranganathan * * @version 1.2 $Revision: 1.122 $ $Date: 2009/12/17 23:33:52 $ */ public class SIPClientTransaction extends SIPTransaction implements ServerResponseInterface, javax.sip.ClientTransaction, gov.nist.javax.sip.ClientTransactionExt { // a SIP Client transaction may belong simultaneously to multiple // dialogs in the early state. These dialogs all have // the same call ID and same From tag but different to tags. private ConcurrentHashMap<String,SIPDialog> sipDialogs; private SIPRequest lastRequest; private int viaPort; private String viaHost; // Real ResponseInterface to pass messages to private transient ServerResponseInterface respondTo; private SIPDialog defaultDialog; private Hop nextHop; private boolean notifyOnRetransmit; private boolean timeoutIfStillInCallingState; private int callingStateTimeoutCount; public class TransactionTimer extends SIPStackTimerTask { public TransactionTimer() { } protected void runTask() { SIPClientTransaction clientTransaction; SIPTransactionStack sipStack; clientTransaction = SIPClientTransaction.this; sipStack = clientTransaction.sipStack; // If the transaction has terminated, if (clientTransaction.isTerminated()) { if (sipStack.isLoggingEnabled()) { sipStack.getStackLogger().logDebug( "removing = " + clientTransaction + " isReliable " + clientTransaction.isReliable()); } sipStack.removeTransaction(clientTransaction); try { this.cancel(); } catch (IllegalStateException ex) { if (!sipStack.isAlive()) return; } // Client transaction terminated. Kill connection if // this is a TCP after the linger timer has expired. // The linger timer is needed to allow any pending requests to // return responses. if ((!sipStack.cacheClientConnections) && clientTransaction.isReliable()) { int newUseCount = --clientTransaction.getMessageChannel().useCount; if (newUseCount <= 0) { // Let the connection linger for a while and then close // it. TimerTask myTimer = new LingerTimer(); sipStack.getTimer().schedule(myTimer, SIPTransactionStack.CONNECTION_LINGER_TIME * 1000); } } else { // Cache the client connections so dont close the // connection. This keeps the connection open permanently // until the client disconnects. if (sipStack.isLoggingEnabled() && clientTransaction.isReliable()) { int useCount = clientTransaction.getMessageChannel().useCount; if (sipStack.isLoggingEnabled()) sipStack.getStackLogger().logDebug("Client Use Count = " + useCount); } } } else { // If this transaction has not // terminated, // Fire the transaction timer. clientTransaction.fireTimer(); } } } /** * Creates a new client transaction. * * @param newSIPStack Transaction stack this transaction belongs to. * @param newChannelToUse Channel to encapsulate. * @return the created client transaction. */ protected SIPClientTransaction(SIPTransactionStack newSIPStack, MessageChannel newChannelToUse) { super(newSIPStack, newChannelToUse); // Create a random branch parameter for this transaction // setBranch( SIPConstants.BRANCH_MAGIC_COOKIE + // Integer.toHexString( hashCode( ) ) ); setBranch(Utils.getInstance().generateBranchId()); this.messageProcessor = newChannelToUse.messageProcessor; this.setEncapsulatedChannel(newChannelToUse); this.notifyOnRetransmit = false; this.timeoutIfStillInCallingState = false; // This semaphore guards the listener from being // re-entered for this transaction. That is // for a give tx, the listener is called at most // once with an outstanding request. if (sipStack.isLoggingEnabled()) { sipStack.getStackLogger().logDebug("Creating clientTransaction " + this); sipStack.getStackLogger().logStackTrace(); } // this.startTransactionTimer(); this.sipDialogs = new ConcurrentHashMap(); } /** * Sets the real ResponseInterface this transaction encapsulates. * * @param newRespondTo ResponseInterface to send messages to. */ public void setResponseInterface(ServerResponseInterface newRespondTo) { if (sipStack.isLoggingEnabled()) { sipStack.getStackLogger().logDebug( "Setting response interface for " + this + " to " + newRespondTo); if (newRespondTo == null) { sipStack.getStackLogger().logStackTrace(); sipStack.getStackLogger().logDebug("WARNING -- setting to null!"); } } respondTo = newRespondTo; } /** * Returns this transaction. */ public MessageChannel getRequestChannel() { return this; } /** * Deterines 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 = messageToTest.getViaHeaders(); // Flags whether the select message is part of this transaction boolean transactionMatches; String messageBranch = ((Via) viaHeaders.getFirst()).getBranch(); boolean rfc3261Compliant = getBranch() != null && messageBranch != null && getBranch().toLowerCase().startsWith( SIPConstants.BRANCH_MAGIC_COOKIE_LOWER_CASE) && messageBranch.toLowerCase().startsWith( SIPConstants.BRANCH_MAGIC_COOKIE_LOWER_CASE); transactionMatches = false; if (TransactionState.COMPLETED == this.getState()) { if (rfc3261Compliant) { transactionMatches = getBranch().equalsIgnoreCase( ((Via) viaHeaders.getFirst()).getBranch()) && getMethod().equals(messageToTest.getCSeq().getMethod()); } else { transactionMatches = getBranch().equals(messageToTest.getTransactionId()); } } else if (!isTerminated()) { if (rfc3261Compliant) { if (viaHeaders != null) { // If the branch parameter is the // same as this transaction and the method is the same, if (getBranch().equalsIgnoreCase(((Via) viaHeaders.getFirst()).getBranch())) { transactionMatches = getOriginalRequest().getCSeq().getMethod().equals( messageToTest.getCSeq().getMethod()); } } } else { // not RFC 3261 compliant. if (getBranch() != null) { transactionMatches = getBranch().equalsIgnoreCase( messageToTest.getTransactionId()); } else { transactionMatches = getOriginalRequest().getTransactionId() .equalsIgnoreCase(messageToTest.getTransactionId()); } } } return transactionMatches; } /** * Send a request message through this transaction and onto the client. * * @param messageToSend Request to process and send. */ public void sendMessage(SIPMessage messageToSend) throws IOException { try { // Message typecast as a request SIPRequest transactionRequest; transactionRequest = (SIPRequest) messageToSend; // Set the branch id for the top via header. Via topVia = (Via) transactionRequest.getViaHeaders().getFirst(); // Tack on a branch identifier to match responses. try { topVia.setBranch(getBranch()); } catch (java.text.ParseException ex) { } if (sipStack.isLoggingEnabled()) { sipStack.getStackLogger().logDebug("Sending Message " + messageToSend); sipStack.getStackLogger().logDebug("TransactionState " + this.getState()); } // If this is the first request for this transaction, if (TransactionState.PROCEEDING == getState() || TransactionState.CALLING == getState()) { // If this is a TU-generated ACK request, if (transactionRequest.getMethod().equals(Request.ACK)) { // Send directly to the underlying // transport and close this transaction if (isReliable()) { this.setState(TransactionState.TERMINATED); } else { this.setState(TransactionState.COMPLETED); } // BUGBUG -- This suppresses sending the ACK uncomment this // to // test 4xx retransmission // if (transactionRequest.getMethod() != Request.ACK) super.sendMessage(transactionRequest); return; } } try { // Send the message to the server lastRequest = transactionRequest; if (getState() == null) { // Save this request as the one this transaction // is handling setOriginalRequest(transactionRequest); // Change to trying/calling state // Set state first to avoid race condition.. if (transactionRequest.getMethod().equals(Request.INVITE)) { this.setState(TransactionState.CALLING); } else if (transactionRequest.getMethod().equals(Request.ACK)) { // Acks are never retransmitted. this.setState(TransactionState.TERMINATED); } else { this.setState(TransactionState.TRYING); } if (!isReliable()) { enableRetransmissionTimer(); } if (isInviteTransaction()) { enableTimeoutTimer(TIMER_B); } else { enableTimeoutTimer(TIMER_F); } } // BUGBUG This supresses sending ACKS -- uncomment to test // 4xx retransmission. // if (transactionRequest.getMethod() != Request.ACK) super.sendMessage(transactionRequest); } catch (IOException e) { this.setState(TransactionState.TERMINATED); throw e; } } finally { this.isMapped = true; this.startTransactionTimer(); } } /** * Process a new response message through this transaction. If necessary, this message will * also be passed onto the TU. * * @param transactionResponse Response to process. * @param sourceChannel Channel that received this message. */ public synchronized void processResponse(SIPResponse transactionResponse, MessageChannel sourceChannel, SIPDialog dialog) { // If the state has not yet been assigned then this is a // spurious response. if (getState() == null) return; // Ignore 1xx if ((TransactionState.COMPLETED == this.getState() || TransactionState.TERMINATED == this .getState()) && transactionResponse.getStatusCode() / 100 == 1) { return; } if (sipStack.isLoggingEnabled()) { sipStack.getStackLogger().logDebug( "processing " + transactionResponse.getFirstLine() + "current state = " + getState()); sipStack.getStackLogger().logDebug("dialog = " + dialog); } this.lastResponse = transactionResponse; /* * JvB: this is now duplicate with code in the other processResponse * * if (dialog != null && transactionResponse.getStatusCode() != 100 && * (transactionResponse.getTo().getTag() != null || sipStack .isRfc2543Supported())) { // * add the route before you process the response. dialog.setLastResponse(this, * transactionResponse); this.setDialog(dialog, transactionResponse.getDialogId(false)); } */ try { if (isInviteTransaction()) inviteClientTransaction(transactionResponse, sourceChannel, dialog); else nonInviteClientTransaction(transactionResponse, sourceChannel, dialog); } catch (IOException ex) { if (sipStack.isLoggingEnabled()) sipStack.getStackLogger().logException(ex); this.setState(TransactionState.TERMINATED); raiseErrorEvent(SIPTransactionErrorEvent.TRANSPORT_ERROR); } } /** * Implements the state machine for invite client transactions. * * <pre> * * * * * * |Request from TU * |send request * Timer E V * send request +-----------+ * +---------| |-------------------+ * | | Trying | Timer F | * +-------->| | or Transport Err.| * +-----------+ inform TU | * 200-699 | | | * resp. to TU | |1xx | * +---------------+ |resp. to TU | * | | | * | Timer E V Timer F | * | send req +-----------+ or Transport Err. | * | +---------| | inform TU | * | | |Proceeding |------------------>| * | +-------->| |-----+ | * | +-----------+ |1xx | * | | ˆ |resp to TU | * | 200-699 | +--------+ | * | resp. to TU | | * | | | * | V | * | +-----------+ | * | | | | * | | Completed | | * | | | | * | +-----------+ | * | ˆ | | * | | | Timer K | * +--------------+ | - | * | | * V | * NOTE: +-----------+ | * | | | * transitions | Terminated|<------------------+ * labeled with | | * the event +-----------+ * over the action * to take * * Figure 6: non-INVITE client transaction * * * * * </pre> * * @param transactionResponse -- transaction response received. * @param sourceChannel - source channel on which the response was received. */ private void nonInviteClientTransaction(SIPResponse transactionResponse, MessageChannel sourceChannel, SIPDialog sipDialog) throws IOException { int statusCode = transactionResponse.getStatusCode(); if (TransactionState.TRYING == this.getState()) { if (statusCode / 100 == 1) { this.setState(TransactionState.PROCEEDING); enableRetransmissionTimer(MAXIMUM_RETRANSMISSION_TICK_COUNT); enableTimeoutTimer(TIMER_F); // According to RFC, the TU has to be informed on // this transition. if (respondTo != null) { respondTo.processResponse(transactionResponse, this, sipDialog); } else { this.semRelease(); } } else if (200 <= statusCode && statusCode <= 699) { // Send the response up to the TU. if (respondTo != null) { respondTo.processResponse(transactionResponse, this, sipDialog); } else { this.semRelease(); } if (!isReliable()) { this.setState(TransactionState.COMPLETED); enableTimeoutTimer(TIMER_K); } else { this.setState(TransactionState.TERMINATED); } } } else if (TransactionState.PROCEEDING == this.getState()) { if (statusCode / 100 == 1) { if (respondTo != null) { respondTo.processResponse(transactionResponse, this, sipDialog); } else { this.semRelease(); } } else if (200 <= statusCode && statusCode <= 699) { if (respondTo != null) { respondTo.processResponse(transactionResponse, this, sipDialog); } else { this.semRelease(); } disableRetransmissionTimer(); disableTimeoutTimer(); if (!isReliable()) { this.setState(TransactionState.COMPLETED); enableTimeoutTimer(TIMER_K); } else { this.setState(TransactionState.TERMINATED); } } } else { if (sipStack.isLoggingEnabled()) { sipStack.getStackLogger().logDebug( " Not sending response to TU! " + getState()); } this.semRelease(); } } /** * Implements the state machine for invite client transactions. * * <pre> * * * * * * |INVITE from TU * Timer A fires |INVITE sent * Reset A, V Timer B fires * INVITE sent +-----------+ or Transport Err. * +---------| |---------------+inform TU * | | Calling | | * +-------->| |-------------->| * +-----------+ 2xx | * | | 2xx to TU | * | |1xx | * 300-699 +---------------+ |1xx to TU | * ACK sent | | | * resp. to TU | 1xx V | * | 1xx to TU -----------+ | * | +---------| | | * | | |Proceeding |-------------->| * | +-------->| | 2xx | * | +-----------+ 2xx to TU | * | 300-699 | | * | ACK sent, | | * | resp. to TU| | * | | | NOTE: * | 300-699 V | * | ACK sent +-----------+Transport Err. | transitions * | +---------| |Inform TU | labeled with * | | | Completed |-------------->| the event * | +-------->| | | over the action * | +-----------+ | to take * | ˆ | | * | | | Timer D fires | * +--------------+ | - | * | | * V | * +-----------+ | * | | | * | Terminated|<--------------+ * | | * +-----------+ * * * * * </pre> * * @param transactionResponse -- transaction response received. * @param sourceChannel - source channel on which the response was received. */ private void inviteClientTransaction(SIPResponse transactionResponse, MessageChannel sourceChannel, SIPDialog dialog) throws IOException { int statusCode = transactionResponse.getStatusCode(); if (TransactionState.TERMINATED == this.getState()) { boolean ackAlreadySent = false; if (dialog != null && dialog.isAckSeen() && dialog.getLastAckSent() != null) { if (dialog.getLastAckSent().getCSeq().getSeqNumber() == transactionResponse.getCSeq() .getSeqNumber() && transactionResponse.getFromTag().equals( dialog.getLastAckSent().getFromTag())) { // the last ack sent corresponded to this response ackAlreadySent = true; } } // retransmit the ACK for this response. if (dialog!= null && ackAlreadySent && transactionResponse.getCSeq().getMethod().equals(dialog.getMethod())) { try { // Found the dialog - resend the ACK and // dont pass up the null transaction if (sipStack.isLoggingEnabled()) sipStack.getStackLogger().logDebug("resending ACK"); dialog.resendAck(); } catch (SipException ex) { // What to do here ?? kill the dialog? } } this.semRelease(); return; } else if (TransactionState.CALLING == this.getState()) { if (statusCode / 100 == 2) { // JvB: do this ~before~ calling the application, to avoid // retransmissions // of the INVITE after app sends ACK disableRetransmissionTimer(); disableTimeoutTimer(); this.setState(TransactionState.TERMINATED); // 200 responses are always seen by TU. if (respondTo != null) respondTo.processResponse(transactionResponse, this, dialog); else { this.semRelease(); } } else if (statusCode / 100 == 1) { disableRetransmissionTimer(); disableTimeoutTimer(); this.setState(TransactionState.PROCEEDING); if (respondTo != null) respondTo.processResponse(transactionResponse, this, dialog); else { this.semRelease(); } } else if (300 <= statusCode && statusCode <= 699) { // Send back an ACK request try { sendMessage((SIPRequest) createErrorAck()); } catch (Exception ex) { sipStack.getStackLogger().logError( "Unexpected Exception sending ACK -- sending error AcK ", ex); } /* * When in either the "Calling" or "Proceeding" states, reception of response with * status code from 300-699 MUST cause the client transaction to transition to * "Completed". The client transaction MUST pass the received response up to the * TU, and the client transaction MUST generate an ACK request. */ if (respondTo != null) { respondTo.processResponse(transactionResponse, this, dialog); } else { this.semRelease(); } if (this.getDialog() != null && ((SIPDialog)this.getDialog()).isBackToBackUserAgent()) { ((SIPDialog) this.getDialog()).releaseAckSem(); } if (!isReliable()) { this.setState(TransactionState.COMPLETED); enableTimeoutTimer(TIMER_D); } else { // Proceed immediately to the TERMINATED state. this.setState(TransactionState.TERMINATED); } } } else if (TransactionState.PROCEEDING == this.getState()) { if (statusCode / 100 == 1) { if (respondTo != null) { respondTo.processResponse(transactionResponse, this, dialog); } else { this.semRelease(); } } else if (statusCode / 100 == 2) { this.setState(TransactionState.TERMINATED); if (respondTo != null) { respondTo.processResponse(transactionResponse, this, dialog); } else { this.semRelease(); } } else if (300 <= statusCode && statusCode <= 699) { // Send back an ACK request try { sendMessage((SIPRequest) createErrorAck()); } catch (Exception ex) { InternalErrorHandler.handleException(ex); } if (this.getDialog() != null) { ((SIPDialog) this.getDialog()).releaseAckSem(); } // JvB: update state before passing to app if (!isReliable()) { this.setState(TransactionState.COMPLETED); this.enableTimeoutTimer(TIMER_D); } else { this.setState(TransactionState.TERMINATED); } // Pass up to the TU for processing. if (respondTo != null) respondTo.processResponse(transactionResponse, this, dialog); else { this.semRelease(); } // JvB: duplicate with line 874 // if (!isReliable()) { // enableTimeoutTimer(TIMER_D); // } } } else if (TransactionState.COMPLETED == this.getState()) { if (300 <= statusCode && statusCode <= 699) { // Send back an ACK request try { sendMessage((SIPRequest) createErrorAck()); } catch (Exception ex) { InternalErrorHandler.handleException(ex); } finally { this.semRelease(); } } } } /* * (non-Javadoc) * * @see javax.sip.ClientTransaction#sendRequest() */ public void sendRequest() throws SipException { SIPRequest sipRequest = this.getOriginalRequest(); if (this.getState() != null) throw new SipException("Request already sent"); if (sipStack.isLoggingEnabled()) { sipStack.getStackLogger().logDebug("sendRequest() " + sipRequest); } try { sipRequest.checkHeaders(); } catch (ParseException ex) { if (sipStack.isLoggingEnabled()) sipStack.getStackLogger().logError("missing required header"); throw new SipException(ex.getMessage()); } if (getMethod().equals(Request.SUBSCRIBE) && sipRequest.getHeader(ExpiresHeader.NAME) == null) { /* * If no "Expires" header is present in a SUBSCRIBE request, the implied default is * defined by the event package being used. * */ if (sipStack.isLoggingEnabled()) sipStack.getStackLogger().logWarning( "Expires header missing in outgoing subscribe --" + " Notifier will assume implied value on event package"); } try { /* * This check is removed because it causes problems for load balancers ( See issue * 136) reported by Raghav Ramesh ( BT ) * */ if (this.getOriginalRequest().getMethod().equals(Request.CANCEL) && sipStack.isCancelClientTransactionChecked()) { SIPClientTransaction ct = (SIPClientTransaction) sipStack.findCancelTransaction( this.getOriginalRequest(), false); if (ct == null) { /* * If the original request has generated a final response, the CANCEL SHOULD * NOT be sent, as it is an effective no-op, since CANCEL has no effect on * requests that have already generated a final response. */ throw new SipException("Could not find original tx to cancel. RFC 3261 9.1"); } else if (ct.getState() == null) { throw new SipException( "State is null no provisional response yet -- cannot cancel RFC 3261 9.1"); } else if (!ct.getMethod().equals(Request.INVITE)) { throw new SipException("Cannot cancel non-invite requests RFC 3261 9.1"); } } else if (this.getOriginalRequest().getMethod().equals(Request.BYE) || this.getOriginalRequest().getMethod().equals(Request.NOTIFY)) { SIPDialog dialog = sipStack.getDialog(this.getOriginalRequest() .getDialogId(false)); // I want to behave like a user agent so send the BYE using the // Dialog if (this.getSipProvider().isAutomaticDialogSupportEnabled() && dialog != null) { throw new SipException( "Dialog is present and AutomaticDialogSupport is enabled for " + " the provider -- Send the Request using the Dialog.sendRequest(transaction)"); } } // Only map this after the fist request is sent out. if (this.getMethod().equals(Request.INVITE)) { SIPDialog dialog = this.getDefaultDialog(); if (dialog != null && dialog.isBackToBackUserAgent()) { // Block sending re-INVITE till we see the ACK. if ( ! dialog.takeAckSem() ) { throw new SipException ("Failed to take ACK semaphore"); } } } this.isMapped = true; this.sendMessage(sipRequest); } catch (IOException ex) { this.setState(TransactionState.TERMINATED); throw new SipException("IO Error sending request", ex); } } /** * Called by the transaction stack when a retransmission timer fires. */ protected void fireRetransmissionTimer() { try { // Resend the last request sent if (this.getState() == null || !this.isMapped) return; boolean inv = isInviteTransaction(); TransactionState s = this.getState(); // JvB: INVITE CTs only retransmit in CALLING, non-INVITE in both TRYING and // PROCEEDING // Bug-fix for non-INVITE transactions not retransmitted when 1xx response received if ((inv && TransactionState.CALLING == s) || (!inv && (TransactionState.TRYING == s || TransactionState.PROCEEDING == s))) { // If the retransmission filter is disabled then // retransmission of the INVITE is the application // responsibility. if (lastRequest != null) { if (sipStack.generateTimeStampHeader && lastRequest.getHeader(TimeStampHeader.NAME) != null) { long milisec = System.currentTimeMillis(); TimeStamp timeStamp = new TimeStamp(); try { timeStamp.setTimeStamp(milisec); } catch (InvalidArgumentException ex) { InternalErrorHandler.handleException(ex); } lastRequest.setHeader(timeStamp); } super.sendMessage(lastRequest); if (this.notifyOnRetransmit) { TimeoutEvent txTimeout = new TimeoutEvent(this.getSipProvider(), this, Timeout.RETRANSMIT); this.getSipProvider().handleEvent(txTimeout, this); } if (this.timeoutIfStillInCallingState && this.getState() == TransactionState.CALLING) { this.callingStateTimeoutCount--; if (callingStateTimeoutCount == 0) { TimeoutEvent timeoutEvent = new TimeoutEvent(this.getSipProvider(), this, Timeout.RETRANSMIT); this.getSipProvider().handleEvent(timeoutEvent, this); this.timeoutIfStillInCallingState = false; } } } } } catch (IOException e) { this.raiseIOExceptionEvent(); raiseErrorEvent(SIPTransactionErrorEvent.TRANSPORT_ERROR); } } /** * Called by the transaction stack when a timeout timer fires. */ protected void fireTimeoutTimer() { if (sipStack.isLoggingEnabled()) sipStack.getStackLogger().logDebug("fireTimeoutTimer " + this); SIPDialog dialog = (SIPDialog) this.getDialog(); if (TransactionState.CALLING == this.getState() || TransactionState.TRYING == this.getState() || TransactionState.PROCEEDING == this.getState()) { // Timeout occured. If this is asociated with a transaction // creation then kill the dialog. if (dialog != null && (dialog.getState() == null || dialog.getState() == DialogState.EARLY)) { if (((SIPTransactionStack) getSIPStack()).isDialogCreated(this .getOriginalRequest().getMethod())) { // If this is a re-invite we do not delete the dialog even // if the // reinvite times out. Else // terminate the enclosing dialog. dialog.delete(); } } else if (dialog != null) { // Guard against the case of BYE time out. if (getOriginalRequest().getMethod().equalsIgnoreCase(Request.BYE) && dialog.isTerminatedOnBye()) { // Terminate the associated dialog on BYE Timeout. dialog.delete(); } } } if (TransactionState.COMPLETED != this.getState()) { raiseErrorEvent(SIPTransactionErrorEvent.TIMEOUT_ERROR); // Got a timeout error on a cancel. if (this.getOriginalRequest().getMethod().equalsIgnoreCase(Request.CANCEL)) { SIPClientTransaction inviteTx = (SIPClientTransaction) this.getOriginalRequest() .getInviteTransaction(); if (inviteTx != null && ((inviteTx.getState() == TransactionState.CALLING || inviteTx .getState() == TransactionState.PROCEEDING)) && inviteTx.getDialog() != null) { /* * A proxy server should have started TIMER C and take care of the Termination * using transaction.terminate() by itself (i.e. this is not the job of the * stack at this point but we do it to be nice. */ inviteTx.setState(TransactionState.TERMINATED); } } } else { this.setState(TransactionState.TERMINATED); } } /* * (non-Javadoc) * * @see javax.sip.ClientTransaction#createCancel() */ public Request createCancel() throws SipException { SIPRequest originalRequest = this.getOriginalRequest(); if (originalRequest == null) throw new SipException("Bad state " + getState()); if (!originalRequest.getMethod().equals(Request.INVITE)) throw new SipException("Only INIVTE may be cancelled"); if (originalRequest.getMethod().equalsIgnoreCase(Request.ACK)) throw new SipException("Cannot Cancel ACK!"); else { SIPRequest cancelRequest = originalRequest.createCancelRequest(); cancelRequest.setInviteTransaction(this); return cancelRequest; } } /* * (non-Javadoc) * * @see javax.sip.ClientTransaction#createAck() */ public Request createAck() throws SipException { SIPRequest originalRequest = this.getOriginalRequest(); if (originalRequest == null) throw new SipException("bad state " + getState()); if (getMethod().equalsIgnoreCase(Request.ACK)) { throw new SipException("Cannot ACK an ACK!"); } else if (lastResponse == null) { throw new SipException("bad Transaction state"); } else if (lastResponse.getStatusCode() < 200) { if (sipStack.isLoggingEnabled()) { sipStack.getStackLogger().logDebug("lastResponse = " + lastResponse); } throw new SipException("Cannot ACK a provisional response!"); } SIPRequest ackRequest = originalRequest.createAckRequest((To) lastResponse.getTo()); // Pull the record route headers from the last reesponse. RecordRouteList recordRouteList = lastResponse.getRecordRouteHeaders(); if (recordRouteList == null) { // If the record route list is null then we can // construct the ACK from the specified contact header. // Note the 3xx check here because 3xx is a redirect. // The contact header for the 3xx is the redirected // location so we cannot use that to construct the // request URI. if (lastResponse.getContactHeaders() != null && lastResponse.getStatusCode() / 100 != 3) { Contact contact = (Contact) lastResponse.getContactHeaders().getFirst(); javax.sip.address.URI uri = (javax.sip.address.URI) contact.getAddress().getURI() .clone(); ackRequest.setRequestURI(uri); } return ackRequest; } ackRequest.removeHeader(RouteHeader.NAME); RouteList routeList = new RouteList(); // start at the end of the list and walk backwards ListIterator<RecordRoute> li = recordRouteList.listIterator(recordRouteList.size()); while (li.hasPrevious()) { RecordRoute rr = (RecordRoute) li.previous(); Route route = new Route(); route.setAddress((AddressImpl) ((AddressImpl) rr.getAddress()).clone()); route.setParameters((NameValueList) rr.getParameters().clone()); routeList.add(route); } Contact contact = null; if (lastResponse.getContactHeaders() != null) { contact = (Contact) lastResponse.getContactHeaders().getFirst(); } if (!((SipURI) ((Route) routeList.getFirst()).getAddress().getURI()).hasLrParam()) { // Contact may not yet be there (bug reported by Andreas B). Route route = null; if (contact != null) { route = new Route(); route.setAddress((AddressImpl) ((AddressImpl) (contact.getAddress())).clone()); } Route firstRoute = (Route) routeList.getFirst(); routeList.removeFirst(); javax.sip.address.URI uri = firstRoute.getAddress().getURI(); ackRequest.setRequestURI(uri); if (route != null) routeList.add(route); ackRequest.addHeader(routeList); } else { if (contact != null) { javax.sip.address.URI uri = (javax.sip.address.URI) contact.getAddress().getURI() .clone(); ackRequest.setRequestURI(uri); ackRequest.addHeader(routeList); } } return ackRequest; } /* * Creates an ACK for an error response, according to RFC3261 section 17.1.1.3 * * Note that this is different from an ACK for 2xx */ private final Request createErrorAck() throws SipException, ParseException { SIPRequest originalRequest = this.getOriginalRequest(); if (originalRequest == null) throw new SipException("bad state " + getState()); if (!getMethod().equals(Request.INVITE)) { throw new SipException("Can only ACK an INVITE!"); } else if (lastResponse == null) { throw new SipException("bad Transaction state"); } else if (lastResponse.getStatusCode() < 200) { if (sipStack.isLoggingEnabled()) { sipStack.getStackLogger().logDebug("lastResponse = " + lastResponse); } throw new SipException("Cannot ACK a provisional response!"); } return originalRequest.createErrorAck((To) lastResponse.getTo()); } /** * Set the port of the recipient. */ protected void setViaPort(int port) { this.viaPort = port; } /** * Set the port of the recipient. */ protected void setViaHost(String host) { this.viaHost = host; } /** * Get the port of the recipient. */ public int getViaPort() { return this.viaPort; } /** * Get the host of the recipient. */ public String getViaHost() { return this.viaHost; } /** * get the via header for an outgoing request. */ public Via getOutgoingViaHeader() { return this.getMessageProcessor().getViaHeader(); } /** * This is called by the stack after a non-invite client transaction goes to completed state. */ public void clearState() { // reduce the state to minimum // This assumes that the application will not need // to access the request once the transaction is // completed. // TODO -- revisit this - results in a null pointer // occuring occasionally. // this.lastRequest = null; // this.originalRequest = null; // this.lastResponse = null; } /** * 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().cacheClientConnections)) { // Set a time after which the connection // is closed. this.collectionTime = TIMER_J; } if (super.getState() != TransactionState.COMPLETED && (newState == TransactionState.COMPLETED || newState == TransactionState.TERMINATED)) { sipStack.decrementActiveClientTransactionCount(); } super.setState(newState); } /** * Start the timer task. */ protected void startTransactionTimer() { if (this.transactionTimerStarted.compareAndSet(false, true)) { TimerTask myTimer = new TransactionTimer(); if ( sipStack.getTimer() != null ) { sipStack.getTimer().schedule(myTimer, BASE_TIMER_INTERVAL, BASE_TIMER_INTERVAL); } } } /* * Terminate a transaction. This marks the tx as terminated The tx scanner will run and remove * the tx. (non-Javadoc) * * @see javax.sip.Transaction#terminate() */ public void terminate() throws ObjectInUseException { this.setState(TransactionState.TERMINATED); } /** * Check if the From tag of the response matches the from tag of the original message. A * Response with a tag mismatch should be dropped if a Dialog has been created for the * original request. * * @param sipResponse the response to check. * @return true if the check passes. */ public boolean checkFromTag(SIPResponse sipResponse) { String originalFromTag = ((SIPRequest) this.getRequest()).getFromTag(); if (this.defaultDialog != null) { if (originalFromTag == null ^ sipResponse.getFrom().getTag() == null) { if (sipStack.isLoggingEnabled()) sipStack.getStackLogger().logDebug("From tag mismatch -- dropping response"); return false; } if (originalFromTag != null && !originalFromTag.equalsIgnoreCase(sipResponse.getFrom().getTag())) { if (sipStack.isLoggingEnabled()) sipStack.getStackLogger().logDebug("From tag mismatch -- dropping response"); return false; } } return true; } /* * (non-Javadoc) * * @see gov.nist.javax.sip.stack.ServerResponseInterface#processResponse(gov.nist.javax.sip.message.SIPResponse, * gov.nist.javax.sip.stack.MessageChannel) */ public void processResponse(SIPResponse sipResponse, MessageChannel incomingChannel) { // If a dialog has already been created for this response, // pass it up. SIPDialog dialog = null; String method = sipResponse.getCSeq().getMethod(); String dialogId = sipResponse.getDialogId(false); if (method.equals(Request.CANCEL) && lastRequest != null) { // JvB for CANCEL: use invite CT in CANCEL request to get dialog // (instead of stripping tag) SIPClientTransaction ict = (SIPClientTransaction) lastRequest.getInviteTransaction(); if (ict != null) { dialog = ict.defaultDialog; } } else { dialog = this.getDialog(dialogId); } // JvB: Check all conditions required for creating a new Dialog if (dialog == null) { int code = sipResponse.getStatusCode(); if ((code > 100 && code < 300) /* skip 100 (may have a to tag */ && (sipResponse.getToTag() != null || sipStack.isRfc2543Supported()) && sipStack.isDialogCreated(method)) { /* * Dialog cannot be found for the response. This must be a forked response. no * dialog assigned to this response but a default dialog has been assigned. Note * that if automatic dialog support is configured then a default dialog is always * created. */ synchronized (this) { /* * We need synchronization here because two responses may compete for the * default dialog simultaneously */ if (defaultDialog != null) { if (sipResponse.getFromTag() != null) { SIPResponse dialogResponse = defaultDialog.getLastResponse(); String defaultDialogId = defaultDialog.getDialogId(); if (dialogResponse == null || (method.equals(Request.SUBSCRIBE) && dialogResponse.getCSeq().getMethod().equals( Request.NOTIFY) && defaultDialogId .equals(dialogId))) { // The default dialog has not been claimed yet. defaultDialog.setLastResponse(this, sipResponse); dialog = defaultDialog; } else { /* * check if we have created one previously (happens in the case of * REINVITE processing. JvB: should not happen, this.defaultDialog * should then get set in Dialog#sendRequest line 1662 */ dialog = sipStack.getDialog(dialogId); if (dialog == null) { if (defaultDialog.isAssigned()) { /* * Nop we dont have one. so go ahead and allocate a new * one. */ dialog = sipStack.createDialog(this, sipResponse); } } } if ( dialog != null ) { this.setDialog(dialog, dialog.getDialogId()); } else { sipStack.getStackLogger().logError("dialog is unexpectedly null",new NullPointerException()); } } else { throw new RuntimeException("Response without from-tag"); } } else { // Need to create a new Dialog, this becomes default // JvB: not sure if this ever gets executed if (sipStack.isAutomaticDialogSupportEnabled) { dialog = sipStack.createDialog(this, sipResponse); this.setDialog(dialog, dialog.getDialogId()); } } } // synchronized } else { dialog = defaultDialog; } } else { dialog.setLastResponse(this, sipResponse); } this.processResponse(sipResponse, incomingChannel, dialog); } /* * (non-Javadoc) * * @see gov.nist.javax.sip.stack.SIPTransaction#getDialog() */ public Dialog getDialog() { // This is for backwards compatibility. Dialog retval = null; if (this.lastResponse != null && this.lastResponse.getFromTag() != null && this.lastResponse.getToTag() != null && this.lastResponse.getStatusCode() != 100) { String dialogId = this.lastResponse.getDialogId(false); retval = (Dialog) getDialog(dialogId); } if (retval == null) { retval = (Dialog) this.defaultDialog; } if (sipStack.isLoggingEnabled()) { sipStack.getStackLogger().logDebug( " sipDialogs = " + sipDialogs + " default dialog " + this.defaultDialog + " retval " + retval); } return retval; } /* * (non-Javadoc) * * @see gov.nist.javax.sip.stack.SIPTransaction#setDialog(gov.nist.javax.sip.stack.SIPDialog, * gov.nist.javax.sip.message.SIPMessage) */ public SIPDialog getDialog(String dialogId) { SIPDialog retval = (SIPDialog) this.sipDialogs.get(dialogId); return retval; } /* * (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: " + dialogId + "sipDialog = " + sipDialog); if (sipDialog == null) { if (sipStack.isLoggingEnabled()) sipStack.getStackLogger().logError("NULL DIALOG!!"); throw new NullPointerException("bad dialog null"); } if (this.defaultDialog == null) { this.defaultDialog = sipDialog; if ( this.getMethod().equals(Request.INVITE) && this.getSIPStack().maxForkTime != 0) { this.getSIPStack().addForkedClientTransaction(this); } } if (dialogId != null && sipDialog.getDialogId() != null) { this.sipDialogs.put(dialogId, sipDialog); } } public SIPDialog getDefaultDialog() { return this.defaultDialog; } /** * Set the next hop ( if it has already been computed). * * @param hop -- the hop that has been previously computed. */ public void setNextHop(Hop hop) { this.nextHop = hop; } /** * Reeturn the previously computed next hop (avoid computing it twice). * * @return -- next hop previously computed. */ public Hop getNextHop() { return nextHop; } /** * Set this flag if you want your Listener to get Timeout.RETRANSMIT notifications each time a * retransmission occurs. * * @param notifyOnRetransmit the notifyOnRetransmit to set */ public void setNotifyOnRetransmit(boolean notifyOnRetransmit) { this.notifyOnRetransmit = notifyOnRetransmit; } /** * @return the notifyOnRetransmit */ public boolean isNotifyOnRetransmit() { return notifyOnRetransmit; } public void alertIfStillInCallingStateBy(int count) { this.timeoutIfStillInCallingState = true; this.callingStateTimeoutCount = count; } }