3344 lines
126 KiB
Java
3344 lines
126 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
|
|
*
|
|
* .
|
|
*
|
|
*/
|
|
/**************************************************************************/
|
|
/* Product of NIST Advanced Networking Technologies Division */
|
|
/**************************************************************************/
|
|
package gov.nist.javax.sip.stack;
|
|
|
|
import gov.nist.core.InternalErrorHandler;
|
|
import gov.nist.core.NameValueList;
|
|
import gov.nist.javax.sip.DialogExt;
|
|
import gov.nist.javax.sip.ListeningPointImpl;
|
|
import gov.nist.javax.sip.SipListenerExt;
|
|
import gov.nist.javax.sip.SipProviderImpl;
|
|
import gov.nist.javax.sip.Utils;
|
|
import gov.nist.javax.sip.address.AddressImpl;
|
|
import gov.nist.javax.sip.address.SipUri;
|
|
import gov.nist.javax.sip.header.Authorization;
|
|
import gov.nist.javax.sip.header.CSeq;
|
|
import gov.nist.javax.sip.header.Contact;
|
|
import gov.nist.javax.sip.header.ContactList;
|
|
import gov.nist.javax.sip.header.From;
|
|
import gov.nist.javax.sip.header.MaxForwards;
|
|
import gov.nist.javax.sip.header.RAck;
|
|
import gov.nist.javax.sip.header.RSeq;
|
|
import gov.nist.javax.sip.header.Reason;
|
|
import gov.nist.javax.sip.header.RecordRoute;
|
|
import gov.nist.javax.sip.header.RecordRouteList;
|
|
import gov.nist.javax.sip.header.Require;
|
|
import gov.nist.javax.sip.header.Route;
|
|
import gov.nist.javax.sip.header.RouteList;
|
|
import gov.nist.javax.sip.header.SIPHeader;
|
|
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.message.MessageFactoryImpl;
|
|
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.io.PrintWriter;
|
|
import java.io.Serializable;
|
|
import java.io.StringWriter;
|
|
import java.net.InetAddress;
|
|
import java.text.ParseException;
|
|
import java.util.ArrayList;
|
|
import java.util.Iterator;
|
|
import java.util.LinkedList;
|
|
import java.util.List;
|
|
import java.util.ListIterator;
|
|
import java.util.Set;
|
|
import java.util.concurrent.CopyOnWriteArraySet;
|
|
import java.util.concurrent.Semaphore;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
import javax.sip.ClientTransaction;
|
|
import javax.sip.DialogDoesNotExistException;
|
|
import javax.sip.DialogState;
|
|
import javax.sip.IOExceptionEvent;
|
|
import javax.sip.InvalidArgumentException;
|
|
import javax.sip.ListeningPoint;
|
|
import javax.sip.ObjectInUseException;
|
|
import javax.sip.SipException;
|
|
import javax.sip.Transaction;
|
|
import javax.sip.TransactionDoesNotExistException;
|
|
import javax.sip.TransactionState;
|
|
import javax.sip.address.Address;
|
|
import javax.sip.address.Hop;
|
|
import javax.sip.address.SipURI;
|
|
import javax.sip.header.CallIdHeader;
|
|
import javax.sip.header.ContactHeader;
|
|
import javax.sip.header.EventHeader;
|
|
import javax.sip.header.OptionTag;
|
|
import javax.sip.header.RAckHeader;
|
|
import javax.sip.header.RSeqHeader;
|
|
import javax.sip.header.ReasonHeader;
|
|
import javax.sip.header.RequireHeader;
|
|
import javax.sip.header.RouteHeader;
|
|
import javax.sip.header.SupportedHeader;
|
|
import javax.sip.header.TimeStampHeader;
|
|
import javax.sip.message.Request;
|
|
import javax.sip.message.Response;
|
|
|
|
/*
|
|
* Acknowledgements:
|
|
*
|
|
* Bugs in this class were reported by Antonis Karydas, Brad Templeton, Jeff Adams, Alex Rootham ,
|
|
* Martin Le Clerk, Christophe Anzille, Andreas Bystrom, Lebing Xie, Jeroen van Bemmel. Hagai Sela
|
|
* reported a bug in updating the route set (on RE-INVITE). Jens Tinfors submitted a bug fix and
|
|
* the .equals method. Jan Schaumloeffel contributed a buf fix ( memory leak was happening when
|
|
* 180 contained a To tag.
|
|
*
|
|
*/
|
|
|
|
/**
|
|
* Tracks dialogs. A dialog is a peer to peer association of communicating SIP entities. For
|
|
* INVITE transactions, a Dialog is created when a success message is received (i.e. a response
|
|
* that has a To tag). The SIP Protocol stores enough state in the message structure to extract a
|
|
* dialog identifier that can be used to retrieve this structure from the SipStack.
|
|
*
|
|
* @version 1.2 $Revision: 1.159 $ $Date: 2010/01/08 15:14:12 $
|
|
*
|
|
* @author M. Ranganathan
|
|
*
|
|
*
|
|
*/
|
|
|
|
public class SIPDialog implements javax.sip.Dialog, DialogExt {
|
|
|
|
private static final long serialVersionUID = -1429794423085204069L;
|
|
|
|
private transient boolean dialogTerminatedEventDelivered; // prevent duplicate
|
|
|
|
private transient String stackTrace; // for semaphore debugging.
|
|
|
|
private String method;
|
|
|
|
// delivery of the event
|
|
private transient boolean isAssigned;
|
|
|
|
private boolean reInviteFlag;
|
|
|
|
private transient Object applicationData; // Opaque pointer to application data.
|
|
|
|
private transient SIPRequest originalRequest;
|
|
|
|
// Last response (JvB: either sent or received).
|
|
private SIPResponse lastResponse;
|
|
|
|
// Should be transient, in case the dialog is serialized it will be null
|
|
// so when a subsequent request will be sent it will be set and a new message channel can be
|
|
// created
|
|
private transient SIPTransaction firstTransaction;
|
|
|
|
private transient SIPTransaction lastTransaction;
|
|
|
|
private String dialogId;
|
|
|
|
private transient String earlyDialogId;
|
|
|
|
private long localSequenceNumber;
|
|
|
|
private long remoteSequenceNumber;
|
|
|
|
protected String myTag;
|
|
|
|
protected String hisTag;
|
|
|
|
private RouteList routeList;
|
|
|
|
private transient SIPTransactionStack sipStack;
|
|
|
|
private int dialogState;
|
|
|
|
protected transient boolean ackSeen;
|
|
|
|
private transient SIPRequest lastAckSent;
|
|
|
|
private SIPRequest lastAckReceived;
|
|
|
|
// could be set on recovery by examining the method looks like a duplicate of ackSeen
|
|
protected transient boolean ackProcessed;
|
|
|
|
protected transient DialogTimerTask timerTask;
|
|
|
|
protected transient Long nextSeqno;
|
|
|
|
private transient int retransmissionTicksLeft;
|
|
|
|
private transient int prevRetransmissionTicks;
|
|
|
|
private long originalLocalSequenceNumber;
|
|
|
|
// This is for debugging only.
|
|
private transient int ackLine;
|
|
|
|
// Audit tag used by the SIP Stack audit
|
|
public transient long auditTag = 0;
|
|
|
|
// The following fields are extracted from the request that created the
|
|
// Dialog.
|
|
|
|
protected javax.sip.address.Address localParty;
|
|
|
|
protected javax.sip.address.Address remoteParty;
|
|
|
|
protected CallIdHeader callIdHeader;
|
|
|
|
public final static int NULL_STATE = -1;
|
|
|
|
public final static int EARLY_STATE = DialogState._EARLY;
|
|
|
|
public final static int CONFIRMED_STATE = DialogState._CONFIRMED;
|
|
|
|
public final static int TERMINATED_STATE = DialogState._TERMINATED;
|
|
|
|
// the amount of time to keep this dialog around before the stack GC's it
|
|
|
|
private static final int DIALOG_LINGER_TIME = 8;
|
|
|
|
private boolean serverTransactionFlag;
|
|
|
|
private transient SipProviderImpl sipProvider;
|
|
|
|
private boolean terminateOnBye;
|
|
|
|
private transient boolean byeSent; // Flag set when BYE is sent, to disallow new
|
|
|
|
// requests
|
|
|
|
private Address remoteTarget;
|
|
|
|
private EventHeader eventHeader; // for Subscribe notify
|
|
|
|
// Stores the last OK for the INVITE
|
|
// Used in createAck.
|
|
private transient long lastInviteOkReceived;
|
|
|
|
private transient Semaphore ackSem = new Semaphore(1);
|
|
|
|
private transient int reInviteWaitTime = 100;
|
|
|
|
private transient DialogDeleteTask dialogDeleteTask;
|
|
|
|
private transient DialogDeleteIfNoAckSentTask dialogDeleteIfNoAckSentTask;
|
|
|
|
private transient boolean isAcknowledged;
|
|
|
|
private transient long highestSequenceNumberAcknowledged = -1;
|
|
|
|
private boolean isBackToBackUserAgent;
|
|
|
|
private boolean sequenceNumberValidation = true;
|
|
|
|
// List of event listeners for this dialog
|
|
private transient Set<SIPDialogEventListener> eventListeners;
|
|
// added for Issue 248 : https://jain-sip.dev.java.net/issues/show_bug.cgi?id=248
|
|
private Semaphore timerTaskLock = new Semaphore(1);
|
|
|
|
// We store here the useful data from the first transaction without having to
|
|
// keep the whole transaction object for the duration of the dialog. It also
|
|
// contains the non-transient information used in the replication of dialogs.
|
|
protected boolean firstTransactionSecure;
|
|
protected boolean firstTransactionSeen;
|
|
protected String firstTransactionMethod;
|
|
protected String firstTransactionId;
|
|
protected boolean firstTransactionIsServerTransaction;
|
|
protected int firstTransactionPort = 5060;
|
|
protected Contact contactHeader;
|
|
|
|
// //////////////////////////////////////////////////////
|
|
// Inner classes
|
|
// //////////////////////////////////////////////////////
|
|
|
|
/**
|
|
* This task waits till a pending ACK has been recorded and then sends out a re-INVITE. This
|
|
* is to prevent interleaving INVITEs ( which will result in a 493 from the UA that receives
|
|
* the out of order INVITE). This is primarily for B2BUA support. A B2BUA may send a delayed
|
|
* ACK while it does mid call codec renegotiation. In the meanwhile, it cannot send an intervening
|
|
* re-INVITE otherwise the othr end will respond with a REQUEST_PENDING. We want to avoid this
|
|
* condition. Hence we wait till the ACK for the previous re-INVITE has been sent before
|
|
* sending the next re-INVITE.
|
|
*/
|
|
public class ReInviteSender implements Runnable, Serializable {
|
|
private static final long serialVersionUID = 1019346148741070635L;
|
|
ClientTransaction ctx;
|
|
|
|
public void terminate() {
|
|
try {
|
|
ctx.terminate();
|
|
Thread.currentThread().interrupt();
|
|
} catch (ObjectInUseException e) {
|
|
sipStack.getStackLogger().logError("unexpected error", e);
|
|
}
|
|
}
|
|
|
|
public ReInviteSender(ClientTransaction ctx) {
|
|
this.ctx = ctx;
|
|
}
|
|
|
|
public void run() {
|
|
try {
|
|
long timeToWait = 0;
|
|
long startTime = System.currentTimeMillis();
|
|
|
|
if (!SIPDialog.this.takeAckSem()) {
|
|
/*
|
|
* Could not send re-INVITE fire a timeout on the INVITE.
|
|
*/
|
|
if (sipStack.isLoggingEnabled())
|
|
sipStack.getStackLogger().logError(
|
|
"Could not send re-INVITE time out ClientTransaction");
|
|
((SIPClientTransaction) ctx).fireTimeoutTimer();
|
|
/*
|
|
* Send BYE to the Dialog.
|
|
*/
|
|
if ( sipProvider.getSipListener() != null && sipProvider.getSipListener() instanceof SipListenerExt ) {
|
|
raiseErrorEvent(SIPDialogErrorEvent.DIALOG_REINVITE_TIMEOUT);
|
|
} else {
|
|
Request byeRequest = SIPDialog.this.createRequest(Request.BYE);
|
|
if ( MessageFactoryImpl.getDefaultUserAgentHeader() != null ) {
|
|
byeRequest.addHeader(MessageFactoryImpl.getDefaultUserAgentHeader());
|
|
}
|
|
ReasonHeader reasonHeader = new Reason();
|
|
reasonHeader.setCause(1024);
|
|
reasonHeader.setText("Timed out waiting to re-INVITE");
|
|
byeRequest.addHeader(reasonHeader);
|
|
ClientTransaction byeCtx = SIPDialog.this.getSipProvider().getNewClientTransaction(byeRequest);
|
|
SIPDialog.this.sendRequest(byeCtx);
|
|
return;
|
|
}
|
|
}
|
|
if (getState() != DialogState.TERMINATED) {
|
|
|
|
timeToWait = System.currentTimeMillis() - startTime;
|
|
}
|
|
|
|
/*
|
|
* If we had to wait for ACK then wait for the ACK to actually get to the other
|
|
* side. Wait for any ACK retransmissions to finish. Then send out the request.
|
|
* This is a hack in support of some UA that want re-INVITEs to be spaced out in
|
|
* time ( else they return a 400 error code ).
|
|
*/
|
|
try {
|
|
if (timeToWait != 0) {
|
|
Thread.sleep(SIPDialog.this.reInviteWaitTime);
|
|
}
|
|
} catch (InterruptedException ex) {
|
|
if (sipStack.isLoggingEnabled())
|
|
sipStack.getStackLogger().logDebug("Interrupted sleep");
|
|
return;
|
|
}
|
|
if (SIPDialog.this.getState() != DialogState.TERMINATED) {
|
|
SIPDialog.this.sendRequest(ctx, true);
|
|
}
|
|
if (sipStack.isLoggingEnabled())
|
|
sipStack.getStackLogger().logDebug("re-INVITE successfully sent");
|
|
} catch (Exception ex) {
|
|
sipStack.getStackLogger().logError("Error sending re-INVITE", ex);
|
|
} finally {
|
|
this.ctx = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
class LingerTimer extends SIPStackTimerTask implements Serializable {
|
|
|
|
public LingerTimer() {
|
|
|
|
}
|
|
|
|
protected void runTask() {
|
|
SIPDialog dialog = SIPDialog.this;
|
|
if(eventListeners != null) {
|
|
eventListeners.clear();
|
|
}
|
|
timerTaskLock = null;
|
|
sipStack.removeDialog(dialog);
|
|
}
|
|
|
|
}
|
|
|
|
class DialogTimerTask extends SIPStackTimerTask implements Serializable {
|
|
int nRetransmissions;
|
|
|
|
SIPServerTransaction transaction;
|
|
|
|
public DialogTimerTask(SIPServerTransaction transaction) {
|
|
this.transaction = transaction;
|
|
this.nRetransmissions = 0;
|
|
}
|
|
|
|
protected void runTask() {
|
|
// If I ACK has not been seen on Dialog,
|
|
// resend last response.
|
|
SIPDialog dialog = SIPDialog.this;
|
|
if (sipStack.isLoggingEnabled())
|
|
sipStack.getStackLogger().logDebug("Running dialog timer");
|
|
nRetransmissions++;
|
|
SIPServerTransaction transaction = this.transaction;
|
|
/*
|
|
* Issue 106. Section 13.3.1.4 RFC 3261 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 If the server retransmits the 2xx response for 64*T1
|
|
* seconds without receiving an ACK, the dialog is confirmed, but the session SHOULD
|
|
* be terminated.
|
|
*/
|
|
|
|
if (nRetransmissions > 64 * SIPTransaction.T1) {
|
|
if (sipProvider.getSipListener() != null && sipProvider.getSipListener() instanceof SipListenerExt ) {
|
|
raiseErrorEvent(SIPDialogErrorEvent.DIALOG_ACK_NOT_RECEIVED_TIMEOUT);
|
|
} else {
|
|
dialog.delete();
|
|
}
|
|
if (transaction != null
|
|
&& transaction.getState() != javax.sip.TransactionState.TERMINATED) {
|
|
transaction.raiseErrorEvent(SIPTransactionErrorEvent.TIMEOUT_ERROR);
|
|
}
|
|
} else if ((!dialog.ackSeen) && (transaction != null)) {
|
|
// Retransmit to 200 until ack receivedialog.
|
|
SIPResponse response = transaction.getLastResponse();
|
|
if (response.getStatusCode() == 200) {
|
|
try {
|
|
|
|
// resend the last response.
|
|
if (dialog.toRetransmitFinalResponse(transaction.T2))
|
|
transaction.sendMessage(response);
|
|
|
|
} catch (IOException ex) {
|
|
|
|
raiseIOException(transaction.getPeerAddress(), transaction.getPeerPort(),
|
|
transaction.getPeerProtocol());
|
|
|
|
} finally {
|
|
// Need to fire the timer so
|
|
// transaction will eventually
|
|
// time out whether or not
|
|
// the IOException occurs
|
|
// Note that this firing also
|
|
// drives Listener timeout.
|
|
SIPTransactionStack stack = dialog.sipStack;
|
|
if (stack.isLoggingEnabled()) {
|
|
stack.getStackLogger().logDebug("resend 200 response from " + dialog);
|
|
}
|
|
transaction.fireTimer();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Stop running this timer if the dialog is in the
|
|
// confirmed state or ack seen if retransmit filter on.
|
|
if (dialog.isAckSeen() || dialog.dialogState == TERMINATED_STATE) {
|
|
this.transaction = null;
|
|
this.cancel();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* This timer task is used to garbage collect the dialog after some time.
|
|
*
|
|
*/
|
|
|
|
class DialogDeleteTask extends SIPStackTimerTask implements Serializable {
|
|
|
|
protected void runTask() {
|
|
delete();
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* This timer task is used to garbage collect the dialog after some time.
|
|
*
|
|
*/
|
|
|
|
class DialogDeleteIfNoAckSentTask extends SIPStackTimerTask implements Serializable {
|
|
private long seqno;
|
|
|
|
public DialogDeleteIfNoAckSentTask(long seqno) {
|
|
this.seqno = seqno;
|
|
}
|
|
|
|
protected void runTask() {
|
|
if (SIPDialog.this.highestSequenceNumberAcknowledged < seqno) {
|
|
/*
|
|
* Did not send ACK so we need to delete the dialog.
|
|
* B2BUA NOTE: we may want to send BYE to the Dialog at this
|
|
* point. Do we want to make this behavior tailorable?
|
|
*/
|
|
dialogDeleteIfNoAckSentTask = null;
|
|
if ( !SIPDialog.this.isBackToBackUserAgent) {
|
|
if (sipStack.isLoggingEnabled())
|
|
sipStack.getStackLogger().logError("ACK Was not sent. killing dialog");
|
|
if ( ((SipProviderImpl)sipProvider).getSipListener() instanceof SipListenerExt ){
|
|
raiseErrorEvent(SIPDialogErrorEvent.DIALOG_ACK_NOT_SENT_TIMEOUT);
|
|
} else {
|
|
delete();
|
|
}
|
|
} else {
|
|
if (sipStack.isLoggingEnabled())
|
|
sipStack.getStackLogger().logError("ACK Was not sent. Sending BYE");
|
|
if ( ((SipProviderImpl)sipProvider).getSipListener() instanceof SipListenerExt ){
|
|
raiseErrorEvent(SIPDialogErrorEvent.DIALOG_ACK_NOT_SENT_TIMEOUT);
|
|
} else {
|
|
|
|
/*
|
|
* Send BYE to the Dialog.
|
|
* This will be removed for the next spec revision.
|
|
*/
|
|
try {
|
|
Request byeRequest = SIPDialog.this.createRequest(Request.BYE);
|
|
if ( MessageFactoryImpl.getDefaultUserAgentHeader() != null ) {
|
|
byeRequest.addHeader(MessageFactoryImpl.getDefaultUserAgentHeader());
|
|
}
|
|
ReasonHeader reasonHeader = new Reason();
|
|
reasonHeader.setProtocol("SIP");
|
|
reasonHeader.setCause(1025);
|
|
reasonHeader.setText("Timed out waiting to send ACK");
|
|
byeRequest.addHeader(reasonHeader);
|
|
ClientTransaction byeCtx = SIPDialog.this.getSipProvider().getNewClientTransaction(byeRequest);
|
|
SIPDialog.this.sendRequest(byeCtx);
|
|
return;
|
|
} catch (Exception ex) {
|
|
SIPDialog.this.delete();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// ///////////////////////////////////////////////////////////
|
|
// Constructors.
|
|
// ///////////////////////////////////////////////////////////
|
|
/**
|
|
* Protected Dialog constructor.
|
|
*/
|
|
private SIPDialog(SipProviderImpl provider) {
|
|
this.terminateOnBye = true;
|
|
this.routeList = new RouteList();
|
|
this.dialogState = NULL_STATE; // not yet initialized.
|
|
localSequenceNumber = 0;
|
|
remoteSequenceNumber = -1;
|
|
this.sipProvider = provider;
|
|
eventListeners = new CopyOnWriteArraySet<SIPDialogEventListener>();
|
|
}
|
|
|
|
private void recordStackTrace() {
|
|
StringWriter stringWriter = new StringWriter();
|
|
PrintWriter writer = new PrintWriter(stringWriter);
|
|
new Exception().printStackTrace(writer);
|
|
this.stackTrace = stringWriter.getBuffer().toString();
|
|
}
|
|
|
|
/**
|
|
* Constructor given the first transaction.
|
|
*
|
|
* @param transaction is the first transaction.
|
|
*/
|
|
public SIPDialog(SIPTransaction transaction) {
|
|
this(transaction.getSipProvider());
|
|
|
|
SIPRequest sipRequest = (SIPRequest) transaction.getRequest();
|
|
this.callIdHeader = sipRequest.getCallId();
|
|
this.earlyDialogId = sipRequest.getDialogId(false);
|
|
if (transaction == null)
|
|
throw new NullPointerException("Null tx");
|
|
this.sipStack = transaction.sipStack;
|
|
|
|
// this.defaultRouter = new DefaultRouter((SipStack) sipStack,
|
|
// sipStack.outboundProxy);
|
|
|
|
this.sipProvider = (SipProviderImpl) transaction.getSipProvider();
|
|
if (sipProvider == null)
|
|
throw new NullPointerException("Null Provider!");
|
|
this.addTransaction(transaction);
|
|
if (sipStack.isLoggingEnabled()) {
|
|
sipStack.getStackLogger().logDebug("Creating a dialog : " + this);
|
|
sipStack.getStackLogger().logDebug(
|
|
"provider port = " + this.sipProvider.getListeningPoint().getPort());
|
|
sipStack.getStackLogger().logStackTrace();
|
|
}
|
|
this.isBackToBackUserAgent = sipStack.isBackToBackUserAgent;
|
|
addEventListener(sipStack);
|
|
}
|
|
|
|
/**
|
|
* Constructor given a transaction and a response.
|
|
*
|
|
* @param transaction -- the transaction ( client/server)
|
|
* @param sipResponse -- response with the appropriate tags.
|
|
*/
|
|
public SIPDialog(SIPClientTransaction transaction, SIPResponse sipResponse) {
|
|
this(transaction);
|
|
if (sipResponse == null)
|
|
throw new NullPointerException("Null SipResponse");
|
|
this.setLastResponse(transaction, sipResponse);
|
|
this.isBackToBackUserAgent = sipStack.isBackToBackUserAgent;
|
|
}
|
|
|
|
/**
|
|
* create a sip dialog with a response ( no tx)
|
|
*/
|
|
public SIPDialog(SipProviderImpl sipProvider, SIPResponse sipResponse) {
|
|
this(sipProvider);
|
|
this.sipStack = (SIPTransactionStack) sipProvider.getSipStack();
|
|
this.setLastResponse(null, sipResponse);
|
|
this.localSequenceNumber = sipResponse.getCSeq().getSeqNumber();
|
|
this.originalLocalSequenceNumber = localSequenceNumber;
|
|
this.myTag = sipResponse.getFrom().getTag();
|
|
this.hisTag = sipResponse.getTo().getTag();
|
|
this.localParty = sipResponse.getFrom().getAddress();
|
|
this.remoteParty = sipResponse.getTo().getAddress();
|
|
this.method = sipResponse.getCSeq().getMethod();
|
|
this.callIdHeader = sipResponse.getCallId();
|
|
this.serverTransactionFlag = false;
|
|
if (sipStack.isLoggingEnabled()) {
|
|
sipStack.getStackLogger().logDebug("Creating a dialog : " + this);
|
|
sipStack.getStackLogger().logStackTrace();
|
|
}
|
|
this.isBackToBackUserAgent = sipStack.isBackToBackUserAgent;
|
|
addEventListener(sipStack);
|
|
}
|
|
|
|
// ///////////////////////////////////////////////////////////
|
|
// Private methods
|
|
// ///////////////////////////////////////////////////////////
|
|
/**
|
|
* A debugging print routine.
|
|
*/
|
|
private void printRouteList() {
|
|
if (sipStack.isLoggingEnabled()) {
|
|
sipStack.getStackLogger().logDebug("this : " + this);
|
|
sipStack.getStackLogger().logDebug("printRouteList : " + this.routeList.encode());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return true if this is a client dialog.
|
|
*
|
|
* @return true if the transaction that created this dialog is a client transaction and false
|
|
* otherwise.
|
|
*/
|
|
private boolean isClientDialog() {
|
|
SIPTransaction transaction = (SIPTransaction) this.getFirstTransaction();
|
|
return transaction instanceof SIPClientTransaction;
|
|
}
|
|
|
|
/**
|
|
* Raise an io exception for asyncrhonous retransmission of responses
|
|
*
|
|
* @param host -- host to where the io was headed
|
|
* @param port -- remote port
|
|
* @param protocol -- protocol (udp/tcp/tls)
|
|
*/
|
|
private void raiseIOException(String host, int port, String protocol) {
|
|
// Error occured in retransmitting response.
|
|
// Deliver the error event to the listener
|
|
// Kill the dialog.
|
|
|
|
IOExceptionEvent ioError = new IOExceptionEvent(this, host, port, protocol);
|
|
sipProvider.handleEvent(ioError, null);
|
|
|
|
setState(SIPDialog.TERMINATED_STATE);
|
|
}
|
|
|
|
/**
|
|
* Raise a dialog timeout if an ACK has not been sent or received
|
|
*
|
|
* @param dialogTimeoutError
|
|
*/
|
|
private void raiseErrorEvent(int dialogTimeoutError) {
|
|
// Error event to send to all listeners
|
|
SIPDialogErrorEvent newErrorEvent;
|
|
// Iterator through the list of listeners
|
|
Iterator<SIPDialogEventListener> listenerIterator;
|
|
// Next listener in the list
|
|
SIPDialogEventListener nextListener;
|
|
|
|
// Create the error event
|
|
newErrorEvent = new SIPDialogErrorEvent(this, dialogTimeoutError);
|
|
|
|
// Loop through all listeners of this transaction
|
|
synchronized (eventListeners) {
|
|
listenerIterator = eventListeners.iterator();
|
|
while (listenerIterator.hasNext()) {
|
|
// Send the event to the next listener
|
|
nextListener = (SIPDialogEventListener) listenerIterator.next();
|
|
nextListener.dialogErrorEvent(newErrorEvent);
|
|
}
|
|
}
|
|
// Clear the event listeners after propagating the error.
|
|
eventListeners.clear();
|
|
// Errors always terminate a dialog except if a timeout has occured because an ACK was not sent or received, then it is the responsibility of the app to terminate
|
|
// the dialog, either by sending a BYE or by calling delete() on the dialog
|
|
if(dialogTimeoutError != SIPDialogErrorEvent.DIALOG_ACK_NOT_SENT_TIMEOUT &&
|
|
dialogTimeoutError != SIPDialogErrorEvent.DIALOG_ACK_NOT_RECEIVED_TIMEOUT &&
|
|
dialogTimeoutError != SIPDialogErrorEvent.DIALOG_REINVITE_TIMEOUT ) {
|
|
delete();
|
|
}
|
|
// we stop the timer in any case
|
|
stopTimer();
|
|
}
|
|
|
|
/**
|
|
* Set the remote party for this Dialog.
|
|
*
|
|
* @param sipMessage -- SIP Message to extract the relevant information from.
|
|
*/
|
|
private void setRemoteParty(SIPMessage sipMessage) {
|
|
|
|
if (!isServer()) {
|
|
|
|
this.remoteParty = sipMessage.getTo().getAddress();
|
|
} else {
|
|
this.remoteParty = sipMessage.getFrom().getAddress();
|
|
|
|
}
|
|
if (sipStack.isLoggingEnabled()) {
|
|
sipStack.getStackLogger().logDebug("settingRemoteParty " + this.remoteParty);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a route list extracted from a record route list. If this is a server dialog then we
|
|
* assume that the record are added to the route list IN order. If this is a client dialog
|
|
* then we assume that the record route headers give us the route list to add in reverse
|
|
* order.
|
|
*
|
|
* @param recordRouteList -- the record route list from the incoming message.
|
|
*/
|
|
|
|
private void addRoute(RecordRouteList recordRouteList) {
|
|
try {
|
|
if (this.isClientDialog()) {
|
|
// This is a client dialog so we extract the record
|
|
// route from the response and reverse its order to
|
|
// careate a route list.
|
|
this.routeList = new RouteList();
|
|
// start at the end of the list and walk backwards
|
|
|
|
ListIterator li = recordRouteList.listIterator(recordRouteList.size());
|
|
boolean addRoute = true;
|
|
while (li.hasPrevious()) {
|
|
RecordRoute rr = (RecordRoute) li.previous();
|
|
|
|
if (addRoute) {
|
|
Route route = new Route();
|
|
AddressImpl address = ((AddressImpl) ((AddressImpl) rr.getAddress())
|
|
.clone());
|
|
|
|
route.setAddress(address);
|
|
route.setParameters((NameValueList) rr.getParameters().clone());
|
|
|
|
this.routeList.add(route);
|
|
}
|
|
}
|
|
} else {
|
|
// This is a server dialog. The top most record route
|
|
// header is the one that is closest to us. We extract the
|
|
// route list in the same order as the addresses in the
|
|
// incoming request.
|
|
this.routeList = new RouteList();
|
|
ListIterator li = recordRouteList.listIterator();
|
|
boolean addRoute = true;
|
|
while (li.hasNext()) {
|
|
RecordRoute rr = (RecordRoute) li.next();
|
|
|
|
if (addRoute) {
|
|
Route route = new Route();
|
|
AddressImpl address = ((AddressImpl) ((AddressImpl) rr.getAddress())
|
|
.clone());
|
|
route.setAddress(address);
|
|
route.setParameters((NameValueList) rr.getParameters().clone());
|
|
routeList.add(route);
|
|
}
|
|
}
|
|
}
|
|
} finally {
|
|
if (sipStack.getStackLogger().isLoggingEnabled()) {
|
|
Iterator it = routeList.iterator();
|
|
|
|
while (it.hasNext()) {
|
|
SipURI sipUri = (SipURI) (((Route) it.next()).getAddress().getURI());
|
|
if (!sipUri.hasLrParam()) {
|
|
if (sipStack.isLoggingEnabled()) {
|
|
sipStack.getStackLogger().logWarning(
|
|
"NON LR route in Route set detected for dialog : " + this);
|
|
sipStack.getStackLogger().logStackTrace();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a route list extacted from the contact list of the incoming message.
|
|
*
|
|
* @param contactList -- contact list extracted from the incoming message.
|
|
*
|
|
*/
|
|
|
|
void setRemoteTarget(ContactHeader contact) {
|
|
this.remoteTarget = contact.getAddress();
|
|
if (sipStack.isLoggingEnabled()) {
|
|
sipStack.getStackLogger().logDebug("Dialog.setRemoteTarget: " + this.remoteTarget);
|
|
sipStack.getStackLogger().logStackTrace();
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Extract the route information from this SIP Message and add the relevant information to the
|
|
* route set.
|
|
*
|
|
* @param sipMessage is the SIP message for which we want to add the route.
|
|
*/
|
|
private synchronized void addRoute(SIPResponse sipResponse) {
|
|
|
|
try {
|
|
if (sipStack.isLoggingEnabled()) {
|
|
sipStack.getStackLogger().logDebug(
|
|
"setContact: dialogState: " + this + "state = " + this.getState());
|
|
}
|
|
if (sipResponse.getStatusCode() == 100) {
|
|
// Do nothing for trying messages.
|
|
return;
|
|
} else if (this.dialogState == TERMINATED_STATE) {
|
|
// Do nothing if the dialog state is terminated.
|
|
return;
|
|
} else if (this.dialogState == CONFIRMED_STATE) {
|
|
// cannot add route list after the dialog is initialized.
|
|
// Remote target is updated on RE-INVITE but not
|
|
// the route list.
|
|
if (sipResponse.getStatusCode() / 100 == 2 && !this.isServer()) {
|
|
ContactList contactList = sipResponse.getContactHeaders();
|
|
if (contactList != null
|
|
&& SIPRequest.isTargetRefresh(sipResponse.getCSeq().getMethod())) {
|
|
this.setRemoteTarget((ContactHeader) contactList.getFirst());
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Update route list on response if I am a client dialog.
|
|
if (!isServer()) {
|
|
|
|
// only update the route set if the dialog is not in the confirmed state.
|
|
if (this.getState() != DialogState.CONFIRMED
|
|
&& this.getState() != DialogState.TERMINATED) {
|
|
RecordRouteList rrlist = sipResponse.getRecordRouteHeaders();
|
|
// Add the route set from the incoming response in reverse
|
|
// order for record route headers.
|
|
if (rrlist != null) {
|
|
this.addRoute(rrlist);
|
|
} else {
|
|
// Set the rotue list to the last seen route list.
|
|
this.routeList = new RouteList();
|
|
}
|
|
}
|
|
|
|
ContactList contactList = sipResponse.getContactHeaders();
|
|
if (contactList != null) {
|
|
this.setRemoteTarget((ContactHeader) contactList.getFirst());
|
|
}
|
|
}
|
|
|
|
} finally {
|
|
if (sipStack.isLoggingEnabled()) {
|
|
sipStack.getStackLogger().logStackTrace();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get a cloned copy of route list for the Dialog.
|
|
*
|
|
* @return -- a cloned copy of the dialog route list.
|
|
*/
|
|
private synchronized RouteList getRouteList() {
|
|
if (sipStack.isLoggingEnabled())
|
|
sipStack.getStackLogger().logDebug("getRouteList " + this);
|
|
// Find the top via in the route list.
|
|
ListIterator li;
|
|
RouteList retval = new RouteList();
|
|
|
|
retval = new RouteList();
|
|
if (this.routeList != null) {
|
|
li = routeList.listIterator();
|
|
while (li.hasNext()) {
|
|
Route route = (Route) li.next();
|
|
retval.add((Route) route.clone());
|
|
}
|
|
}
|
|
|
|
if (sipStack.isLoggingEnabled()) {
|
|
sipStack.getStackLogger().logDebug("----- ");
|
|
sipStack.getStackLogger().logDebug("getRouteList for " + this);
|
|
if (retval != null)
|
|
sipStack.getStackLogger().logDebug("RouteList = " + retval.encode());
|
|
if (routeList != null)
|
|
sipStack.getStackLogger().logDebug("myRouteList = " + routeList.encode());
|
|
sipStack.getStackLogger().logDebug("----- ");
|
|
}
|
|
return retval;
|
|
}
|
|
|
|
void setRouteList(RouteList routeList) {
|
|
this.routeList = routeList;
|
|
}
|
|
|
|
/**
|
|
* Sends ACK Request to the remote party of this Dialogue.
|
|
*
|
|
*
|
|
* @param request the new ACK Request message to send.
|
|
* @param throwIOExceptionAsSipException - throws SipException if IOEx encountered. Otherwise,
|
|
* no exception is propagated.
|
|
* @param releaseAckSem - release ack semaphore.
|
|
* @throws SipException if implementation cannot send the ACK Request for any other reason
|
|
*
|
|
*/
|
|
private void sendAck(Request request, boolean throwIOExceptionAsSipException)
|
|
throws SipException {
|
|
SIPRequest ackRequest = (SIPRequest) request;
|
|
if (sipStack.isLoggingEnabled())
|
|
sipStack.getStackLogger().logDebug("sendAck" + this);
|
|
|
|
if (!ackRequest.getMethod().equals(Request.ACK))
|
|
throw new SipException("Bad request method -- should be ACK");
|
|
if (this.getState() == null || this.getState().getValue() == EARLY_STATE) {
|
|
if (sipStack.isLoggingEnabled()) {
|
|
sipStack.getStackLogger().logError(
|
|
"Bad Dialog State for " + this + " dialogID = " + this.getDialogId());
|
|
}
|
|
throw new SipException("Bad dialog state " + this.getState());
|
|
}
|
|
|
|
if (!this.getCallId().getCallId().equals(((SIPRequest) request).getCallId().getCallId())) {
|
|
if (sipStack.isLoggingEnabled()) {
|
|
sipStack.getStackLogger().logError("CallID " + this.getCallId());
|
|
sipStack.getStackLogger().logError(
|
|
"RequestCallID = " + ackRequest.getCallId().getCallId());
|
|
sipStack.getStackLogger().logError("dialog = " + this);
|
|
}
|
|
throw new SipException("Bad call ID in request");
|
|
}
|
|
try {
|
|
if (sipStack.isLoggingEnabled()) {
|
|
sipStack.getStackLogger().logDebug(
|
|
"setting from tag For outgoing ACK= " + this.getLocalTag());
|
|
sipStack.getStackLogger().logDebug(
|
|
"setting To tag for outgoing ACK = " + this.getRemoteTag());
|
|
sipStack.getStackLogger().logDebug("ack = " + ackRequest);
|
|
}
|
|
if (this.getLocalTag() != null)
|
|
ackRequest.getFrom().setTag(this.getLocalTag());
|
|
if (this.getRemoteTag() != null)
|
|
ackRequest.getTo().setTag(this.getRemoteTag());
|
|
} catch (ParseException ex) {
|
|
throw new SipException(ex.getMessage());
|
|
}
|
|
|
|
Hop hop = sipStack.getNextHop(ackRequest);
|
|
// Hop hop = defaultRouter.getNextHop(ackRequest);
|
|
if (hop == null)
|
|
throw new SipException("No route!");
|
|
try {
|
|
if (sipStack.isLoggingEnabled())
|
|
sipStack.getStackLogger().logDebug("hop = " + hop);
|
|
ListeningPointImpl lp = (ListeningPointImpl) this.sipProvider.getListeningPoint(hop
|
|
.getTransport());
|
|
if (lp == null)
|
|
throw new SipException("No listening point for this provider registered at "
|
|
+ hop);
|
|
InetAddress inetAddress = InetAddress.getByName(hop.getHost());
|
|
MessageChannel messageChannel = lp.getMessageProcessor().createMessageChannel(
|
|
inetAddress, hop.getPort());
|
|
boolean releaseAckSem = false;
|
|
long cseqNo = ((SIPRequest)request).getCSeq().getSeqNumber();
|
|
if (!this.isAckSent(cseqNo)) {
|
|
releaseAckSem = true;
|
|
}
|
|
|
|
this.setLastAckSent(ackRequest);
|
|
messageChannel.sendMessage(ackRequest);
|
|
// Sent atleast one ACK.
|
|
this.isAcknowledged = true;
|
|
this.highestSequenceNumberAcknowledged = Math.max(this.highestSequenceNumberAcknowledged,
|
|
((SIPRequest)ackRequest).getCSeq().getSeqNumber());
|
|
if (releaseAckSem && this.isBackToBackUserAgent) {
|
|
this.releaseAckSem();
|
|
} else {
|
|
if ( sipStack.isLoggingEnabled() ) {
|
|
sipStack.getStackLogger().logDebug("Not releasing ack sem for " + this + " isAckSent " + releaseAckSem );
|
|
}
|
|
}
|
|
} catch (IOException ex) {
|
|
if (throwIOExceptionAsSipException)
|
|
throw new SipException("Could not send ack", ex);
|
|
this.raiseIOException(hop.getHost(), hop.getPort(), hop.getTransport());
|
|
} catch (SipException ex) {
|
|
if (sipStack.isLoggingEnabled())
|
|
sipStack.getStackLogger().logException(ex);
|
|
throw ex;
|
|
} catch (Exception ex) {
|
|
if (sipStack.isLoggingEnabled())
|
|
sipStack.getStackLogger().logException(ex);
|
|
throw new SipException("Could not create message channel", ex);
|
|
}
|
|
if (this.dialogDeleteTask != null) {
|
|
this.dialogDeleteTask.cancel();
|
|
this.dialogDeleteTask = null;
|
|
}
|
|
this.ackSeen = true;
|
|
|
|
}
|
|
|
|
// /////////////////////////////////////////////////////////////
|
|
// Package local methods
|
|
// /////////////////////////////////////////////////////////////
|
|
|
|
/**
|
|
* Set the stack address. Prevent us from routing messages to ourselves.
|
|
*
|
|
* @param sipStack the address of the SIP stack.
|
|
*
|
|
*/
|
|
void setStack(SIPTransactionStack sipStack) {
|
|
this.sipStack = sipStack;
|
|
|
|
}
|
|
|
|
/**
|
|
* Get the stack .
|
|
*
|
|
* @return sipStack the SIP stack of the dialog.
|
|
*
|
|
*/
|
|
SIPTransactionStack getStack() {
|
|
return sipStack;
|
|
}
|
|
|
|
/**
|
|
* Return True if this dialog is terminated on BYE.
|
|
*
|
|
*/
|
|
boolean isTerminatedOnBye() {
|
|
|
|
return this.terminateOnBye;
|
|
}
|
|
|
|
/**
|
|
* Mark that the dialog has seen an ACK.
|
|
*/
|
|
void ackReceived(SIPRequest sipRequest) {
|
|
|
|
// Suppress retransmission of the final response
|
|
if (this.ackSeen)
|
|
return;
|
|
SIPServerTransaction tr = this.getInviteTransaction();
|
|
if (tr != null) {
|
|
if (tr.getCSeq() == sipRequest.getCSeq().getSeqNumber()) {
|
|
acquireTimerTaskSem();
|
|
try {
|
|
if (this.timerTask != null) {
|
|
this.timerTask.cancel();
|
|
this.timerTask = null;
|
|
}
|
|
} finally {
|
|
releaseTimerTaskSem();
|
|
}
|
|
this.ackSeen = true;
|
|
if (this.dialogDeleteTask != null) {
|
|
this.dialogDeleteTask.cancel();
|
|
this.dialogDeleteTask = null;
|
|
}
|
|
this.setLastAckReceived(sipRequest);
|
|
if (sipStack.isLoggingEnabled()) {
|
|
sipStack.getStackLogger().logDebug(
|
|
"ackReceived for " + ((SIPTransaction) tr).getMethod());
|
|
this.ackLine = sipStack.getStackLogger().getLineCount();
|
|
this.printDebugInfo();
|
|
}
|
|
if (this.isBackToBackUserAgent) {
|
|
this.releaseAckSem();
|
|
}
|
|
this.setState(CONFIRMED_STATE);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return true if a terminated event was delivered to the application as a result of the
|
|
* dialog termination.
|
|
*
|
|
*/
|
|
synchronized boolean testAndSetIsDialogTerminatedEventDelivered() {
|
|
boolean retval = this.dialogTerminatedEventDelivered;
|
|
this.dialogTerminatedEventDelivered = true;
|
|
return retval;
|
|
}
|
|
|
|
// /////////////////////////////////////////////////////////
|
|
// Public methods
|
|
// /////////////////////////////////////////////////////////
|
|
|
|
/**
|
|
* Adds a new event listener to this dialog.
|
|
*
|
|
* @param newListener
|
|
* Listener to add.
|
|
*/
|
|
public void addEventListener(SIPDialogEventListener newListener) {
|
|
eventListeners.add(newListener);
|
|
}
|
|
|
|
/**
|
|
* Removed an event listener from this dialog.
|
|
*
|
|
* @param oldListener
|
|
* Listener to remove.
|
|
*/
|
|
public void removeEventListener(SIPDialogEventListener oldListener) {
|
|
eventListeners.remove(oldListener);
|
|
}
|
|
|
|
/*
|
|
* @see javax.sip.Dialog#setApplicationData()
|
|
*/
|
|
public void setApplicationData(Object applicationData) {
|
|
this.applicationData = applicationData;
|
|
}
|
|
|
|
/*
|
|
* (non-Javadoc)
|
|
*
|
|
* @see javax.sip.Dialog#getApplicationData()
|
|
*/
|
|
public Object getApplicationData() {
|
|
return this.applicationData;
|
|
}
|
|
|
|
/**
|
|
* Updates the next consumable seqno.
|
|
*
|
|
*/
|
|
public synchronized void requestConsumed() {
|
|
this.nextSeqno = Long.valueOf(this.getRemoteSeqNumber() + 1);
|
|
|
|
if (sipStack.isLoggingEnabled()) {
|
|
this.sipStack.getStackLogger().logDebug(
|
|
"Request Consumed -- next consumable Request Seqno = " + this.nextSeqno);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Return true if this request can be consumed by the dialog.
|
|
*
|
|
* @param dialogRequest is the request to check with the dialog.
|
|
* @return true if the dialogRequest sequence number matches the next consumable seqno.
|
|
*/
|
|
public synchronized boolean isRequestConsumable(SIPRequest dialogRequest) {
|
|
// have not yet set remote seqno - this is a fresh
|
|
if (dialogRequest.getMethod().equals(Request.ACK))
|
|
throw new RuntimeException("Illegal method");
|
|
|
|
// For loose validation this function is delegated to the application
|
|
if (!this.isSequnceNumberValidation()) {
|
|
return true;
|
|
}
|
|
|
|
// JvB: Acceptable iff remoteCSeq < cseq. remoteCSeq==-1
|
|
// when not defined yet, so that works too
|
|
return remoteSequenceNumber < dialogRequest.getCSeq().getSeqNumber();
|
|
}
|
|
|
|
/**
|
|
* This method is called when a forked dialog is created from the client side. It starts a
|
|
* timer task. If the timer task expires before an ACK is sent then the dialog is cancelled
|
|
* (i.e. garbage collected ).
|
|
*
|
|
*/
|
|
public void doDeferredDelete() {
|
|
if (sipStack.getTimer() == null)
|
|
this.setState(TERMINATED_STATE);
|
|
else {
|
|
this.dialogDeleteTask = new DialogDeleteTask();
|
|
// Delete the transaction after the max ack timeout.
|
|
sipStack.getTimer().schedule(this.dialogDeleteTask,
|
|
SIPTransaction.TIMER_H * SIPTransactionStack.BASE_TIMER_INTERVAL);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Set the state for this dialog.
|
|
*
|
|
* @param state is the state to set for the dialog.
|
|
*/
|
|
|
|
public void setState(int state) {
|
|
if (sipStack.isLoggingEnabled()) {
|
|
sipStack.getStackLogger().logDebug(
|
|
"Setting dialog state for " + this + "newState = " + state);
|
|
sipStack.getStackLogger().logStackTrace();
|
|
if (state != NULL_STATE && state != this.dialogState)
|
|
if (sipStack.isLoggingEnabled()) {
|
|
sipStack.getStackLogger().logDebug(
|
|
this + " old dialog state is " + this.getState());
|
|
sipStack.getStackLogger().logDebug(
|
|
this + " New dialog state is " + DialogState.getObject(state));
|
|
}
|
|
|
|
}
|
|
this.dialogState = state;
|
|
// Dialog is in terminated state set it up for GC.
|
|
if (state == TERMINATED_STATE) {
|
|
if (sipStack.getTimer() != null) { // may be null after shutdown
|
|
sipStack.getTimer().schedule(new LingerTimer(), DIALOG_LINGER_TIME * 1000);
|
|
}
|
|
this.stopTimer();
|
|
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Debugging print for the dialog.
|
|
*/
|
|
public void printDebugInfo() {
|
|
if (sipStack.isLoggingEnabled()) {
|
|
sipStack.getStackLogger().logDebug("isServer = " + isServer());
|
|
sipStack.getStackLogger().logDebug("localTag = " + getLocalTag());
|
|
sipStack.getStackLogger().logDebug("remoteTag = " + getRemoteTag());
|
|
sipStack.getStackLogger().logDebug("localSequenceNumer = " + getLocalSeqNumber());
|
|
sipStack.getStackLogger().logDebug("remoteSequenceNumer = " + getRemoteSeqNumber());
|
|
sipStack.getStackLogger().logDebug("ackLine:" + this.getRemoteTag() + " " + ackLine);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return true if the dialog has already seen the ack.
|
|
*
|
|
* @return flag that records if the ack has been seen.
|
|
*/
|
|
public boolean isAckSeen() {
|
|
return this.ackSeen;
|
|
}
|
|
|
|
/**
|
|
* Get the last ACK for this transaction.
|
|
*/
|
|
public SIPRequest getLastAckSent() {
|
|
return this.lastAckSent;
|
|
}
|
|
|
|
/**
|
|
* Return true if ACK was sent ( for client tx ). For server tx, this is a NO-OP ( we dont
|
|
* send ACK).
|
|
*/
|
|
public boolean isAckSent(long cseqNo) {
|
|
if (this.getLastTransaction() == null)
|
|
return true;
|
|
if (this.getLastTransaction() instanceof ClientTransaction) {
|
|
if (this.getLastAckSent() == null) {
|
|
return false;
|
|
} else {
|
|
return cseqNo <=((SIPRequest) this.getLastAckSent()).getCSeq().getSeqNumber();
|
|
}
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the transaction that created this dialog.
|
|
*/
|
|
public Transaction getFirstTransaction() {
|
|
return this.firstTransaction;
|
|
}
|
|
|
|
|
|
/**
|
|
* Gets the route set for the dialog. When acting as an User Agent Server the route set MUST
|
|
* be set to the list of URIs in the Record-Route header field from the request, taken in
|
|
* order and preserving all URI parameters. When acting as an User Agent Client the route set
|
|
* MUST be set to the list of URIs in the Record-Route header field from the response, taken
|
|
* in reverse order and preserving all URI parameters. If no Record-Route header field is
|
|
* present in the request or response, the route set MUST be set to the empty set. This route
|
|
* set, even if empty, overrides any pre-existing route set for future requests in this
|
|
* dialog.
|
|
* <p>
|
|
* Requests within a dialog MAY contain Record-Route and Contact header fields. However, these
|
|
* requests do not cause the dialog's route set to be modified.
|
|
* <p>
|
|
* The User Agent Client uses the remote target and route set to build the Request-URI and
|
|
* Route header field of the request.
|
|
*
|
|
* @return an Iterator containing a list of route headers to be used for forwarding. Empty
|
|
* iterator is returned if route has not been established.
|
|
*/
|
|
public Iterator getRouteSet() {
|
|
if (this.routeList == null) {
|
|
return new LinkedList().listIterator();
|
|
} else {
|
|
return this.getRouteList().listIterator();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a Route list extracted from a SIPRequest to this Dialog.
|
|
*
|
|
* @param sipRequest
|
|
*/
|
|
public synchronized void addRoute(SIPRequest sipRequest) {
|
|
if (sipStack.isLoggingEnabled()) {
|
|
sipStack.getStackLogger().logDebug(
|
|
"setContact: dialogState: " + this + "state = " + this.getState());
|
|
}
|
|
|
|
if (this.dialogState == CONFIRMED_STATE
|
|
&& SIPRequest.isTargetRefresh(sipRequest.getMethod())) {
|
|
this.doTargetRefresh(sipRequest);
|
|
}
|
|
if (this.dialogState == CONFIRMED_STATE || this.dialogState == TERMINATED_STATE) {
|
|
return;
|
|
}
|
|
|
|
// Fix for issue #225: mustn't learn Route set from mid-dialog requests
|
|
if ( sipRequest.getToTag()!=null ) return;
|
|
|
|
// Incoming Request has the route list
|
|
RecordRouteList rrlist = sipRequest.getRecordRouteHeaders();
|
|
// Add the route set from the incoming response in reverse
|
|
// order
|
|
if (rrlist != null) {
|
|
this.addRoute(rrlist);
|
|
} else {
|
|
// Set the rotue list to the last seen route list.
|
|
this.routeList = new RouteList();
|
|
}
|
|
|
|
// put the contact header from the incoming request into
|
|
// the route set. JvB: some duplication here, ref. doTargetRefresh
|
|
ContactList contactList = sipRequest.getContactHeaders();
|
|
if (contactList != null) {
|
|
this.setRemoteTarget((ContactHeader) contactList.getFirst());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the dialog identifier.
|
|
*/
|
|
public void setDialogId(String dialogId) {
|
|
this.dialogId = dialogId;
|
|
}
|
|
|
|
/**
|
|
* Creates a new dialog based on a received NOTIFY. The dialog state is initialized
|
|
* appropriately. The NOTIFY differs in the From tag
|
|
*
|
|
* Made this a separate method to clearly distinguish what's happening here - this is a
|
|
* non-trivial case
|
|
*
|
|
* @param subscribeTx - the transaction started with the SUBSCRIBE that we sent
|
|
* @param notifyST - the ServerTransaction created for an incoming NOTIFY
|
|
* @return -- a new dialog created from the subscribe original SUBSCRIBE transaction.
|
|
*
|
|
*
|
|
*/
|
|
public static SIPDialog createFromNOTIFY(SIPClientTransaction subscribeTx,
|
|
SIPTransaction notifyST) {
|
|
SIPDialog d = new SIPDialog(notifyST);
|
|
//
|
|
// The above sets d.firstTransaction to NOTIFY (ST), correct that
|
|
//
|
|
d.serverTransactionFlag = false;
|
|
// they share this one
|
|
d.lastTransaction = subscribeTx;
|
|
storeFirstTransactionInfo(d, subscribeTx);
|
|
d.terminateOnBye = false;
|
|
d.localSequenceNumber = subscribeTx.getCSeq();
|
|
SIPRequest not = (SIPRequest) notifyST.getRequest();
|
|
d.remoteSequenceNumber = not.getCSeq().getSeqNumber();
|
|
d.setDialogId(not.getDialogId(true));
|
|
d.setLocalTag(not.getToTag());
|
|
d.setRemoteTag(not.getFromTag());
|
|
// to properly create the Dialog object.
|
|
// If not the stack will throw an exception when creating the response.
|
|
d.setLastResponse(subscribeTx, subscribeTx.getLastResponse());
|
|
|
|
// Dont use setLocal / setRemote here, they make other assumptions
|
|
d.localParty = not.getTo().getAddress();
|
|
d.remoteParty = not.getFrom().getAddress();
|
|
|
|
// initialize d's route set based on the NOTIFY. Any proxies must have
|
|
// Record-Routed
|
|
d.addRoute(not);
|
|
d.setState(CONFIRMED_STATE); // set state, *after* setting route set!
|
|
return d;
|
|
}
|
|
|
|
/**
|
|
* Return true if is server.
|
|
*
|
|
* @return true if is server transaction created this dialog.
|
|
*/
|
|
public boolean isServer() {
|
|
if (this.firstTransactionSeen == false)
|
|
return this.serverTransactionFlag;
|
|
else
|
|
return this.firstTransactionIsServerTransaction;
|
|
|
|
}
|
|
|
|
/**
|
|
* Return true if this is a re-establishment of the dialog.
|
|
*
|
|
* @return true if the reInvite flag is set.
|
|
*/
|
|
protected boolean isReInvite() {
|
|
return this.reInviteFlag;
|
|
}
|
|
|
|
/**
|
|
* Get the id for this dialog.
|
|
*
|
|
* @return the string identifier for this dialog.
|
|
*
|
|
*/
|
|
public String getDialogId() {
|
|
|
|
if (this.dialogId == null && this.lastResponse != null)
|
|
this.dialogId = this.lastResponse.getDialogId(isServer());
|
|
|
|
return this.dialogId;
|
|
}
|
|
|
|
private static void storeFirstTransactionInfo(SIPDialog dialog, SIPTransaction transaction) {
|
|
dialog.firstTransaction = transaction;
|
|
dialog.firstTransactionSeen = true;
|
|
dialog.firstTransactionIsServerTransaction = transaction.isServerTransaction();
|
|
dialog.firstTransactionSecure = transaction.getRequest().getRequestURI().getScheme()
|
|
.equalsIgnoreCase("sips");
|
|
dialog.firstTransactionPort = transaction.getPort();
|
|
dialog.firstTransactionId = transaction.getBranchId();
|
|
dialog.firstTransactionMethod = transaction.getMethod();
|
|
|
|
if (dialog.isServer()) {
|
|
SIPServerTransaction st = (SIPServerTransaction) transaction;
|
|
SIPResponse response = st.getLastResponse();
|
|
dialog.contactHeader = response != null ? response.getContactHeader() : null;
|
|
} else {
|
|
SIPClientTransaction ct = (SIPClientTransaction) transaction;
|
|
if (ct != null){
|
|
SIPRequest sipRequest = ct.getOriginalRequest();
|
|
dialog.contactHeader = sipRequest.getContactHeader();
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Add a transaction record to the dialog.
|
|
*
|
|
* @param transaction is the transaction to add to the dialog.
|
|
*/
|
|
public void addTransaction(SIPTransaction transaction) {
|
|
|
|
SIPRequest sipRequest = (SIPRequest) transaction.getOriginalRequest();
|
|
|
|
// Proessing a re-invite.
|
|
if (firstTransactionSeen && !firstTransactionId.equals(transaction.getBranchId())
|
|
&& transaction.getMethod().equals(firstTransactionMethod)) {
|
|
this.reInviteFlag = true;
|
|
}
|
|
|
|
if (firstTransactionSeen == false) {
|
|
// Record the local and remote sequenc
|
|
// numbers and the from and to tags for future
|
|
// use on this dialog.
|
|
storeFirstTransactionInfo(this, transaction);
|
|
if (sipRequest.getMethod().equals(Request.SUBSCRIBE))
|
|
this.eventHeader = (EventHeader) sipRequest.getHeader(EventHeader.NAME);
|
|
|
|
this.setLocalParty(sipRequest);
|
|
this.setRemoteParty(sipRequest);
|
|
this.setCallId(sipRequest);
|
|
if (this.originalRequest == null) {
|
|
this.originalRequest = sipRequest;
|
|
}
|
|
if (this.method == null) {
|
|
this.method = sipRequest.getMethod();
|
|
}
|
|
|
|
if (transaction instanceof SIPServerTransaction) {
|
|
this.hisTag = sipRequest.getFrom().getTag();
|
|
// My tag is assigned when sending response
|
|
} else {
|
|
setLocalSequenceNumber(sipRequest.getCSeq().getSeqNumber());
|
|
this.originalLocalSequenceNumber = localSequenceNumber;
|
|
this.myTag = sipRequest.getFrom().getTag();
|
|
if (myTag == null)
|
|
if (sipStack.isLoggingEnabled())
|
|
sipStack.getStackLogger().logError(
|
|
"The request's From header is missing the required Tag parameter.");
|
|
}
|
|
} else if (transaction.getMethod().equals(firstTransactionMethod)
|
|
&& firstTransactionIsServerTransaction != transaction.isServerTransaction()) {
|
|
// This case occurs when you are processing a re-invite.
|
|
// Switch from client side to server side for re-invite
|
|
// (put the other side on hold).
|
|
|
|
storeFirstTransactionInfo(this, transaction);
|
|
|
|
this.setLocalParty(sipRequest);
|
|
this.setRemoteParty(sipRequest);
|
|
this.setCallId(sipRequest);
|
|
this.originalRequest = sipRequest;
|
|
this.method = sipRequest.getMethod();
|
|
|
|
}
|
|
if (transaction instanceof SIPServerTransaction)
|
|
setRemoteSequenceNumber(sipRequest.getCSeq().getSeqNumber());
|
|
|
|
// If this is a server transaction record the remote
|
|
// sequence number to avoid re-processing of requests
|
|
// with the same sequence number directed towards this
|
|
// dialog.
|
|
|
|
this.lastTransaction = transaction;
|
|
// set a back ptr in the incoming dialog.
|
|
// CHECKME -- why is this here?
|
|
// transaction.setDialog(this,sipRequest);
|
|
if (sipStack.isLoggingEnabled()) {
|
|
sipStack.getStackLogger()
|
|
.logDebug("Transaction Added " + this + myTag + "/" + hisTag);
|
|
sipStack.getStackLogger().logDebug(
|
|
"TID = " + transaction.getTransactionId() + "/"
|
|
+ transaction.isServerTransaction());
|
|
sipStack.getStackLogger().logStackTrace();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the remote tag.
|
|
*
|
|
* @param hisTag is the remote tag to set.
|
|
*/
|
|
private void setRemoteTag(String hisTag) {
|
|
if (sipStack.isLoggingEnabled()) {
|
|
sipStack.getStackLogger().logDebug(
|
|
"setRemoteTag(): " + this + " remoteTag = " + this.hisTag + " new tag = "
|
|
+ hisTag);
|
|
}
|
|
if (this.hisTag != null && hisTag != null && !hisTag.equals(this.hisTag)) {
|
|
if (this.getState() != DialogState.EARLY) {
|
|
if (sipStack.isLoggingEnabled())
|
|
sipStack.getStackLogger().logDebug(
|
|
"Dialog is already established -- ignoring remote tag re-assignment");
|
|
return;
|
|
} else if (sipStack.isRemoteTagReassignmentAllowed()) {
|
|
if (sipStack.isLoggingEnabled())
|
|
sipStack.getStackLogger().logDebug(
|
|
"UNSAFE OPERATION ! tag re-assignment " + this.hisTag
|
|
+ " trying to set to " + hisTag
|
|
+ " can cause unexpected effects ");
|
|
boolean removed = false;
|
|
if (this.sipStack.getDialog(dialogId) == this) {
|
|
this.sipStack.removeDialog(dialogId);
|
|
removed = true;
|
|
|
|
}
|
|
// Force recomputation of Dialog ID;
|
|
this.dialogId = null;
|
|
this.hisTag = hisTag;
|
|
if (removed) {
|
|
if (sipStack.isLoggingEnabled())
|
|
sipStack.getStackLogger().logDebug("ReInserting Dialog");
|
|
this.sipStack.putDialog(this);
|
|
}
|
|
}
|
|
} else {
|
|
if (hisTag != null) {
|
|
this.hisTag = hisTag;
|
|
} else {
|
|
if (sipStack.isLoggingEnabled())
|
|
sipStack.getStackLogger().logWarning("setRemoteTag : called with null argument ");
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the last transaction from the dialog.
|
|
*/
|
|
public SIPTransaction getLastTransaction() {
|
|
return this.lastTransaction;
|
|
}
|
|
|
|
/**
|
|
* Get the INVITE transaction (null if no invite transaction).
|
|
*/
|
|
public SIPServerTransaction getInviteTransaction() {
|
|
DialogTimerTask t = this.timerTask;
|
|
if (t != null)
|
|
return t.transaction;
|
|
else
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Set the local sequece number for the dialog (defaults to 1 when the dialog is created).
|
|
*
|
|
* @param lCseq is the local cseq number.
|
|
*
|
|
*/
|
|
private void setLocalSequenceNumber(long lCseq) {
|
|
if (sipStack.isLoggingEnabled())
|
|
sipStack.getStackLogger().logDebug(
|
|
"setLocalSequenceNumber: original " + this.localSequenceNumber + " new = "
|
|
+ lCseq);
|
|
if (lCseq <= this.localSequenceNumber)
|
|
throw new RuntimeException("Sequence number should not decrease !");
|
|
this.localSequenceNumber = lCseq;
|
|
}
|
|
|
|
/**
|
|
* Set the remote sequence number for the dialog.
|
|
*
|
|
* @param rCseq is the remote cseq number.
|
|
*
|
|
*/
|
|
public void setRemoteSequenceNumber(long rCseq) {
|
|
if (sipStack.isLoggingEnabled())
|
|
sipStack.getStackLogger().logDebug("setRemoteSeqno " + this + "/" + rCseq);
|
|
this.remoteSequenceNumber = rCseq;
|
|
}
|
|
|
|
/**
|
|
* Increment the local CSeq # for the dialog. This is useful for if you want to create a hole
|
|
* in the sequence number i.e. route a request outside the dialog and then resume within the
|
|
* dialog.
|
|
*/
|
|
public void incrementLocalSequenceNumber() {
|
|
++this.localSequenceNumber;
|
|
}
|
|
|
|
/**
|
|
* Get the remote sequence number (for cseq assignment of outgoing requests within this
|
|
* dialog).
|
|
*
|
|
* @deprecated
|
|
* @return local sequence number.
|
|
*/
|
|
|
|
public int getRemoteSequenceNumber() {
|
|
return (int) this.remoteSequenceNumber;
|
|
}
|
|
|
|
/**
|
|
* Get the local sequence number (for cseq assignment of outgoing requests within this
|
|
* dialog).
|
|
*
|
|
* @deprecated
|
|
* @return local sequence number.
|
|
*/
|
|
|
|
public int getLocalSequenceNumber() {
|
|
return (int) this.localSequenceNumber;
|
|
}
|
|
|
|
/**
|
|
* Get the sequence number for the request that origianlly created the Dialog.
|
|
*
|
|
* @return -- the original starting sequence number for this dialog.
|
|
*/
|
|
public long getOriginalLocalSequenceNumber() {
|
|
return this.originalLocalSequenceNumber;
|
|
}
|
|
|
|
/*
|
|
* (non-Javadoc)
|
|
*
|
|
* @see javax.sip.Dialog#getLocalSequenceNumberLong()
|
|
*/
|
|
public long getLocalSeqNumber() {
|
|
return this.localSequenceNumber;
|
|
}
|
|
|
|
/*
|
|
* (non-Javadoc)
|
|
*
|
|
* @see javax.sip.Dialog#getRemoteSequenceNumberLong()
|
|
*/
|
|
public long getRemoteSeqNumber() {
|
|
return this.remoteSequenceNumber;
|
|
}
|
|
|
|
/*
|
|
* (non-Javadoc)
|
|
*
|
|
* @see javax.sip.Dialog#getLocalTag()
|
|
*/
|
|
public String getLocalTag() {
|
|
return this.myTag;
|
|
}
|
|
|
|
/*
|
|
* (non-Javadoc)
|
|
*
|
|
* @see javax.sip.Dialog#getRemoteTag()
|
|
*/
|
|
public String getRemoteTag() {
|
|
|
|
return hisTag;
|
|
}
|
|
|
|
/**
|
|
* Set local tag for the transaction.
|
|
*
|
|
* @param mytag is the tag to use in From headers client transactions that belong to this
|
|
* dialog and for generating To tags for Server transaction requests that belong to
|
|
* this dialog.
|
|
*/
|
|
private void setLocalTag(String mytag) {
|
|
if (sipStack.isLoggingEnabled()) {
|
|
sipStack.getStackLogger().logDebug("set Local tag " + mytag + " " + this.dialogId);
|
|
sipStack.getStackLogger().logStackTrace();
|
|
}
|
|
|
|
this.myTag = mytag;
|
|
|
|
}
|
|
|
|
/*
|
|
* (non-Javadoc)
|
|
*
|
|
* @see javax.sip.Dialog#delete()
|
|
*/
|
|
|
|
public void delete() {
|
|
// the reaper will get him later.
|
|
this.setState(TERMINATED_STATE);
|
|
}
|
|
|
|
/*
|
|
* (non-Javadoc)
|
|
*
|
|
* @see javax.sip.Dialog#getCallId()
|
|
*/
|
|
public CallIdHeader getCallId() {
|
|
return this.callIdHeader;
|
|
}
|
|
|
|
/**
|
|
* set the call id header for this dialog.
|
|
*/
|
|
private void setCallId(SIPRequest sipRequest) {
|
|
this.callIdHeader = sipRequest.getCallId();
|
|
}
|
|
|
|
/*
|
|
* (non-Javadoc)
|
|
*
|
|
* @see javax.sip.Dialog#getLocalParty()
|
|
*/
|
|
|
|
public javax.sip.address.Address getLocalParty() {
|
|
return this.localParty;
|
|
}
|
|
|
|
private void setLocalParty(SIPMessage sipMessage) {
|
|
if (!isServer()) {
|
|
this.localParty = sipMessage.getFrom().getAddress();
|
|
} else {
|
|
this.localParty = sipMessage.getTo().getAddress();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the Address identifying the remote party. This is the value of the To header of
|
|
* locally initiated requests in this dialogue when acting as an User Agent Client.
|
|
* <p>
|
|
* This is the value of the From header of recieved responses in this dialogue when acting as
|
|
* an User Agent Server.
|
|
*
|
|
* @return the address object of the remote party.
|
|
*/
|
|
public javax.sip.address.Address getRemoteParty() {
|
|
|
|
if (sipStack.isLoggingEnabled()) {
|
|
sipStack.getStackLogger().logDebug("gettingRemoteParty " + this.remoteParty);
|
|
}
|
|
return this.remoteParty;
|
|
|
|
}
|
|
|
|
/*
|
|
* (non-Javadoc)
|
|
*
|
|
* @see javax.sip.Dialog#getRemoteTarget()
|
|
*/
|
|
public javax.sip.address.Address getRemoteTarget() {
|
|
|
|
return this.remoteTarget;
|
|
}
|
|
|
|
/*
|
|
* (non-Javadoc)
|
|
*
|
|
* @see javax.sip.Dialog#getState()
|
|
*/
|
|
public DialogState getState() {
|
|
if (this.dialogState == NULL_STATE)
|
|
return null; // not yet initialized
|
|
return DialogState.getObject(this.dialogState);
|
|
}
|
|
|
|
/**
|
|
* Returns true if this Dialog is secure i.e. if the request arrived over TLS, and the
|
|
* Request-URI contained a SIPS URI, the "secure" flag is set to TRUE.
|
|
*
|
|
* @return <code>true</code> if this dialogue was established using a sips URI over TLS, and
|
|
* <code>false</code> otherwise.
|
|
*/
|
|
public boolean isSecure() {
|
|
return this.firstTransactionSecure;
|
|
}
|
|
|
|
/*
|
|
* (non-Javadoc)
|
|
*
|
|
* @see javax.sip.Dialog#sendAck(javax.sip.message.Request)
|
|
*/
|
|
public void sendAck(Request request) throws SipException {
|
|
this.sendAck(request, true);
|
|
}
|
|
|
|
/*
|
|
* (non-Javadoc)
|
|
*
|
|
* @see javax.sip.Dialog#createRequest(java.lang.String)
|
|
*/
|
|
public Request createRequest(String method) throws SipException {
|
|
|
|
if (method.equals(Request.ACK) || method.equals(Request.PRACK)) {
|
|
throw new SipException("Invalid method specified for createRequest:" + method);
|
|
}
|
|
if (lastResponse != null)
|
|
return this.createRequest(method, this.lastResponse);
|
|
else
|
|
throw new SipException("Dialog not yet established -- no response!");
|
|
}
|
|
|
|
/**
|
|
* The method that actually does the work of creating a request.
|
|
*
|
|
* @param method
|
|
* @param response
|
|
* @return
|
|
* @throws SipException
|
|
*/
|
|
private Request createRequest(String method, SIPResponse sipResponse) throws SipException {
|
|
/*
|
|
* Check if the dialog is in the right state (RFC 3261 section 15). The caller's UA MAY
|
|
* send a BYE for either CONFIRMED or EARLY dialogs, and the callee's UA MAY send a BYE on
|
|
* CONFIRMED dialogs, but MUST NOT send a BYE on EARLY dialogs.
|
|
*
|
|
* Throw out cancel request.
|
|
*/
|
|
|
|
if (method == null || sipResponse == null)
|
|
throw new NullPointerException("null argument");
|
|
|
|
if (method.equals(Request.CANCEL))
|
|
throw new SipException("Dialog.createRequest(): Invalid request");
|
|
|
|
if (this.getState() == null
|
|
|| (this.getState().getValue() == TERMINATED_STATE && !method
|
|
.equalsIgnoreCase(Request.BYE))
|
|
|| (this.isServer() && this.getState().getValue() == EARLY_STATE && method
|
|
.equalsIgnoreCase(Request.BYE)))
|
|
throw new SipException("Dialog " + getDialogId()
|
|
+ " not yet established or terminated " + this.getState());
|
|
|
|
SipUri sipUri = null;
|
|
if (this.getRemoteTarget() != null)
|
|
sipUri = (SipUri) this.getRemoteTarget().getURI().clone();
|
|
else {
|
|
sipUri = (SipUri) this.getRemoteParty().getURI().clone();
|
|
sipUri.clearUriParms();
|
|
}
|
|
|
|
CSeq cseq = new CSeq();
|
|
try {
|
|
cseq.setMethod(method);
|
|
cseq.setSeqNumber(this.getLocalSeqNumber());
|
|
} catch (Exception ex) {
|
|
if (sipStack.isLoggingEnabled())
|
|
sipStack.getStackLogger().logError("Unexpected error");
|
|
InternalErrorHandler.handleException(ex);
|
|
}
|
|
/*
|
|
* Add a via header for the outbound request based on the transport of the message
|
|
* processor.
|
|
*/
|
|
|
|
ListeningPointImpl lp = (ListeningPointImpl) this.sipProvider
|
|
.getListeningPoint(sipResponse.getTopmostVia().getTransport());
|
|
if (lp == null) {
|
|
if (sipStack.isLoggingEnabled())
|
|
sipStack.getStackLogger().logError(
|
|
"Cannot find listening point for transport "
|
|
+ sipResponse.getTopmostVia().getTransport());
|
|
throw new SipException("Cannot find listening point for transport "
|
|
+ sipResponse.getTopmostVia().getTransport());
|
|
}
|
|
Via via = lp.getViaHeader();
|
|
|
|
From from = new From();
|
|
from.setAddress(this.localParty);
|
|
To to = new To();
|
|
to.setAddress(this.remoteParty);
|
|
SIPRequest sipRequest = sipResponse.createRequest(sipUri, via, cseq, from, to);
|
|
|
|
/*
|
|
* The default contact header is obtained from the provider. The application can override
|
|
* this.
|
|
*
|
|
* JvB: Should only do this for target refresh requests, ie not for BYE, PRACK, etc
|
|
*/
|
|
|
|
if (SIPRequest.isTargetRefresh(method)) {
|
|
ContactHeader contactHeader = ((ListeningPointImpl) this.sipProvider
|
|
.getListeningPoint(lp.getTransport())).createContactHeader();
|
|
|
|
((SipURI) contactHeader.getAddress().getURI()).setSecure(this.isSecure());
|
|
sipRequest.setHeader(contactHeader);
|
|
}
|
|
|
|
try {
|
|
/*
|
|
* Guess of local sequence number - this is being re-set when the request is actually
|
|
* dispatched
|
|
*/
|
|
cseq = (CSeq) sipRequest.getCSeq();
|
|
cseq.setSeqNumber(this.localSequenceNumber + 1);
|
|
|
|
} catch (InvalidArgumentException ex) {
|
|
InternalErrorHandler.handleException(ex);
|
|
}
|
|
|
|
if (method.equals(Request.SUBSCRIBE)) {
|
|
|
|
if (eventHeader != null)
|
|
sipRequest.addHeader(eventHeader);
|
|
|
|
}
|
|
|
|
/*
|
|
* RFC3261, section 12.2.1.1:
|
|
*
|
|
* The URI in the To field of the request MUST be set to the remote URI from the dialog
|
|
* state. The tag in the To header field of the request MUST be set to the remote tag of
|
|
* the dialog ID. The From URI of the request MUST be set to the local URI from the dialog
|
|
* state. The tag in the From header field of the request MUST be set to the local tag of
|
|
* the dialog ID. If the value of the remote or local tags is null, the tag parameter MUST
|
|
* be omitted from the To or From header fields, respectively.
|
|
*/
|
|
|
|
try {
|
|
if (this.getLocalTag() != null) {
|
|
from.setTag(this.getLocalTag());
|
|
} else {
|
|
from.removeTag();
|
|
}
|
|
if (this.getRemoteTag() != null) {
|
|
to.setTag(this.getRemoteTag());
|
|
} else {
|
|
to.removeTag();
|
|
}
|
|
} catch (ParseException ex) {
|
|
InternalErrorHandler.handleException(ex);
|
|
}
|
|
|
|
// get the route list from the dialog.
|
|
this.updateRequest(sipRequest);
|
|
|
|
return sipRequest;
|
|
|
|
}
|
|
|
|
/*
|
|
* (non-Javadoc)
|
|
*
|
|
* @see javax.sip.Dialog#sendRequest(javax.sip.ClientTransaction)
|
|
*/
|
|
|
|
public void sendRequest(ClientTransaction clientTransactionId)
|
|
throws TransactionDoesNotExistException, SipException {
|
|
this.sendRequest(clientTransactionId, !this.isBackToBackUserAgent);
|
|
}
|
|
|
|
public void sendRequest(ClientTransaction clientTransactionId, boolean allowInterleaving)
|
|
throws TransactionDoesNotExistException, SipException {
|
|
|
|
if ( (!allowInterleaving)
|
|
&& clientTransactionId.getRequest().getMethod().equals(Request.INVITE)) {
|
|
new Thread((new ReInviteSender(clientTransactionId))).start();
|
|
return;
|
|
}
|
|
|
|
SIPRequest dialogRequest = ((SIPClientTransaction) clientTransactionId)
|
|
.getOriginalRequest();
|
|
|
|
if (sipStack.isLoggingEnabled())
|
|
sipStack.getStackLogger().logDebug(
|
|
"dialog.sendRequest " + " dialog = " + this + "\ndialogRequest = \n"
|
|
+ dialogRequest);
|
|
|
|
if (clientTransactionId == null)
|
|
throw new NullPointerException("null parameter");
|
|
|
|
if (dialogRequest.getMethod().equals(Request.ACK)
|
|
|| dialogRequest.getMethod().equals(Request.CANCEL))
|
|
throw new SipException("Bad Request Method. " + dialogRequest.getMethod());
|
|
|
|
// JvB: added, allow re-sending of BYE after challenge
|
|
if (byeSent && isTerminatedOnBye() && !dialogRequest.getMethod().equals(Request.BYE)) {
|
|
if (sipStack.isLoggingEnabled())
|
|
sipStack.getStackLogger().logError("BYE already sent for " + this);
|
|
throw new SipException("Cannot send request; BYE already sent");
|
|
}
|
|
|
|
if (dialogRequest.getTopmostVia() == null) {
|
|
Via via = ((SIPClientTransaction) clientTransactionId).getOutgoingViaHeader();
|
|
dialogRequest.addHeader(via);
|
|
}
|
|
if (!this.getCallId().getCallId().equalsIgnoreCase(dialogRequest.getCallId().getCallId())) {
|
|
|
|
if (sipStack.isLoggingEnabled()) {
|
|
sipStack.getStackLogger().logError("CallID " + this.getCallId());
|
|
sipStack.getStackLogger().logError(
|
|
"RequestCallID = " + dialogRequest.getCallId().getCallId());
|
|
sipStack.getStackLogger().logError("dialog = " + this);
|
|
}
|
|
throw new SipException("Bad call ID in request");
|
|
}
|
|
|
|
// Set the dialog back pointer.
|
|
((SIPClientTransaction) clientTransactionId).setDialog(this, this.dialogId);
|
|
|
|
this.addTransaction((SIPTransaction) clientTransactionId);
|
|
// Enable the retransmission filter for the transaction
|
|
|
|
((SIPClientTransaction) clientTransactionId).isMapped = true;
|
|
|
|
From from = (From) dialogRequest.getFrom();
|
|
To to = (To) dialogRequest.getTo();
|
|
|
|
// Caller already did the tag assignment -- check to see if the
|
|
// tag assignment is OK.
|
|
if (this.getLocalTag() != null && from.getTag() != null
|
|
&& !from.getTag().equals(this.getLocalTag()))
|
|
throw new SipException("From tag mismatch expecting " + this.getLocalTag());
|
|
|
|
if (this.getRemoteTag() != null && to.getTag() != null
|
|
&& !to.getTag().equals(this.getRemoteTag())) {
|
|
if (sipStack.isLoggingEnabled())
|
|
this.sipStack.getStackLogger().logWarning(
|
|
"To header tag mismatch expecting " + this.getRemoteTag());
|
|
}
|
|
/*
|
|
* The application is sending a NOTIFY before sending the response of the dialog.
|
|
*/
|
|
if (this.getLocalTag() == null && dialogRequest.getMethod().equals(Request.NOTIFY)) {
|
|
if (!this.getMethod().equals(Request.SUBSCRIBE))
|
|
throw new SipException("Trying to send NOTIFY without SUBSCRIBE Dialog!");
|
|
this.setLocalTag(from.getTag());
|
|
|
|
}
|
|
|
|
try {
|
|
if (this.getLocalTag() != null)
|
|
from.setTag(this.getLocalTag());
|
|
if (this.getRemoteTag() != null)
|
|
to.setTag(this.getRemoteTag());
|
|
|
|
} catch (ParseException ex) {
|
|
|
|
InternalErrorHandler.handleException(ex);
|
|
|
|
}
|
|
|
|
Hop hop = ((SIPClientTransaction) clientTransactionId).getNextHop();
|
|
if (sipStack.isLoggingEnabled()) {
|
|
sipStack.getStackLogger().logDebug(
|
|
"Using hop = " + hop.getHost() + " : " + hop.getPort());
|
|
}
|
|
|
|
try {
|
|
MessageChannel messageChannel = sipStack.createRawMessageChannel(this
|
|
.getSipProvider().getListeningPoint(hop.getTransport()).getIPAddress(),
|
|
this.firstTransactionPort, hop);
|
|
|
|
MessageChannel oldChannel = ((SIPClientTransaction)
|
|
clientTransactionId).getMessageChannel();
|
|
|
|
// Remove this from the connection cache if it is in the
|
|
// connection
|
|
// cache and is not yet active.
|
|
oldChannel.uncache();
|
|
|
|
// Not configured to cache client connections.
|
|
if (!sipStack.cacheClientConnections) {
|
|
oldChannel.useCount--;
|
|
if (sipStack.isLoggingEnabled())
|
|
sipStack.getStackLogger().logDebug(
|
|
"oldChannel: useCount " + oldChannel.useCount);
|
|
|
|
}
|
|
|
|
if (messageChannel == null) {
|
|
/*
|
|
* At this point the procedures of 8.1.2 and 12.2.1.1 of RFC3261 have been tried
|
|
* but the resulting next hop cannot be resolved (recall that the exception thrown
|
|
* is caught and ignored in SIPStack.createMessageChannel() so we end up here with
|
|
* a null messageChannel instead of the exception handler below). All else
|
|
* failing, try the outbound proxy in accordance with 8.1.2, in particular: This
|
|
* ensures that outbound proxies that do not add Record-Route header field values
|
|
* will drop out of the path of subsequent requests. It allows endpoints that
|
|
* cannot resolve the first Route URI to delegate that task to an outbound proxy.
|
|
*
|
|
* if one considers the 'first Route URI' of a request constructed according to
|
|
* 12.2.1.1 to be the request URI when the route set is empty.
|
|
*/
|
|
if (sipStack.isLoggingEnabled())
|
|
sipStack.getStackLogger().logDebug(
|
|
"Null message channel using outbound proxy !");
|
|
Hop outboundProxy = sipStack.getRouter(dialogRequest).getOutboundProxy();
|
|
if (outboundProxy == null)
|
|
throw new SipException("No route found! hop=" + hop);
|
|
messageChannel = sipStack.createRawMessageChannel(this.getSipProvider()
|
|
.getListeningPoint(outboundProxy.getTransport()).getIPAddress(),
|
|
this.firstTransactionPort, outboundProxy);
|
|
if (messageChannel != null)
|
|
((SIPClientTransaction) clientTransactionId)
|
|
.setEncapsulatedChannel(messageChannel);
|
|
} else {
|
|
((SIPClientTransaction) clientTransactionId)
|
|
.setEncapsulatedChannel(messageChannel);
|
|
|
|
if (sipStack.isLoggingEnabled()) {
|
|
sipStack.getStackLogger().logDebug("using message channel " + messageChannel);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (messageChannel != null) messageChannel.useCount++;
|
|
|
|
// See if we need to release the previously mapped channel.
|
|
if ((!sipStack.cacheClientConnections) && oldChannel != null
|
|
&& oldChannel.useCount <= 0)
|
|
oldChannel.close();
|
|
} catch (Exception ex) {
|
|
if (sipStack.isLoggingEnabled())
|
|
sipStack.getStackLogger().logException(ex);
|
|
throw new SipException("Could not create message channel", ex);
|
|
}
|
|
|
|
try {
|
|
// Increment before setting!!
|
|
localSequenceNumber++;
|
|
dialogRequest.getCSeq().setSeqNumber(getLocalSeqNumber());
|
|
} catch (InvalidArgumentException ex) {
|
|
sipStack.getStackLogger().logFatalError(ex.getMessage());
|
|
}
|
|
|
|
try {
|
|
((SIPClientTransaction) clientTransactionId).sendMessage(dialogRequest);
|
|
/*
|
|
* Note that if the BYE is rejected then the Dialog should bo back to the ESTABLISHED
|
|
* state so we only set state after successful send.
|
|
*/
|
|
if (dialogRequest.getMethod().equals(Request.BYE)) {
|
|
this.byeSent = true;
|
|
/*
|
|
* Dialog goes into TERMINATED state as soon as BYE is sent. ISSUE 182.
|
|
*/
|
|
if (isTerminatedOnBye()) {
|
|
this.setState(DialogState._TERMINATED);
|
|
}
|
|
}
|
|
} catch (IOException ex) {
|
|
throw new SipException("error sending message", ex);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Return yes if the last response is to be retransmitted.
|
|
*/
|
|
private boolean toRetransmitFinalResponse(int T2) {
|
|
if (--retransmissionTicksLeft == 0) {
|
|
if (2 * prevRetransmissionTicks <= T2)
|
|
this.retransmissionTicksLeft = 2 * prevRetransmissionTicks;
|
|
else
|
|
this.retransmissionTicksLeft = prevRetransmissionTicks;
|
|
this.prevRetransmissionTicks = retransmissionTicksLeft;
|
|
return true;
|
|
} else
|
|
return false;
|
|
|
|
}
|
|
|
|
protected void setRetransmissionTicks() {
|
|
this.retransmissionTicksLeft = 1;
|
|
this.prevRetransmissionTicks = 1;
|
|
}
|
|
|
|
/**
|
|
* Resend the last ack.
|
|
*/
|
|
public void resendAck() throws SipException {
|
|
// Check for null.
|
|
|
|
if (this.getLastAckSent() != null) {
|
|
if (getLastAckSent().getHeader(TimeStampHeader.NAME) != null
|
|
&& sipStack.generateTimeStampHeader) {
|
|
TimeStamp ts = new TimeStamp();
|
|
try {
|
|
ts.setTimeStamp(System.currentTimeMillis());
|
|
getLastAckSent().setHeader(ts);
|
|
} catch (InvalidArgumentException e) {
|
|
|
|
}
|
|
}
|
|
this.sendAck(getLastAckSent(), false);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Get the method of the request/response that resulted in the creation of the Dialog.
|
|
*
|
|
* @return -- the method of the dialog.
|
|
*/
|
|
public String getMethod() {
|
|
// Method of the request or response used to create this dialog
|
|
return this.method;
|
|
}
|
|
|
|
/**
|
|
* Start the dialog timer.
|
|
*
|
|
* @param transaction
|
|
*/
|
|
|
|
protected void startTimer(SIPServerTransaction transaction) {
|
|
if (this.timerTask != null && timerTask.transaction == transaction) {
|
|
if (sipStack.isLoggingEnabled())
|
|
sipStack.getStackLogger().logDebug("Timer already running for " + getDialogId());
|
|
return;
|
|
}
|
|
if (sipStack.isLoggingEnabled())
|
|
sipStack.getStackLogger().logDebug("Starting dialog timer for " + getDialogId());
|
|
this.ackSeen = false;
|
|
|
|
acquireTimerTaskSem();
|
|
try {
|
|
if (this.timerTask != null) {
|
|
this.timerTask.transaction = transaction;
|
|
} else {
|
|
this.timerTask = new DialogTimerTask(transaction);
|
|
sipStack.getTimer().schedule(timerTask, SIPTransactionStack.BASE_TIMER_INTERVAL,
|
|
SIPTransactionStack.BASE_TIMER_INTERVAL);
|
|
}
|
|
} finally {
|
|
releaseTimerTaskSem();
|
|
}
|
|
|
|
this.setRetransmissionTicks();
|
|
}
|
|
|
|
/**
|
|
* Stop the dialog timer. This is called when the dialog is terminated.
|
|
*
|
|
*/
|
|
protected void stopTimer() {
|
|
try {
|
|
acquireTimerTaskSem();
|
|
try {
|
|
if (this.timerTask != null) {
|
|
this.timerTask.cancel();
|
|
this.timerTask = null;
|
|
}
|
|
} finally {
|
|
releaseTimerTaskSem();
|
|
}
|
|
} catch (Exception ex) {
|
|
}
|
|
}
|
|
|
|
/*
|
|
* (non-Javadoc) Retransmissions of the reliable provisional response cease when a matching
|
|
* PRACK is received by the UA core. PRACK is like any other request within a dialog, and the
|
|
* UAS core processes it according to the procedures of Sections 8.2 and 12.2.2 of RFC 3261. A
|
|
* matching PRACK is defined as one within the same dialog as the response, and whose method,
|
|
* CSeq-num, and response-num in the RAck header field match, respectively, the method from
|
|
* the CSeq, the sequence number from the CSeq, and the sequence number from the RSeq of the
|
|
* reliable provisional response.
|
|
*
|
|
* @see javax.sip.Dialog#createPrack(javax.sip.message.Response)
|
|
*/
|
|
public Request createPrack(Response relResponse) throws DialogDoesNotExistException,
|
|
SipException {
|
|
|
|
if (this.getState() == null || this.getState().equals(DialogState.TERMINATED))
|
|
throw new DialogDoesNotExistException("Dialog not initialized or terminated");
|
|
|
|
if ((RSeq) relResponse.getHeader(RSeqHeader.NAME) == null) {
|
|
throw new SipException("Missing RSeq Header");
|
|
}
|
|
|
|
try {
|
|
SIPResponse sipResponse = (SIPResponse) relResponse;
|
|
SIPRequest sipRequest = (SIPRequest) this.createRequest(Request.PRACK,
|
|
(SIPResponse) relResponse);
|
|
String toHeaderTag = sipResponse.getTo().getTag();
|
|
sipRequest.setToTag(toHeaderTag);
|
|
RAck rack = new RAck();
|
|
RSeq rseq = (RSeq) relResponse.getHeader(RSeqHeader.NAME);
|
|
rack.setMethod(sipResponse.getCSeq().getMethod());
|
|
rack.setCSequenceNumber((int) sipResponse.getCSeq().getSeqNumber());
|
|
rack.setRSequenceNumber(rseq.getSeqNumber());
|
|
sipRequest.setHeader(rack);
|
|
return (Request) sipRequest;
|
|
} catch (Exception ex) {
|
|
InternalErrorHandler.handleException(ex);
|
|
return null;
|
|
}
|
|
|
|
}
|
|
|
|
private void updateRequest(SIPRequest sipRequest) {
|
|
|
|
RouteList rl = this.getRouteList();
|
|
if (rl.size() > 0) {
|
|
sipRequest.setHeader(rl);
|
|
} else {
|
|
sipRequest.removeHeader(RouteHeader.NAME);
|
|
}
|
|
if (MessageFactoryImpl.getDefaultUserAgentHeader() != null) {
|
|
sipRequest.setHeader(MessageFactoryImpl.getDefaultUserAgentHeader());
|
|
}
|
|
|
|
}
|
|
|
|
/*
|
|
* (non-Javadoc) The UAC core MUST generate an ACK request for each 2xx received from the
|
|
* transaction layer. The header fields of the ACK are constructed in the same way as for any
|
|
* request sent within a dialog (see Section 12) with the exception of the CSeq and the header
|
|
* fields related to authentication. The sequence number of the CSeq header field MUST be the
|
|
* same as the INVITE being acknowledged, but the CSeq method MUST be ACK. The ACK MUST
|
|
* contain the same credentials as the INVITE. If the 2xx contains an offer (based on the
|
|
* rules above), the ACK MUST carry an answer in its body. If the offer in the 2xx response is
|
|
* not acceptable, the UAC core MUST generate a valid answer in the ACK and then send a BYE
|
|
* immediately.
|
|
*
|
|
* Note that for the case of forked requests, you can create multiple outgoing invites each
|
|
* with a different cseq and hence you need to supply the invite.
|
|
*
|
|
* @see javax.sip.Dialog#createAck(long)
|
|
*/
|
|
public Request createAck(long cseqno) throws InvalidArgumentException, SipException {
|
|
|
|
// JvB: strictly speaking it is allowed to start a dialog with
|
|
// SUBSCRIBE,
|
|
// then send INVITE+ACK later on
|
|
if (!method.equals(Request.INVITE))
|
|
throw new SipException("Dialog was not created with an INVITE" + method);
|
|
|
|
if (cseqno <= 0)
|
|
throw new InvalidArgumentException("bad cseq <= 0 ");
|
|
else if (cseqno > ((((long) 1) << 32) - 1))
|
|
throw new InvalidArgumentException("bad cseq > " + ((((long) 1) << 32) - 1));
|
|
|
|
if (this.remoteTarget == null) {
|
|
throw new SipException("Cannot create ACK - no remote Target!");
|
|
}
|
|
|
|
if (this.sipStack.isLoggingEnabled()) {
|
|
this.sipStack.getStackLogger().logDebug("createAck " + this + " cseqno " + cseqno);
|
|
}
|
|
|
|
// MUST ack in the same order that the OKs were received. This traps
|
|
// out of order ACK sending. Old ACKs seqno's can always be ACKed.
|
|
if (lastInviteOkReceived < cseqno) {
|
|
if (sipStack.isLoggingEnabled()) {
|
|
this.sipStack.getStackLogger().logDebug(
|
|
"WARNING : Attempt to crete ACK without OK " + this);
|
|
this.sipStack.getStackLogger().logDebug("LAST RESPONSE = " + this.lastResponse);
|
|
}
|
|
throw new SipException("Dialog not yet established -- no OK response!");
|
|
}
|
|
|
|
try {
|
|
|
|
// JvB: Transport from first entry in route set, or remote Contact
|
|
// if none
|
|
// Only used to find correct LP & create correct Via
|
|
SipURI uri4transport = null;
|
|
|
|
if (this.routeList != null && !this.routeList.isEmpty()) {
|
|
Route r = (Route) this.routeList.getFirst();
|
|
uri4transport = ((SipURI) r.getAddress().getURI());
|
|
} else { // should be !=null, checked above
|
|
uri4transport = ((SipURI) this.remoteTarget.getURI());
|
|
}
|
|
|
|
String transport = uri4transport.getTransportParam();
|
|
if (transport == null) {
|
|
// JvB fix: also support TLS
|
|
transport = uri4transport.isSecure() ? ListeningPoint.TLS : ListeningPoint.UDP;
|
|
}
|
|
ListeningPointImpl lp = (ListeningPointImpl) sipProvider.getListeningPoint(transport);
|
|
if (lp == null) {
|
|
if (sipStack.isLoggingEnabled()) {
|
|
sipStack.getStackLogger().logError(
|
|
"remoteTargetURI " + this.remoteTarget.getURI());
|
|
sipStack.getStackLogger().logError("uri4transport = " + uri4transport);
|
|
sipStack.getStackLogger().logError("No LP found for transport=" + transport);
|
|
}
|
|
throw new SipException(
|
|
"Cannot create ACK - no ListeningPoint for transport towards next hop found:"
|
|
+ transport);
|
|
}
|
|
SIPRequest sipRequest = new SIPRequest();
|
|
sipRequest.setMethod(Request.ACK);
|
|
sipRequest.setRequestURI((SipUri) getRemoteTarget().getURI().clone());
|
|
sipRequest.setCallId(this.callIdHeader);
|
|
sipRequest.setCSeq(new CSeq(cseqno, Request.ACK));
|
|
List<Via> vias = new ArrayList<Via>();
|
|
// Via via = lp.getViaHeader();
|
|
// The user may have touched the sentby for the response.
|
|
// so use the via header extracted from the response for the ACK =>
|
|
// https://jain-sip.dev.java.net/issues/show_bug.cgi?id=205
|
|
// strip the params from the via of the response and use the params from the
|
|
// original request
|
|
Via via = this.lastResponse.getTopmostVia();
|
|
via.removeParameters();
|
|
if (originalRequest != null && originalRequest.getTopmostVia() != null) {
|
|
NameValueList originalRequestParameters = originalRequest.getTopmostVia()
|
|
.getParameters();
|
|
if (originalRequestParameters != null && originalRequestParameters.size() > 0) {
|
|
via.setParameters((NameValueList) originalRequestParameters.clone());
|
|
}
|
|
}
|
|
via.setBranch(Utils.getInstance().generateBranchId()); // new branch
|
|
vias.add(via);
|
|
sipRequest.setVia(vias);
|
|
From from = new From();
|
|
from.setAddress(this.localParty);
|
|
from.setTag(this.myTag);
|
|
sipRequest.setFrom(from);
|
|
To to = new To();
|
|
to.setAddress(this.remoteParty);
|
|
if (hisTag != null)
|
|
to.setTag(this.hisTag);
|
|
sipRequest.setTo(to);
|
|
sipRequest.setMaxForwards(new MaxForwards(70));
|
|
|
|
if (this.originalRequest != null) {
|
|
Authorization authorization = this.originalRequest.getAuthorization();
|
|
if (authorization != null)
|
|
sipRequest.setHeader(authorization);
|
|
}
|
|
|
|
// ACKs for 2xx responses
|
|
// use the Route values learned from the Record-Route of the 2xx
|
|
// responses.
|
|
this.updateRequest(sipRequest);
|
|
|
|
return sipRequest;
|
|
} catch (Exception ex) {
|
|
InternalErrorHandler.handleException(ex);
|
|
throw new SipException("unexpected exception ", ex);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Get the provider for this Dialog.
|
|
*
|
|
* SPEC_REVISION
|
|
*
|
|
* @return -- the SIP Provider associated with this transaction.
|
|
*/
|
|
public SipProviderImpl getSipProvider() {
|
|
return this.sipProvider;
|
|
}
|
|
|
|
/**
|
|
* @param sipProvider the sipProvider to set
|
|
*/
|
|
public void setSipProvider(SipProviderImpl sipProvider) {
|
|
this.sipProvider = sipProvider;
|
|
}
|
|
|
|
/**
|
|
* Check the tags of the response against the tags of the Dialog. Return true if the respnse
|
|
* matches the tags of the dialog. We do this check wehn sending out a response.
|
|
*
|
|
* @param sipResponse -- the response to check.
|
|
*
|
|
*/
|
|
public void setResponseTags(SIPResponse sipResponse) {
|
|
if (this.getLocalTag() != null || this.getRemoteTag() != null) {
|
|
return;
|
|
}
|
|
String responseFromTag = sipResponse.getFromTag();
|
|
if ( responseFromTag != null ) {
|
|
if (responseFromTag.equals(this.getLocalTag())) {
|
|
sipResponse.setToTag(this.getRemoteTag());
|
|
} else if (responseFromTag.equals(this.getRemoteTag())) {
|
|
sipResponse.setToTag(this.getLocalTag());
|
|
}
|
|
} else {
|
|
if (sipStack.isLoggingEnabled())
|
|
sipStack.getStackLogger().logWarning("No from tag in response! Not RFC 3261 compatible.");
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Set the last response for this dialog. This method is called for updating the dialog state
|
|
* when a response is either sent or received from within a Dialog.
|
|
*
|
|
* @param transaction -- the transaction associated with the response
|
|
* @param sipResponse -- the last response to set.
|
|
*/
|
|
public void setLastResponse(SIPTransaction transaction, SIPResponse sipResponse) {
|
|
this.callIdHeader = sipResponse.getCallId();
|
|
int statusCode = sipResponse.getStatusCode();
|
|
if (statusCode == 100) {
|
|
if (sipStack.isLoggingEnabled())
|
|
sipStack.getStackLogger().logWarning(
|
|
"Invalid status code - 100 in setLastResponse - ignoring");
|
|
return;
|
|
}
|
|
|
|
this.lastResponse = sipResponse;
|
|
this.setAssigned();
|
|
// Adjust state of the Dialog state machine.
|
|
if (sipStack.isLoggingEnabled()) {
|
|
sipStack.getStackLogger().logDebug(
|
|
"sipDialog: setLastResponse:" + this + " lastResponse = "
|
|
+ this.lastResponse.getFirstLine());
|
|
}
|
|
if (this.getState() == DialogState.TERMINATED) {
|
|
if (sipStack.isLoggingEnabled()) {
|
|
sipStack.getStackLogger().logDebug(
|
|
"sipDialog: setLastResponse -- dialog is terminated - ignoring ");
|
|
}
|
|
// Capture the OK response for later use in createAck
|
|
// This is handy for late arriving OK's that we want to ACK.
|
|
if (sipResponse.getCSeq().getMethod().equals(Request.INVITE) && statusCode == 200) {
|
|
|
|
this.lastInviteOkReceived = Math.max(sipResponse.getCSeq().getSeqNumber(),
|
|
this.lastInviteOkReceived);
|
|
}
|
|
return;
|
|
}
|
|
String cseqMethod = sipResponse.getCSeq().getMethod();
|
|
if (sipStack.isLoggingEnabled()) {
|
|
sipStack.getStackLogger().logStackTrace();
|
|
sipStack.getStackLogger().logDebug("cseqMethod = " + cseqMethod);
|
|
sipStack.getStackLogger().logDebug("dialogState = " + this.getState());
|
|
sipStack.getStackLogger().logDebug("method = " + this.getMethod());
|
|
sipStack.getStackLogger().logDebug("statusCode = " + statusCode);
|
|
sipStack.getStackLogger().logDebug("transaction = " + transaction);
|
|
}
|
|
|
|
// JvB: don't use "!this.isServer" here
|
|
// note that the transaction can be null for forked
|
|
// responses.
|
|
if (transaction == null || transaction instanceof ClientTransaction) {
|
|
if (sipStack.isDialogCreated(cseqMethod)) {
|
|
// Make a final tag assignment.
|
|
if (getState() == null && (statusCode / 100 == 1)) {
|
|
/*
|
|
* Guard aginst slipping back into early state from confirmed state.
|
|
*/
|
|
// Was (sipResponse.getToTag() != null || sipStack.rfc2543Supported)
|
|
setState(SIPDialog.EARLY_STATE);
|
|
if ((sipResponse.getToTag() != null || sipStack.rfc2543Supported)
|
|
&& this.getRemoteTag() == null) {
|
|
setRemoteTag(sipResponse.getToTag());
|
|
this.setDialogId(sipResponse.getDialogId(false));
|
|
sipStack.putDialog(this);
|
|
this.addRoute(sipResponse);
|
|
}
|
|
} else if (getState() != null && getState().equals(DialogState.EARLY)
|
|
&& statusCode / 100 == 1) {
|
|
/*
|
|
* This case occurs for forked dialog responses. The To tag can change as a
|
|
* result of the forking. The remote target can also change as a result of the
|
|
* forking.
|
|
*/
|
|
if (cseqMethod.equals(getMethod()) && transaction != null
|
|
&& (sipResponse.getToTag() != null || sipStack.rfc2543Supported)) {
|
|
setRemoteTag(sipResponse.getToTag());
|
|
this.setDialogId(sipResponse.getDialogId(false));
|
|
sipStack.putDialog(this);
|
|
this.addRoute(sipResponse);
|
|
}
|
|
} else if (statusCode / 100 == 2) {
|
|
// This is a dialog creating method (such as INVITE).
|
|
// 2xx response -- set the state to the confirmed
|
|
// state. To tag is MANDATORY for the response.
|
|
|
|
// Only do this if method equals initial request!
|
|
|
|
if (cseqMethod.equals(getMethod())
|
|
&& (sipResponse.getToTag() != null || sipStack.rfc2543Supported)
|
|
&& this.getState() != DialogState.CONFIRMED) {
|
|
setRemoteTag(sipResponse.getToTag());
|
|
this.setDialogId(sipResponse.getDialogId(false));
|
|
sipStack.putDialog(this);
|
|
this.addRoute(sipResponse);
|
|
|
|
setState(SIPDialog.CONFIRMED_STATE);
|
|
}
|
|
|
|
// Capture the OK response for later use in createAck
|
|
if (cseqMethod.equals(Request.INVITE)) {
|
|
this.lastInviteOkReceived = Math.max(sipResponse.getCSeq().getSeqNumber(),
|
|
this.lastInviteOkReceived);
|
|
}
|
|
|
|
} else if (statusCode >= 300
|
|
&& statusCode <= 699
|
|
&& (getState() == null || (cseqMethod.equals(getMethod()) && getState()
|
|
.getValue() == SIPDialog.EARLY_STATE))) {
|
|
/*
|
|
* This case handles 3xx, 4xx, 5xx and 6xx responses. RFC 3261 Section 12.3 -
|
|
* dialog termination. Independent of the method, if a request outside of a
|
|
* dialog generates a non-2xx final response, any early dialogs created
|
|
* through provisional responses to that request are terminated.
|
|
*/
|
|
setState(SIPDialog.TERMINATED_STATE);
|
|
}
|
|
|
|
/*
|
|
* This code is in support of "proxy" servers that are constructed as back to back
|
|
* user agents. This could be a dialog in the middle of the call setup path
|
|
* somewhere. Hence the incoming invite has record route headers in it. The
|
|
* response will have additional record route headers. However, for this dialog
|
|
* only the downstream record route headers matter. Ideally proxy servers should
|
|
* not be constructed as Back to Back User Agents. Remove all the record routes
|
|
* that are present in the incoming INVITE so you only have the downstream Route
|
|
* headers present in the dialog. Note that for an endpoint - you will have no
|
|
* record route headers present in the original request so the loop will not
|
|
* execute.
|
|
*/
|
|
if ( this.getState() != DialogState.CONFIRMED && this.getState() != DialogState.TERMINATED ) {
|
|
if (originalRequest != null) {
|
|
RecordRouteList rrList = originalRequest.getRecordRouteHeaders();
|
|
if (rrList != null) {
|
|
ListIterator<RecordRoute> it = rrList.listIterator(rrList.size());
|
|
while (it.hasPrevious()) {
|
|
RecordRoute rr = (RecordRoute) it.previous();
|
|
Route route = (Route) routeList.getFirst();
|
|
if (route != null && rr.getAddress().equals(route.getAddress())) {
|
|
routeList.removeFirst();
|
|
} else
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
} else if (cseqMethod.equals(Request.NOTIFY)
|
|
&& (this.getMethod().equals(Request.SUBSCRIBE) || this.getMethod().equals(
|
|
Request.REFER)) && sipResponse.getStatusCode() / 100 == 2
|
|
&& this.getState() == null) {
|
|
// This is a notify response.
|
|
this.setDialogId(sipResponse.getDialogId(true));
|
|
sipStack.putDialog(this);
|
|
this.setState(SIPDialog.CONFIRMED_STATE);
|
|
|
|
} else if (cseqMethod.equals(Request.BYE) && statusCode / 100 == 2
|
|
&& isTerminatedOnBye()) {
|
|
// Dialog will be terminated when the transction is terminated.
|
|
setState(SIPDialog.TERMINATED_STATE);
|
|
}
|
|
} else {
|
|
// Processing Server Dialog.
|
|
|
|
if (cseqMethod.equals(Request.BYE) && statusCode / 100 == 2
|
|
&& this.isTerminatedOnBye()) {
|
|
/*
|
|
* Only transition to terminated state when 200 OK is returned for the BYE. Other
|
|
* status codes just result in leaving the state in COMPLETED state.
|
|
*/
|
|
this.setState(SIPDialog.TERMINATED_STATE);
|
|
} else {
|
|
boolean doPutDialog = false;
|
|
|
|
if (getLocalTag() == null && sipResponse.getTo().getTag() != null
|
|
&& sipStack.isDialogCreated(cseqMethod) && cseqMethod.equals(getMethod())) {
|
|
setLocalTag(sipResponse.getTo().getTag());
|
|
|
|
doPutDialog = true;
|
|
}
|
|
|
|
if (statusCode / 100 != 2) {
|
|
if (statusCode / 100 == 1) {
|
|
if (doPutDialog) {
|
|
|
|
setState(SIPDialog.EARLY_STATE);
|
|
this.setDialogId(sipResponse.getDialogId(true));
|
|
sipStack.putDialog(this);
|
|
}
|
|
} else {
|
|
/*
|
|
* RFC 3265 chapter 3.1.4.1 "Non-200 class final responses indicate that
|
|
* no subscription or dialog has been created, and no subsequent NOTIFY
|
|
* message will be sent. All non-200 class" + responses (with the
|
|
* exception of "489", described herein) have the same meanings and
|
|
* handling as described in SIP"
|
|
*/
|
|
// Bug Fix by Jens tinfors
|
|
// see https://jain-sip.dev.java.net/servlets/ReadMsg?list=users&msgNo=797
|
|
if (statusCode == 489
|
|
&& (cseqMethod.equals(Request.NOTIFY) || cseqMethod
|
|
.equals(Request.SUBSCRIBE))) {
|
|
if (sipStack.isLoggingEnabled())
|
|
sipStack.getStackLogger().logDebug(
|
|
"RFC 3265 : Not setting dialog to TERMINATED for 489");
|
|
} else {
|
|
// baranowb: simplest fix to
|
|
// https://jain-sip.dev.java.net/issues/show_bug.cgi?id=175
|
|
// application is responsible for terminating in this case
|
|
// see rfc 5057 for better explanation
|
|
if (!this.isReInvite() && getState() != DialogState.CONFIRMED) {
|
|
this.setState(SIPDialog.TERMINATED_STATE);
|
|
}
|
|
}
|
|
}
|
|
|
|
} else {
|
|
|
|
/*
|
|
* JvB: RFC4235 says that when sending 2xx on UAS side, state should move to
|
|
* CONFIRMED
|
|
*/
|
|
if (this.dialogState <= SIPDialog.EARLY_STATE
|
|
&& (cseqMethod.equals(Request.INVITE)
|
|
|| cseqMethod.equals(Request.SUBSCRIBE) || cseqMethod
|
|
.equals(Request.REFER))) {
|
|
this.setState(SIPDialog.CONFIRMED_STATE);
|
|
}
|
|
|
|
if (doPutDialog) {
|
|
this.setDialogId(sipResponse.getDialogId(true));
|
|
sipStack.putDialog(this);
|
|
}
|
|
/*
|
|
* We put the dialog into the table. We must wait for ACK before re-INVITE is
|
|
* sent
|
|
*/
|
|
if (transaction.getState() != TransactionState.TERMINATED
|
|
&& sipResponse.getStatusCode() == Response.OK
|
|
&& cseqMethod.equals(Request.INVITE)
|
|
&& this.isBackToBackUserAgent) {
|
|
/*
|
|
* Acquire the flag for re-INVITE so that we cannot re-INVITE before
|
|
* ACK is received.
|
|
*/
|
|
if (!this.takeAckSem()) {
|
|
if (sipStack.isLoggingEnabled()) {
|
|
sipStack.getStackLogger().logDebug(
|
|
"Delete dialog -- cannot acquire ackSem");
|
|
}
|
|
this.delete();
|
|
return;
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Start the retransmit timer.
|
|
*
|
|
* @param sipServerTx -- server transaction on which the response was sent
|
|
* @param response - response that was sent.
|
|
*/
|
|
public void startRetransmitTimer(SIPServerTransaction sipServerTx, Response response) {
|
|
if (sipServerTx.getRequest().getMethod().equals(Request.INVITE)
|
|
&& response.getStatusCode() / 100 == 2) {
|
|
this.startTimer(sipServerTx);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return -- the last response associated with the dialog.
|
|
*/
|
|
public SIPResponse getLastResponse() {
|
|
|
|
return lastResponse;
|
|
}
|
|
|
|
/**
|
|
* Do taget refresh dialog state updates.
|
|
*
|
|
* RFC 3261: Requests within a dialog MAY contain Record-Route and Contact header fields.
|
|
* However, these requests do not cause the dialog's route set to be modified, although they
|
|
* may modify the remote target URI. Specifically, requests that are not target refresh
|
|
* requests do not modify the dialog's remote target URI, and requests that are target refresh
|
|
* requests do. For dialogs that have been established with an
|
|
*
|
|
* INVITE, the only target refresh request defined is re-INVITE (see Section 14). Other
|
|
* extensions may define different target refresh requests for dialogs established in other
|
|
* ways.
|
|
*/
|
|
private void doTargetRefresh(SIPMessage sipMessage) {
|
|
|
|
ContactList contactList = sipMessage.getContactHeaders();
|
|
|
|
/*
|
|
* INVITE is the target refresh for INVITE dialogs. SUBSCRIBE is the target refresh for
|
|
* subscribe dialogs from the client side. This modifies the remote target URI potentially
|
|
*/
|
|
if (contactList != null) {
|
|
|
|
Contact contact = (Contact) contactList.getFirst();
|
|
this.setRemoteTarget(contact);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
private static final boolean optionPresent(ListIterator l, String option) {
|
|
while (l.hasNext()) {
|
|
OptionTag opt = (OptionTag) l.next();
|
|
if (opt != null && option.equalsIgnoreCase(opt.getOptionTag()))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
* (non-Javadoc)
|
|
*
|
|
* @see javax.sip.Dialog#createReliableProvisionalResponse(int)
|
|
*/
|
|
public Response createReliableProvisionalResponse(int statusCode)
|
|
throws InvalidArgumentException, SipException {
|
|
|
|
if (!(firstTransactionIsServerTransaction)) {
|
|
throw new SipException("Not a Server Dialog!");
|
|
|
|
}
|
|
/*
|
|
* A UAS MUST NOT attempt to send a 100 (Trying) response reliably. Only provisional
|
|
* responses numbered 101 to 199 may be sent reliably. If the request did not include
|
|
* either a Supported or Require header field indicating this feature, the UAS MUST NOT
|
|
* send the provisional response reliably.
|
|
*/
|
|
if (statusCode <= 100 || statusCode > 199)
|
|
throw new InvalidArgumentException("Bad status code ");
|
|
SIPRequest request = this.originalRequest;
|
|
if (!request.getMethod().equals(Request.INVITE))
|
|
throw new SipException("Bad method");
|
|
|
|
ListIterator<SIPHeader> list = request.getHeaders(SupportedHeader.NAME);
|
|
if (list == null || !optionPresent(list, "100rel")) {
|
|
list = request.getHeaders(RequireHeader.NAME);
|
|
if (list == null || !optionPresent(list, "100rel")) {
|
|
throw new SipException("No Supported/Require 100rel header in the request");
|
|
}
|
|
}
|
|
|
|
SIPResponse response = request.createResponse(statusCode);
|
|
/*
|
|
* The provisional response to be sent reliably is constructed by the UAS core according
|
|
* to the procedures of Section 8.2.6 of RFC 3261. In addition, it MUST contain a Require
|
|
* header field containing the option tag 100rel, and MUST include an RSeq header field.
|
|
* The value of the header field for the first reliable provisional response in a
|
|
* transaction MUST be between 1 and 2**31 - 1. It is RECOMMENDED that it be chosen
|
|
* uniformly in this range. The RSeq numbering space is within a single transaction. This
|
|
* means that provisional responses for different requests MAY use the same values for the
|
|
* RSeq number.
|
|
*/
|
|
Require require = new Require();
|
|
try {
|
|
require.setOptionTag("100rel");
|
|
} catch (Exception ex) {
|
|
InternalErrorHandler.handleException(ex);
|
|
}
|
|
response.addHeader(require);
|
|
RSeq rseq = new RSeq();
|
|
/*
|
|
* set an arbitrary sequence number. This is actually set when the response is sent out
|
|
*/
|
|
rseq.setSeqNumber(1L);
|
|
/*
|
|
* Copy the record route headers from the request to the response ( Issue 160 ). Note that
|
|
* other 1xx headers do not get their Record Route headers copied over but reliable
|
|
* provisional responses do. See RFC 3262 Table 2.
|
|
*/
|
|
RecordRouteList rrl = request.getRecordRouteHeaders();
|
|
if (rrl != null) {
|
|
RecordRouteList rrlclone = (RecordRouteList) rrl.clone();
|
|
response.setHeader(rrlclone);
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
/**
|
|
* Do the processing necessary for the PRACK
|
|
*
|
|
* @param prackRequest
|
|
* @return true if this is the first time the tx has seen the prack ( and hence needs to be
|
|
* passed up to the TU)
|
|
*/
|
|
public boolean handlePrack(SIPRequest prackRequest) {
|
|
/*
|
|
* The RAck header is sent in a PRACK request to support reliability of provisional
|
|
* responses. It contains two numbers and a method tag. The first number is the value from
|
|
* the RSeq header in the provisional response that is being acknowledged. The next
|
|
* number, and the method, are copied from the CSeq in the response that is being
|
|
* acknowledged. The method name in the RAck header is case sensitive.
|
|
*/
|
|
if (!this.isServer()) {
|
|
if (sipStack.isLoggingEnabled())
|
|
sipStack.getStackLogger().logDebug("Dropping Prack -- not a server Dialog");
|
|
return false;
|
|
}
|
|
SIPServerTransaction sipServerTransaction = (SIPServerTransaction) this
|
|
.getFirstTransaction();
|
|
SIPResponse sipResponse = sipServerTransaction.getReliableProvisionalResponse();
|
|
|
|
if (sipResponse == null) {
|
|
if (sipStack.isLoggingEnabled())
|
|
sipStack.getStackLogger()
|
|
.logDebug("Dropping Prack -- ReliableResponse not found");
|
|
return false;
|
|
}
|
|
|
|
RAck rack = (RAck) prackRequest.getHeader(RAckHeader.NAME);
|
|
|
|
if (rack == null) {
|
|
if (sipStack.isLoggingEnabled())
|
|
sipStack.getStackLogger().logDebug("Dropping Prack -- rack header not found");
|
|
return false;
|
|
}
|
|
CSeq cseq = (CSeq) sipResponse.getCSeq();
|
|
|
|
if (!rack.getMethod().equals(cseq.getMethod())) {
|
|
if (sipStack.isLoggingEnabled())
|
|
sipStack.getStackLogger().logDebug(
|
|
"Dropping Prack -- CSeq Header does not match PRACK");
|
|
return false;
|
|
}
|
|
|
|
if (rack.getCSeqNumberLong() != cseq.getSeqNumber()) {
|
|
if (sipStack.isLoggingEnabled())
|
|
sipStack.getStackLogger().logDebug(
|
|
"Dropping Prack -- CSeq Header does not match PRACK");
|
|
return false;
|
|
}
|
|
|
|
RSeq rseq = (RSeq) sipResponse.getHeader(RSeqHeader.NAME);
|
|
|
|
if (rack.getRSequenceNumber() != rseq.getSeqNumber()) {
|
|
if (sipStack.isLoggingEnabled())
|
|
sipStack.getStackLogger().logDebug(
|
|
"Dropping Prack -- RSeq Header does not match PRACK");
|
|
return false;
|
|
}
|
|
|
|
return sipServerTransaction.prackRecieved();
|
|
}
|
|
|
|
/*
|
|
* (non-Javadoc)
|
|
*
|
|
* @see javax.sip.Dialog#sendReliableProvisionalResponse(javax.sip.message.Response)
|
|
*/
|
|
public void sendReliableProvisionalResponse(Response relResponse) throws SipException {
|
|
if (!this.isServer()) {
|
|
throw new SipException("Not a Server Dialog");
|
|
}
|
|
|
|
SIPResponse sipResponse = (SIPResponse) relResponse;
|
|
|
|
if (relResponse.getStatusCode() == 100)
|
|
throw new SipException("Cannot send 100 as a reliable provisional response");
|
|
|
|
if (relResponse.getStatusCode() / 100 > 2)
|
|
throw new SipException(
|
|
"Response code is not a 1xx response - should be in the range 101 to 199 ");
|
|
|
|
/*
|
|
* Do a little checking on the outgoing response.
|
|
*/
|
|
if (sipResponse.getToTag() == null) {
|
|
throw new SipException(
|
|
"Badly formatted response -- To tag mandatory for Reliable Provisional Response");
|
|
}
|
|
ListIterator requireList = (ListIterator) relResponse.getHeaders(RequireHeader.NAME);
|
|
boolean found = false;
|
|
|
|
if (requireList != null) {
|
|
|
|
while (requireList.hasNext() && !found) {
|
|
RequireHeader rh = (RequireHeader) requireList.next();
|
|
if (rh.getOptionTag().equalsIgnoreCase("100rel")) {
|
|
found = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!found) {
|
|
Require require = new Require("100rel");
|
|
relResponse.addHeader(require);
|
|
if (sipStack.isLoggingEnabled()) {
|
|
sipStack.getStackLogger().logDebug(
|
|
"Require header with optionTag 100rel is needed -- adding one");
|
|
}
|
|
|
|
}
|
|
|
|
SIPServerTransaction serverTransaction = (SIPServerTransaction) this
|
|
.getFirstTransaction();
|
|
/*
|
|
* put into the dialog table before sending the response so as to avoid race condition
|
|
* with PRACK
|
|
*/
|
|
this.setLastResponse(serverTransaction, sipResponse);
|
|
|
|
this.setDialogId(sipResponse.getDialogId(true));
|
|
|
|
serverTransaction.sendReliableProvisionalResponse(relResponse);
|
|
|
|
this.startRetransmitTimer(serverTransaction, relResponse);
|
|
|
|
}
|
|
|
|
/*
|
|
* (non-Javadoc)
|
|
*
|
|
* @see javax.sip.Dialog#terminateOnBye(boolean)
|
|
*/
|
|
public void terminateOnBye(boolean terminateFlag) throws SipException {
|
|
|
|
this.terminateOnBye = terminateFlag;
|
|
}
|
|
|
|
/**
|
|
* Set the "assigned" flag to true. We do this when inserting the dialog into the dialog table
|
|
* of the stack.
|
|
*
|
|
*/
|
|
public void setAssigned() {
|
|
this.isAssigned = true;
|
|
}
|
|
|
|
/**
|
|
* Return true if the dialog has already been mapped to a transaction.
|
|
*
|
|
*/
|
|
|
|
public boolean isAssigned() {
|
|
return this.isAssigned;
|
|
}
|
|
|
|
/**
|
|
* Get the contact header that the owner of this dialog assigned. Subsequent Requests are
|
|
* considered to belong to the dialog if the dialog identifier matches and the contact header
|
|
* matches the ip address and port on which the request is received.
|
|
*
|
|
* @return contact header belonging to the dialog.
|
|
*/
|
|
public Contact getMyContactHeader() {
|
|
return contactHeader;
|
|
}
|
|
|
|
/**
|
|
* Do the necessary processing to handle an ACK directed at this Dialog.
|
|
*
|
|
* @param ackTransaction -- the ACK transaction that was directed at this dialog.
|
|
* @return -- true if the ACK was successfully consumed by the Dialog and resulted in the
|
|
* dialog state being changed.
|
|
*/
|
|
public boolean handleAck(SIPServerTransaction ackTransaction) {
|
|
SIPRequest sipRequest = ackTransaction.getOriginalRequest();
|
|
|
|
if (isAckSeen() && getRemoteSeqNumber() == sipRequest.getCSeq().getSeqNumber()) {
|
|
|
|
if (sipStack.isLoggingEnabled()) {
|
|
sipStack.getStackLogger().logDebug(
|
|
"ACK already seen by dialog -- dropping Ack" + " retransmission");
|
|
}
|
|
acquireTimerTaskSem();
|
|
try {
|
|
if (this.timerTask != null) {
|
|
this.timerTask.cancel();
|
|
this.timerTask = null;
|
|
}
|
|
} finally {
|
|
releaseTimerTaskSem();
|
|
}
|
|
return false;
|
|
} else if (this.getState() == DialogState.TERMINATED) {
|
|
if (sipStack.isLoggingEnabled())
|
|
sipStack.getStackLogger().logDebug("Dialog is terminated -- dropping ACK");
|
|
return false;
|
|
|
|
} else {
|
|
|
|
/*
|
|
* This could be a re-invite processing. check to see if the ack matches with the last
|
|
* transaction. s
|
|
*/
|
|
|
|
SIPServerTransaction tr = getInviteTransaction();
|
|
|
|
SIPResponse sipResponse = (tr != null ? tr.getLastResponse() : null);
|
|
|
|
// Idiot check for sending ACK from the wrong side!
|
|
if (tr != null
|
|
&& sipResponse != null
|
|
&& sipResponse.getStatusCode() / 100 == 2
|
|
&& sipResponse.getCSeq().getMethod().equals(Request.INVITE)
|
|
&& sipResponse.getCSeq().getSeqNumber() == sipRequest.getCSeq()
|
|
.getSeqNumber()) {
|
|
|
|
ackTransaction.setDialog(this, sipResponse.getDialogId(false));
|
|
/*
|
|
* record that we already saw an ACK for this dialog.
|
|
*/
|
|
|
|
ackReceived(sipRequest);
|
|
if (sipStack.isLoggingEnabled())
|
|
sipStack.getStackLogger().logDebug("ACK for 2XX response --- sending to TU ");
|
|
return true;
|
|
|
|
} else {
|
|
/*
|
|
* This happens when the ACK is re-transmitted and arrives too late to be
|
|
* processed.
|
|
*/
|
|
|
|
if (sipStack.isLoggingEnabled())
|
|
sipStack.getStackLogger().logDebug(
|
|
" INVITE transaction not found -- Discarding ACK");
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
void setEarlyDialogId(String earlyDialogId) {
|
|
this.earlyDialogId = earlyDialogId;
|
|
}
|
|
|
|
String getEarlyDialogId() {
|
|
return earlyDialogId;
|
|
}
|
|
|
|
/**
|
|
* Release the semaphore for ACK processing so the next re-INVITE may proceed.
|
|
*/
|
|
void releaseAckSem() {
|
|
if (this.isBackToBackUserAgent) {
|
|
if (sipStack.isLoggingEnabled()) {
|
|
sipStack.getStackLogger().logDebug("releaseAckSem]" + this);
|
|
}
|
|
this.ackSem.release();
|
|
}
|
|
|
|
}
|
|
|
|
boolean takeAckSem() {
|
|
if (sipStack.isLoggingEnabled()) {
|
|
sipStack.getStackLogger().logDebug("[takeAckSem " + this);
|
|
}
|
|
try {
|
|
if (!this.ackSem.tryAcquire(2, TimeUnit.SECONDS)) {
|
|
if (sipStack.isLoggingEnabled()) {
|
|
sipStack.getStackLogger().logError("Cannot aquire ACK semaphore");
|
|
}
|
|
|
|
if ( sipStack.isLoggingEnabled() ) {
|
|
sipStack.getStackLogger().logDebug("Semaphore previously acquired at " + this.stackTrace);
|
|
sipStack.getStackLogger().logStackTrace();
|
|
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if ( sipStack.isLoggingEnabled() ) {
|
|
|
|
this.recordStackTrace();
|
|
}
|
|
|
|
} catch (InterruptedException ex) {
|
|
sipStack.getStackLogger().logError("Cannot aquire ACK semaphore");
|
|
return false;
|
|
|
|
}
|
|
return true;
|
|
|
|
}
|
|
|
|
/**
|
|
* @param lastAckReceived the lastAckReceived to set
|
|
*/
|
|
private void setLastAckReceived(SIPRequest lastAckReceived) {
|
|
this.lastAckReceived = lastAckReceived;
|
|
}
|
|
|
|
/**
|
|
* @return the lastAckReceived
|
|
*/
|
|
protected SIPRequest getLastAckReceived() {
|
|
return lastAckReceived;
|
|
}
|
|
|
|
/**
|
|
* @param lastAckSent the lastAckSent to set
|
|
*/
|
|
private void setLastAckSent(SIPRequest lastAckSent) {
|
|
this.lastAckSent = lastAckSent;
|
|
}
|
|
|
|
/**
|
|
* @return true if an ack was ever sent for this Dialog
|
|
*/
|
|
public boolean isAtleastOneAckSent() {
|
|
return this.isAcknowledged;
|
|
}
|
|
|
|
|
|
|
|
public boolean isBackToBackUserAgent() {
|
|
return this.isBackToBackUserAgent;
|
|
}
|
|
|
|
public synchronized void doDeferredDeleteIfNoAckSent(long seqno) {
|
|
if (sipStack.getTimer() == null) {
|
|
this.setState(TERMINATED_STATE);
|
|
} else if(dialogDeleteIfNoAckSentTask == null){
|
|
// Delete the transaction after the max ack timeout.
|
|
dialogDeleteIfNoAckSentTask = new DialogDeleteIfNoAckSentTask(seqno);
|
|
sipStack.getTimer().schedule(
|
|
dialogDeleteIfNoAckSentTask,
|
|
SIPTransaction.TIMER_J
|
|
* SIPTransactionStack.BASE_TIMER_INTERVAL);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* (non-Javadoc)
|
|
* @see gov.nist.javax.sip.DialogExt#setBackToBackUserAgent(boolean)
|
|
*/
|
|
public void setBackToBackUserAgent() {
|
|
this.isBackToBackUserAgent = true;
|
|
}
|
|
|
|
/**
|
|
* @return the eventHeader
|
|
*/
|
|
EventHeader getEventHeader() {
|
|
return eventHeader;
|
|
}
|
|
|
|
/**
|
|
* @param eventHeader the eventHeader to set
|
|
*/
|
|
void setEventHeader(EventHeader eventHeader) {
|
|
this.eventHeader = eventHeader;
|
|
}
|
|
|
|
/**
|
|
* @param serverTransactionFlag the serverTransactionFlag to set
|
|
*/
|
|
void setServerTransactionFlag(boolean serverTransactionFlag) {
|
|
this.serverTransactionFlag = serverTransactionFlag;
|
|
}
|
|
|
|
/**
|
|
* @param reInviteFlag the reinviteFlag to set
|
|
*/
|
|
void setReInviteFlag(boolean reInviteFlag) {
|
|
this.reInviteFlag = reInviteFlag;
|
|
}
|
|
|
|
|
|
public boolean isSequnceNumberValidation() {
|
|
return this.sequenceNumberValidation;
|
|
}
|
|
|
|
public void disableSequenceNumberValidation() {
|
|
this.sequenceNumberValidation = false;
|
|
}
|
|
|
|
|
|
public void acquireTimerTaskSem() {
|
|
boolean acquired = false;
|
|
try {
|
|
acquired = this.timerTaskLock.tryAcquire(10, TimeUnit.SECONDS);
|
|
} catch ( InterruptedException ex) {
|
|
acquired = false;
|
|
}
|
|
if(!acquired) {
|
|
throw new IllegalStateException("Impossible to acquire the dialog timer task lock");
|
|
}
|
|
}
|
|
|
|
public void releaseTimerTaskSem() {
|
|
this.timerTaskLock.release();
|
|
}
|
|
|
|
|
|
}
|