1571 lines
67 KiB
Java
1571 lines
67 KiB
Java
/*
|
|
* Conditions Of Use
|
|
*
|
|
* This software was developed by employees of the National Institute of
|
|
* Standards and Technology (NIST), an agency of the Federal Government.
|
|
* Pursuant to title 15 Untied States Code Section 105, works of NIST
|
|
* employees are not subject to copyright protection in the United States
|
|
* and are considered to be in the public domain. As a result, a formal
|
|
* license is not needed to use the software.
|
|
*
|
|
* This software is provided by NIST as a service and is expressly
|
|
* provided "AS IS." NIST MAKES NO WARRANTY OF ANY KIND, EXPRESS, IMPLIED
|
|
* OR STATUTORY, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTY OF
|
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT
|
|
* AND DATA ACCURACY. NIST does not warrant or make any representations
|
|
* regarding the use of the software or the results thereof, including but
|
|
* not limited to the correctness, accuracy, reliability or usefulness of
|
|
* the software.
|
|
*
|
|
* Permission to use this software is contingent upon your acceptance
|
|
* of the terms of this agreement
|
|
*
|
|
* .
|
|
*
|
|
*/
|
|
package gov.nist.javax.sip.stack;
|
|
|
|
import gov.nist.core.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;
|
|
}
|
|
|
|
|
|
|
|
}
|