/* * 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) * *
* * * * * * * |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 * * * * * * ** * * @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
* * * * * * |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 * * * * ** * @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. * *
* * * * * * |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|<--------------+ * | | * +-----------+ * * * * ** * @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