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