/* * 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