/*
* Portions Copyright 2000-2007 Sun Microsystems, Inc. All Rights
* Reserved. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version
* 2 only, as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License version 2 for more details (a copy is
* included at /legal/license.txt).
*
* You should have received a copy of the GNU General Public License
* version 2 along with this work; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA
*
* Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
* Clara, CA 95054 or visit www.sun.com if you need additional
* information or have any questions.
*/
/*
*
*
* Created on Jan 29, 2004
*
*/
package gov.nist.microedition.sip;
import gov.nist.core.ParseException;
import gov.nist.core.NameValue;
import java.io.IOException;
import java.io.OutputStream;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.ByteArrayOutputStream;
import java.io.ByteArrayInputStream;
import java.util.Vector;
import java.util.Enumeration;
import javax.microedition.rms.RecordEnumeration;
import javax.microedition.rms.RecordStore;
import javax.microedition.rms.RecordStoreException;
import javax.microedition.sip.SipConnection;
import javax.microedition.sip.SipClientConnection;
import javax.microedition.sip.SipClientConnectionListener;
import javax.microedition.sip.SipConnectionNotifier;
import javax.microedition.sip.SipDialog;
import javax.microedition.sip.SipException;
import javax.microedition.sip.SipRefreshListener;
import gov.nist.siplite.TransactionUnavailableException;
import gov.nist.siplite.SipStack;
import gov.nist.siplite.address.Address;
import gov.nist.siplite.address.SipURI;
import gov.nist.siplite.address.URI;
import gov.nist.siplite.message.*;
import gov.nist.siplite.stack.Subscription;
import gov.nist.siplite.stack.ClientTransaction;
import gov.nist.siplite.stack.Dialog;
import gov.nist.siplite.stack.authentication.Credentials;
import gov.nist.siplite.stack.authentication.DigestClientAuthentication;
import gov.nist.siplite.header.CSeqHeader;
import gov.nist.siplite.header.CallIdHeader;
import gov.nist.siplite.header.ContactHeader;
import gov.nist.siplite.header.ContentLengthHeader;
import gov.nist.siplite.header.ContactList;
import gov.nist.siplite.header.ContentTypeHeader;
import gov.nist.siplite.header.ExpiresHeader;
import gov.nist.siplite.header.FromHeader;
import gov.nist.siplite.header.Header;
import gov.nist.siplite.header.HeaderList;
import gov.nist.siplite.header.MaxForwardsHeader;
import gov.nist.siplite.header.ToHeader;
import gov.nist.siplite.header.ViaHeader;
import gov.nist.siplite.header.SubscriptionStateHeader;
import gov.nist.siplite.header.ParameterLessHeader;
import gov.nist.siplite.header.ExtensionHeader;
import gov.nist.siplite.SIPUtils;
import gov.nist.siplite.SIPConstants;
import gov.nist.core.NameValueList;
import gov.nist.core.Utils;
import gov.nist.core.Separators;
import com.sun.midp.security.SecurityToken;
import gov.nist.siplite.parser.Lexer;
import com.sun.midp.log.Logging;
import com.sun.midp.log.LogChannels;
/**
* Client SIP connection implementation.
*
* <a href="{@docRoot}/uncopyright.html">This code is in the public domain.</a>
*/
public class SipClientConnectionImpl
implements SipClientConnection {
// Runnable {
/**
* Security token for SIP/SIPS protocol class
*/
private SecurityToken classSecurityToken;
// Client Transaction States
/**
* Terminated, the final state, in which the SIP connection has
* been terminated by error or closed
*/
public static final int TERMINATED = 0;
/**
* Created, SipClientConnection created from Connector or SipDialog
*/
public static final int CREATED = 1;
/**
* Initialized, request has been initialized with initRequest(...)
* or initAck() or initCancel()
*/
public static final int INITIALIZED = 2;
/**
* Stream Open, OutputStream opened with openContentOutputStream().
* Opening InputStream for received response does not trigger
* state transition.
*/
public static final int STREAM_OPEN = 3;
/**
* Proceeding, request has been sent, waiting for the response, or
* provisional 1xx response received. initCancel() can be called,
* which will spawn a new SipClientConnection which is in
* Initialized state
*/
public static final int PROCEEDING = 4;
/**
* Unauthorized, transaction completed with response 401
* (Unauthorized) or 407 (Proxy Authentication Required). The
* application can re-originate the request with proper
* credentials by calling setCredentials() method. After this the
* SipClientConnection is back in Proceeding state.
*/
public static final int UNAUTHORIZED = 5;
/**
* Completed, transaction completed with final response
* (2xx, 3xx, 4xx, 5xx, 6xx) in this state the ACK can be initialized.
* Multiple 200 OK responses can be received. Note different state
* transition for responses 401 and 407.
*/
public static final int COMPLETED = 6;
/**
* Max length of queue of incoming responses.
*/
private static final int MAX_NUM_RESPONSES = 10;
/**
* the sip dialog this client transaction belongs to
*/
private SipDialog sipDialog = null;
/**
* Listener to notify when a response will be received
*/
private SipClientConnectionListener sipClientConnectionListener = null;
/**
* The Sip Connection notifier associated with this client connection
*/
private SipConnectionNotifier sipConnectionNotifier = null;
/**
* Callback interface for refresh events
*/
private SipRefreshListener sipRefreshListener = null;
/**
* The refresh ID of the refresh task associated with this client
* connection if there is any
*/
private String refreshID = null;
/**
* current state of this client transaction
*/
protected int state;
/**
* flag to know the state of the connection (open or close)
*/
private boolean connectionOpen;
/**
* list of credentials that can be used for authorization
*/
private Vector credentials;
/**
* The request for this client transaction.
*/
private Request request = null;
/**
* The initial request saved before ACK is sent.
*/
private Request requestSavedBeforeACK = null;
/**
* the response to the actual request
*/
private Response response = null;
/**
* The queue of responses for processing.
*/
private Vector responses = new Vector();
/**
* the last received response
*/
private Response responseReceived = null;
/**
* content of the response body
*/
private SDPOutputStream contentOutputStream = null;
/**
* content from the request body
*/
private InputStream contentInputStream = null;
/**
* The request URI created from the user, host, port and
* parameters attributes
*/
private URI requestURI = null;
/**
* Scheme name
*/
private String scheme = null;
/**
* the user part of the SIP URI
*/
private String user = null;
/**
* the host part of the SIP URI
*/
private String host = null;
/**
* the port Number on which to send the request, part of
* the SIP URI
*/
private int port = -1;
/**
* the parameters of the SIP URI
*/
private NameValueList parameters = null;
/**
* the sip uri of the user
*/
private String userSipURI = "sip:anonymous@anonymous.invalid";
/**
* the client Transaction for an INVITE request
*/
private ClientTransaction clientTransaction = null;
/**
* Handle for asynchronous listening thread.
*/
private Thread listeningThread = null;
/**
* Current stack of connectors.
*/
private StackConnector stackConnector = null;
/**
* Flag of creating internal notifier.
*/
private boolean isNotifierCreated = false;
/**
* Flag indicating which SIP message (request or response)
* should be used in getHeader()/getHeaders().
*/
private boolean useRequest;
/**
* Permission of generating CANCEL request.
*/
protected boolean enableInitCancel = false;
/**
* Count of authorization requests (RFC 2617, 3.2.2).
*/
private int countReoriginateRequest = 1;
/**
* Creates a sip Client Connection to send a request to the
* following SIP URI user@host:portNumber;parameters
* @param inputURI input SIP URI
* @param classSecurityToken Security token for SIP/SIPS protocol class
*/
protected SipClientConnectionImpl(
SipURI inputURI,
SecurityToken classSecurityToken) throws IllegalArgumentException {
this.user = inputURI.getUser();
this.host = inputURI.getHost();
this.port = inputURI.getPort();
this.parameters = inputURI.getUriParms();
this.classSecurityToken = classSecurityToken;
this.scheme = inputURI.getScheme();
connectionOpen = true;
credentials = new Vector();
try {
stackConnector = StackConnector.getInstance(classSecurityToken);
} catch (IOException ioe) {
}
// Create the REQUEST URI of the request
try {
requestURI = StackConnector.addressFactory.createURI(scheme + ":" +
((user == null) ? "" : (user + "@")) + host);
if (port != -1) {
((SipURI) requestURI).setPort(port);
}
// handle the parameters
if (parameters != null) {
Enumeration parNames = parameters.getKeys();
while (parNames.hasMoreElements()) {
String name = (String) parNames.nextElement();
String value = (String) parameters.getValue(name);
((SipURI) requestURI).setParameter(name, value);
}
}
} catch (ParseException pe) {
throw new
IllegalArgumentException("The request URI can not be" +
" created, check the URI syntax");
}
state = CREATED;
useRequest = true;
}
/**
* Constructs the client connection implementation.
* @param requestURI the target SIP session URI
* @param sipDialog the current transaction state
*/
protected SipClientConnectionImpl(URI requestURI,
SipDialog sipDialog)
throws IllegalArgumentException {
if (!requestURI.isSipURI()) {
throw new IllegalArgumentException("URI is not correct");
}
SipURI sipURI = (SipURI) requestURI;
SipDialogImpl sipDialogImpl = (SipDialogImpl) sipDialog;
user = sipURI.getUser();
host = sipURI.getHost();
port = sipURI.getPort();
parameters = sipURI.getUriParms();
classSecurityToken = sipDialogImpl.getSecurityToken();
scheme = requestURI.getScheme();
connectionOpen = true;
credentials = new Vector();
// Create the REQUEST URI of the request
this.requestURI = requestURI;
this.sipDialog = sipDialog;
this.refreshID = sipDialogImpl.getRefreshID();
// this.sipClientConnectionListener =
// ((SipDialogImpl)sipDialog).getSipClientConnectionListener();
try {
stackConnector = StackConnector.getInstance(classSecurityToken);
} catch (IOException ioe) {
if (Logging.REPORT_LEVEL <= Logging.ERROR) {
Logging.report(Logging.ERROR, LogChannels.LC_JSR180,
"Could not create SipClientConnectionImpl: " +
ioe);
}
}
sipDialogImpl.dialog.setStack(stackConnector.getSipStack());
state = CREATED;
useRequest = true;
}
/**
* Constructs the client connection implementation.
* @param request the current transaction
* @param sipConnectionNotifier the state transition notifier
* @param sipUserURI the user session information
*/
private SipClientConnectionImpl(Request request,
SipConnectionNotifier
sipConnectionNotifier,
String sipUserURI)
throws IllegalArgumentException {
connectionOpen = true;
credentials = new Vector();
// Create the REQUEST of the connection
this.request = request;
this.userSipURI = sipUserURI;
// Create the REQUEST URI of the request
this.requestURI = request.getRequestURI();
this.sipConnectionNotifier = sipConnectionNotifier;
try {
stackConnector = StackConnector.getInstance(classSecurityToken);
} catch (IOException ioe) {
if (Logging.REPORT_LEVEL <= Logging.ERROR) {
Logging.report(Logging.ERROR, LogChannels.LC_JSR180,
"Could not create SipClientConnectionImpl: " +
ioe);
}
}
if (request.getMethod().equals(Request.CANCEL)) {
state = INITIALIZED;
} else {
state = CREATED;
}
useRequest = true;
}
/**
* Initializes the connection.
* @param method the operation to be performed
* @param scn the state transition notifier
* @exception IllegalArgumentException if the parameters are not
* valid
* @exception SipException if a transition error occurs
* @see SipClientConnection#initRequest
*/
public void initRequest(String method, SipConnectionNotifier scn) throws
IllegalArgumentException, SipException {
if (method == null) {
throw new IllegalArgumentException("The method can not be null");
}
if (state != CREATED) {
throw new SipException("the request can not be initialized," +
" because of wrong state.",
SipException.INVALID_STATE);
}
if ((state == CREATED) &&
((method.equals(Request.ACK)) ||
(method.equals(Request.CANCEL)))) {
throw new SipException("the request can not be initialized," +
" because of wrong state.",
SipException.INVALID_STATE);
}
if (!Lexer.isValidName(method)) {
throw new IllegalArgumentException("Invalid method: '" +
method + "'");
}
// Affect the sip connection notifier
if (scn != null) {
sipConnectionNotifier = scn;
} else {
String transport = parameters.getValueDefault(
SIPConstants.GENERAL_TRANSPORT, SIPConstants.TRANSPORT_UDP);
int localPort;
// see RFC 3261, 18.1.1 - selecting port number
if (transport.equalsIgnoreCase(SIPConstants.TRANSPORT_TLS)) {
localPort = SIPConstants.DEFAULT_TLS_PORT;
} else { // other transport protocol
localPort = SIPConstants.DEFAULT_NONTLS_PORT;
}
// Check if sipConnectionNotifier already exists on the same
// port. This is always true when UAC and UAS are in the same
// application and the user has opened a connection (like
// Connector.open("sip:5060");) before calling this function.
Vector connectionNotifiersList =
stackConnector.getConnectionNotifiersList();
try {
for (int i = 0; i < connectionNotifiersList.size(); i++) {
SipConnectionNotifier currNotifier =
(SipConnectionNotifier)
connectionNotifiersList.elementAt(i);
if ((currNotifier.getLocalPort() == localPort) &&
(((SipConnectionNotifierImpl) currNotifier).
getSipProvider().getListeningPoint().
getTransport().equalsIgnoreCase(transport))) {
sipConnectionNotifier = currNotifier;
break;
}
}
} catch (IOException ioe) {
throw new SipException(ioe.getMessage(),
SipException.GENERAL_ERROR);
}
if (sipConnectionNotifier == null) {
// Notifier was not found - create it.
try {
sipConnectionNotifier =
stackConnector.createSipConnectionNotifier(
localPort,
scheme.equals(SIPConstants.SCHEME_SIP),
transport, null);
} catch (IOException ioe) {
throw new SipException(ioe.getMessage(),
SipException.GENERAL_ERROR);
}
isNotifierCreated = true;
} // end if
} // end else (scn == null)
// redirect the methods ACK and CANCEL towards their helper
// methods
if (method.equals(Request.ACK)) {
initAck();
}
if (method.equals(Request.BYE) && (sipDialog != null)) {
initBye();
state = INITIALIZED;
useRequest = true;
return;
}
if (method.equals(Request.NOTIFY)) {
// if a dialog was not created, send NOTIFY out of dialog.
if (sipDialog != null) {
initNotify();
state = INITIALIZED;
useRequest = true;
return;
}
}
// Create request into dialog
if (sipDialog != null) {
byte dialogState = sipDialog.getState();
if ((dialogState == SipDialog.EARLY) ||
(dialogState == SipDialog.CONFIRMED)) {
if (method.equals(Request.PRACK) &&
(! ((SipDialogImpl) sipDialog).isReliableProvReceived)) {
return;
}
// if (sipDialog.getState() == SipDialog.CONFIRMED) {
// When SipDialog instance has CONFIRMED state, any new
// request should be inside of dialog and have same
// headers (To, From, Call-ID...) as original request.
try {
request =
((SipDialogImpl) sipDialog).dialog.createRequest(
method);
} catch (SipException ex) {
throw ex;
// throw new IllegalArgumentException(
// "Could not create the bye request! " + ex);
}
state = INITIALIZED;
useRequest = true;
return;
}
}
// We lookup in a record store to see whether or not there is
// the user sip uri
String sipURI = null;
try {
RecordStore rs = RecordStore.openRecordStore("UserSipUri", false);
RecordEnumeration re = rs.enumerateRecords(null, null, false);
if (re.hasNextElement()) {
int recordID = re.nextRecordId();
sipURI = new String(rs.getRecord(recordID));
}
} catch (RecordStoreException rse) {
// rse.printStackTrace();
}
// if the record store is null the sip uri for the user
// it is an anonymous sip uri
if (sipURI != null) {
userSipURI = sipURI;
}
Address userAddress = null;
try {
userAddress = StackConnector.addressFactory
.createAddress(userSipURI);
} catch (ParseException pe) {
throw new IllegalArgumentException("The system property UserSipUri"
+
"can not be parsed, check the syntax");
}
// Call ID
CallIdHeader callIdHeader = null;
String callId = SIPUtils.generateCallIdentifier
(stackConnector.getSipStack().getIPAddress());
callIdHeader = new CallIdHeader();
callIdHeader.setCallId(callId);
// CSeq
CSeqHeader cSeqHeader = null;
try {
cSeqHeader = StackConnector.headerFactory.createCSeqHeader(1,
method);
} catch (ParseException pe) {
throw new SipException("Problem during the creation" +
" of the CSeqHeader",
SipException.GENERAL_ERROR);
}
// From
FromHeader fromHeader = null;
try {
fromHeader = StackConnector
.headerFactory
.createFromHeader(
userAddress,
StackConnector.generateTag());
} catch (ParseException ex) {
throw new SipException("Problem during the creation" +
" of the FromHeader",
SipException.GENERAL_ERROR);
}
// ToHeader
Address toAddress = StackConnector
.addressFactory.createAddress(
requestURI);
ToHeader toHeader = null;
try {
toHeader = StackConnector
.headerFactory.createToHeader(
toAddress, null);
} catch (ParseException ex) {
throw new SipException("Problem during the creation" +
" of the ToHeader",
SipException.GENERAL_ERROR);
}
// ViaHeader
Vector viaHeaders = new Vector();
String viaLocalAddress;
String viaTransport;
int viaLocalPort;
try {
viaLocalAddress = sipConnectionNotifier.getLocalAddress();
viaLocalPort = sipConnectionNotifier.getLocalPort();
viaTransport =
((SipConnectionNotifierImpl) sipConnectionNotifier)
.getSipProvider().getListeningPoint().getTransport();
} catch (IOException ioe) {
throw new SipException("Internal Error, cannot get " +
"the local port or address",
SipException.GENERAL_ERROR);
}
try {
ViaHeader viaHeader = StackConnector
.headerFactory
.createViaHeader(
viaLocalAddress,
viaLocalPort,
viaTransport,
SIPUtils.generateBranchId());
viaHeaders.addElement(viaHeader);
} catch (ParseException ex) {
throw new SipException("Problem during the creation" +
" of the ViaHeaders",
SipException.GENERAL_ERROR);
}
// Max Forward Header
MaxForwardsHeader maxForwardsHeader =
StackConnector.headerFactory.createMaxForwardsHeader(70);
// generate the request
try {
request = StackConnector
.messageFactory.createRequest(
requestURI,
method,
callIdHeader,
cSeqHeader,
fromHeader,
toHeader,
viaHeaders,
maxForwardsHeader);
} catch (ParseException ex) {
throw new SipException("Problem during the creation " +
" of the Request " + method,
SipException.GENERAL_ERROR);
}
/*
* Contact header - not in MESSAGE request (RFC 3428, 4).
* RFC 3903, p. 5:
* The PUBLISH request MAY contain a Contact header field, but including
* one in a PUBLISH request has no meaning in the event publication
* context and will be ignored by the ESC (Event State Compositor).
*/
if (!method.equals(Request.MESSAGE) &&
!method.equals(Request.PUBLISH)) {
ContactHeader contactHeader = null;
try {
if (isNotifierCreated) {
// Notifier was not passed as an argument to initRequest()
SipURI contactURI = StackConnector
.addressFactory
.createSipURI("anonymous", // name
viaLocalAddress);
contactURI.setTransportParam(viaTransport);
contactURI.setPort(viaLocalPort);
contactHeader =
StackConnector
.headerFactory
.createContactHeader(
StackConnector
.addressFactory
.createAddress(contactURI));
} else { // notifier is given
SipURI contactURI = StackConnector
.addressFactory
.createSipURI(
userSipURI
.substring(scheme.length() + 1,
userSipURI.indexOf("@")),
sipConnectionNotifier.getLocalAddress());
contactURI
.setTransportParam(
((SipConnectionNotifierImpl)
sipConnectionNotifier).
getSipProvider().getListeningPoint()
.getTransport());
contactHeader =
StackConnector
.headerFactory
.createContactHeader(
StackConnector
.addressFactory
.createAddress(contactURI));
contactURI.setPort(sipConnectionNotifier.getLocalPort());
}
} catch (IOException ioe) {
throw new SipException("Internal Error, cannot get " +
"the local port or address",
SipException.GENERAL_ERROR);
} catch (ParseException ex) {
throw new SipException("Problem during the creation " +
"of the Contact Header",
SipException.GENERAL_ERROR);
}
// set the header
request.addHeader(contactHeader);
}
state = INITIALIZED;
useRequest = true;
}
/**
* @see SipClientConnection#setRequestURI(java.lang.String)
*/
/**
* Sets Request-URI explicitly. Request-URI can be set only in
* Initialized state.
* @param newUri Request-URI
* @throws IllegalArgumentException MAY be thrown if the URI is invalid
* @throws SipException INVALID_STATE if the Request-URI can not be set,
* because of wrong state.
* INVALID_OPERATION if the Request-URI is not allowed to be set.
*/
public void setRequestURI(String newUri) throws IllegalArgumentException,
SipException {
if (state != INITIALIZED) {
throw new SipException("the request URI can not be set, " +
" because of wrong state.",
SipException.INVALID_STATE);
}
if (newUri == null) {
throw new IllegalArgumentException("Invalid URI");
}
URI uri = null;
try {
uri = StackConnector.addressFactory.createURI(newUri);
} catch (ParseException pe) {
throw new IllegalArgumentException("Invalid URI");
}
request.setRequestURI(uri);
requestURI = uri;
}
/**
* Convenience method to initialize SipClientConnection with SIP request
* method ACK. ACK can be applied only to INVITE request.
* @see JSR180 spec, v 1.0.1, p 27
*
*/
public void initAck() throws SipException {
if (state != COMPLETED) {
throw new SipException("the ACK request can not be initialized,"
+ " because of wrong state.",
SipException.INVALID_STATE);
}
// restore first request
if (requestSavedBeforeACK != null) {
request = requestSavedBeforeACK;
}
if (!request.getMethod().equals(Request.INVITE)) {
// original request is non-INVITE
throw new SipException("Original request is non-INVITE",
SipException.INVALID_OPERATION);
}
// JSR180: For error responses (3xx-6xx) the ACK is sent
// automatically by the system in transaction level.
// If user initializes an ACK which has already been
// sent an Exception will be thrown.
int statusCode = 0;
if (response != null) {
statusCode = response.getStatusCode() / 100;
} else if (responseReceived != null) {
statusCode = responseReceived.getStatusCode() / 100;
}
if (responseReceived.getStatusCode() / 100 > 2) {
throw new SipException("ACK request was already sent",
SipException.INVALID_OPERATION);
}
requestSavedBeforeACK = request; // save request
// This may throw SipException.
request = clientTransaction.createAck();
state = INITIALIZED;
useRequest = true;
}
/**
* Initialize the session termination transaction.
*/
protected void initBye() {
// Generate Request
SipDialogImpl sipDialogImpl = (SipDialogImpl) sipDialog;
try {
request = sipDialogImpl.dialog.createRequest(Request.BYE);
// handle the parameters
if (parameters != null) {
Enumeration parNames = parameters.getKeys();
while (parNames.hasMoreElements()) {
String name = (String) parNames.nextElement();
String value = (String) parameters.getValue(name);
((SipURI) requestURI).setParameter(name, value);
}
}
} catch (SipException ex) {
if (Logging.REPORT_LEVEL <= Logging.ERROR) {
Logging.report(Logging.ERROR, LogChannels.LC_JSR180,
"Could not create BYE request! " + ex);
}
} catch (ParseException pe) {
// intentionally ignored
// setParameter() is used with verified parameters
}
}
/**
* Convenience method to initialize SipClientConnection with SIP request
* method NOTIFY.
* This method is copied from latest updates to NIST workspace
*/
protected void initNotify() {
// don't call this method out of dialog
if (sipDialog == null) {
throw new IllegalArgumentException(
"Initialization NOTIFY request out of dialog");
}
// Generate Request
SipDialogImpl sipDialogImpl = (SipDialogImpl) sipDialog;
try {
request = sipDialogImpl.dialog.createRequest(Request.NOTIFY);
// handle the parameters
if (parameters != null) {
Enumeration parNames = parameters.getKeys();
while (parNames.hasMoreElements()) {
String name = (String) parNames.nextElement();
String value = (String) parameters.getValue(name);
((SipURI) requestURI).setParameter(name, value);
}
}
} catch (SipException ex) {
// IMPL_NOTE : cleanup
if (Logging.REPORT_LEVEL <= Logging.ERROR) {
Logging.report(Logging.ERROR, LogChannels.LC_JSR180,
"Could not create the notify request! " + ex);
}
} catch (ParseException pe) {
// intentionally ignored
// setParameter() is used with verified parameters
}
}
/**
* Convenience method to initialize SipClientConnection with SIP request
* method CANCEL.
* @return A new SipClientConnection with preinitialized CANCEL request.
* @throws SipException - INVALID_STATE if the request can not be set,
* because of wrong state (in SipClientConnection) or the system
* has already
* got the 200 OK response (even if not read with receive() method).
* INVALID_OPERATION if CANCEL method can not be applied to the current
* request method.
* @see javax.microedition.sip.SipClientConnection#initCancel()
*/
public SipClientConnection initCancel() throws SipException {
if ((state != PROCEEDING) || !enableInitCancel) {
throw new SipException("the CANCEL request can not be initialized,"
+ " because of wrong state.",
SipException.INVALID_STATE);
}
// JSR180: The CANCEL request will be built according to
// the original INVITE request within this connection.
// Therefore building CANCEL request from not-INVITE
// original request is not allowed.
if (!request.getMethod().equals(Request.INVITE)) {
throw new SipException("The method of original request " +
"is not INVITE",
SipException.INVALID_OPERATION);
}
// init the cancel request
Request cancelRequest = clientTransaction.createCancel();
SipClientConnection sipClientConnectionCancel =
new SipClientConnectionImpl(
cancelRequest,
sipConnectionNotifier,
userSipURI);
// stackConnector.clientConnectionList.addElement(
// sipClientConnectionCancel);
return sipClientConnectionCancel;
}
/**
* Receives SIP response message. The receive method will update the
* SipClientConnection with the last new received response.
* If no message is received the method will block until something is
* received or specified amount of time has elapsed.
* @param timeout - the maximum time to wait in milliseconds.
* 0 = do not wait, just poll
* @return Returns true if response was received. Returns false if
* the given timeout elapsed and no response was received.
* @throws SipException - INVALID_STATE if the receive can not be
* called because of wrong state.
* @throws IOException - if the message could not be received or
* because of network failure
*/
public boolean receive(long timeout) throws SipException, IOException {
if ((state != PROCEEDING) && (state != COMPLETED)) {
throw new SipException(SipException.INVALID_STATE);
}
// check for a response
if (responses.isEmpty()) {
// wait for a response during the time specified by the timeout
if (timeout != 0) {
synchronized (this) {
try {
// listeningThread.sleep(timeout);
wait(timeout);
} catch (InterruptedException ie) {
}
}
}
}
if (responses.isEmpty()) {
return false; // queue is empty
}
// get first response from queue
IncomingQueueElement incomingElement =
(IncomingQueueElement) responses.firstElement();
responseReceived = incomingElement.getResponse();
responses.removeElementAt(0); // remove from queue
useRequest = false;
// change client transaction if need
if (incomingElement.containsClientTransaction()) {
clientTransaction = incomingElement.getClientTransaction();
}
if ((responseReceived.getStatusCode() / 100 == 2) &&
(state == COMPLETED)) { // multiple responses
// change dialog
sipDialog = new SipDialogImpl(clientTransaction.getDialog(),
sipConnectionNotifier,
classSecurityToken);
// transaction is INVITE, checked in
// ClientTransaction.isMessageTransOrMult()
((SipDialogImpl) sipDialog).setWaitForBye(true);
((SipDialogImpl) sipDialog).setState(SipDialog.CONFIRMED);
}
changeDialogState();
changeClientConnectionState();
return true;
}
/**
* Sets the listener for incoming responses. If a listener is
* already set it
* will be overwritten. Setting listener to null will remove the current
* listener.
* @param sccl - reference to the listener object. Value null will remove
* the existing listener.
* @throws IOException - if the connection is closed.
*/
public void setListener(SipClientConnectionListener sccl)
throws IOException {
if (!connectionOpen) {
throw new IOException("The Connection has been closed!");
}
this.sipClientConnectionListener = sccl;
}
/**
* Enables the refresh on for the request to be sent. The method return a
* refresh ID, which can be used to update or stop the refresh.
* @param srl - callback interface for refresh events, if this is null the
* method returns 0 and refresh is not enabled.
* @return refresh ID. If the request is not refreshable returns 0.
* @throws SipException - INVALID_STATE if the refresh can not be enabled
* in this state.
*/
public int enableRefresh(SipRefreshListener srl) throws SipException {
if (state != INITIALIZED) {
throw new SipException("can not enable the refresh,"
+ " because of wrong state.",
SipException.INVALID_STATE);
}
if (srl == null) {
return 0;
}
String method = request.getMethod();
if (!method.equals(Request.REGISTER) &&
!method.equals(Request.SUBSCRIBE) &&
!method.equals(Request.PUBLISH)) {
return 0;
}
// understand the refresh listener thing
sipRefreshListener = srl;
int taskID = RefreshManager
.getInstance()
.createRefreshTask(
request,
sipConnectionNotifier,
sipRefreshListener,
this);
refreshID = String.valueOf(taskID);
return taskID;
}
/**
* Sets credentials for possible digest authentication.
* The application can set multiple credential triplets
* (username, password, realm) for one SipClientConnection.
* The username and password are specified for certain protection domain,
* which is defined by the realm parameter.
* The credentials can be set:
* before sending the original request in Initialized state.
* The API implementation caches the credentials for later use.
* when 401 (Unauthorized) or 407 (Proxy Authentication Required) response
* is received in the Unauthorized state. The API implementation uses the
* given credentials to re-originate the request with proper authorization
* header. After that the SipClientConnection will be in Proceeding state.
* @param username username (for this protection domain)
* @param password user password (for this protection domain)
* @param realm defines the protection domain
* @throws SipException INVALID_STATE if the credentials can not
* be set in this state.
* @throws NullPointerException - if the username, password or realm is null
*/
public void setCredentials(String username, String password, String realm)
throws SipException {
if (state != INITIALIZED && state != UNAUTHORIZED) {
throw new SipException("can not set the credentials, " +
"because of wrong state.",
SipException.INVALID_STATE);
}
if (username == null || password == null || realm == null) {
throw new NullPointerException();
}
Credentials credential = new Credentials(username, password, realm);
credentials.addElement(credential);
// reoriginate the requests with the proper credentials
if (state == UNAUTHORIZED) {
reoriginateRequest();
}
}
/**
* Sends the SIP message. Send must also close the OutputStream
* if it was opened.
* @throws IOException if the message could not be sent or because
* of network failure
* @throws InterruptedIOException if a timeout occurs while
* either trying
* to send the message or if this Connection object is closed
* during this send operation
* @throws SipException INVALID_STATE if the message cannot be sent
* in this state. <br> INVALID_MESSAGE there was an error
* in message format
*/
public void send()
throws IOException, InterruptedIOException, SipException {
sendRequestImpl(false);
}
/**
* This function is an implementation for send(). It sends the SIP message.
* Send must also close the OutputStream if it was opened.
* @param isRefreshRequest a flag indicating if the request to be sent
* is a refreshing request (isRefreshRequest is true) or it is a regular
* request (isRefreshRequest is false).
* @throws IOException if the message could not be sent or because
* of network failure
* @throws InterruptedIOException if a timeout occurs while
* either trying
* to send the message or if this Connection object is closed
* during this send operation
* @throws SipException INVALID_STATE if the message cannot be sent
* in this state. <br> INVALID_MESSAGE there was an error
* in message format
*/
private void sendRequestImpl(boolean isRefreshRequest) throws IOException,
InterruptedIOException, SipException {
if (state != STREAM_OPEN && state != INITIALIZED) {
throw new SipException("can not send the request, " +
"because of wrong state.",
SipException.INVALID_STATE);
}
if (!connectionOpen) {
throw new IOException("The Connection has been closed!");
}
if (contentOutputStream != null) {
contentOutputStream.setOpen(false);
request.setContent(
contentOutputStream.getByteArrayOutputStream().toByteArray(),
(ContentTypeHeader) request.getHeader(Header.CONTENT_TYPE));
contentOutputStream = null;
}
// Check mandatory headers (RFC3261, 8.1.1)
String[] mandatoryHeaders = {
Header.TO, Header.FROM, Header.CSEQ,
Header.CALL_ID, Header.MAX_FORWARDS, Header.VIA};
Vector mandatoryList = new Vector();
// add header names for all types of requests
for (int i = 0; i < mandatoryHeaders.length; i++) {
mandatoryList.addElement(mandatoryHeaders[i]);
}
String method = request.getMethod();
// RFC 3515, p. 6:
// A REFER request MUST contain exactly one Refer-To header field value.
if (method.equals(Request.REFER)) {
mandatoryList.addElement(Header.REFER_TO);
}
// RFC3265, p. 15:
// NOTIFY requests MUST contain a "Subscription-State" header with
// a value of "active", "pending", or "terminated".
if (method.equals(Request.NOTIFY)) {
mandatoryList.addElement(Header.SUBSCRIPTION_STATE);
}
for (int i = 0; i < mandatoryList.size(); i++) {
if (request.getHeader(
(String) mandatoryList.elementAt(i)) == null) {
throw new SipException("Header " +
(String) mandatoryList.elementAt(i) +
" is missed", SipException.INVALID_STATE);
}
}
// add "tag" parameter to "From" header if necessary
FromHeader fromHeader = (FromHeader) request.getHeader(Header.FROM);
// it is not null - please see above
if (!fromHeader.hasTag()) { // no "tag" parameter
fromHeader.setTag(StackConnector.generateTag());
}
// Request-URI
// Fix added per NIST cvs digest dated July 3, 2005
// RFC 3261, 10.2:
// Request-URI: ... The "userinfo" and "@" components of the
// SIP URI MUST NOT be present.
if (method.equals(Request.REGISTER)) {
Address reqUriAddress = null;
try {
reqUriAddress =
StackConnector.addressFactory.createAddress(
requestURI.toString());
if (reqUriAddress.isSIPAddress()) {
((SipURI) reqUriAddress.getURI()).removeUser();
requestURI = reqUriAddress.getURI();
}
request.setRequestURI(requestURI);
} catch (ParseException pe) {
throw new SipException(
"The system property UserSipUri can not be " +
"parsed, check the syntax",
SipException.INVALID_OPERATION);
}
}
// Check that the parameters specified in Via header match
// those which were set in sipConnectionNotifier.
ViaHeader requestViaHeader =
(ViaHeader) request.getViaHeaders().getFirst();
int viaPort = requestViaHeader.getPort();
int localPort = sipConnectionNotifier.getLocalPort();
String transport = requestViaHeader.getTransport();
if (localPort != viaPort) {
throw new IOException("Via port (" + viaPort + ") doesn't " +
"match the listener's port (" + localPort +
")!");
}
SipConnectionNotifierImpl notifierImpl =
(SipConnectionNotifierImpl) sipConnectionNotifier;
if (!notifierImpl.getSipProvider().getListeningPoint().
getTransport().equalsIgnoreCase(transport)) {
throw new IOException("Via transport doesn't match " +
"the listener's transport!");
}
// RFC 3903 (PUBLISH method), p. 5:
// The Record-Route header field has no meaning in PUBLISH
// requests or responses, and MUST be ignored if present.
if (method.equals(Request.PUBLISH)) {
request.removeHeader(Header.RECORD_ROUTE);
}
if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
Logging.report(Logging.INFORMATION, LogChannels.LC_JSR180,
"Request to be sent : " + request);
}
// System.out.println(">>> Request to be sent: \n" + request);
// Creates the Nist-Siplite client Transaction for this
// request
try {
clientTransaction =
((SipConnectionNotifierImpl) sipConnectionNotifier).
getSipProvider().getNewClientTransaction(request);
} catch (TransactionUnavailableException tue) {
throw new SipException("Cannot create a new Client " +
" Transaction for this request",
SipException.TRANSACTION_UNAVAILABLE);
} catch (IllegalArgumentException iae) {
throw new SipException("SCC.send(): IAE occured (1): " +
iae.getMessage(), SipException.GENERAL_ERROR);
} catch (NullPointerException npe) {
throw new SipException("SCC.send(): NPE occured (1): " +
npe.getMessage(), SipException.GENERAL_ERROR);
}
// Set the application data so that when the response comes in,
// it will retrieve this SipClientConnection
clientTransaction.setApplicationData(this);
// Send the request
if (method.equals(Request.ACK)) {
Dialog dlg = clientTransaction.getDialog();
if (dlg.isServer()) {
// Switch from server side to client side for re-invite
dlg.addTransaction(clientTransaction);
}
try {
dlg.sendAck(request);
} catch (IllegalArgumentException iae) {
throw new SipException("SCC.send(): can't send ACK: " + iae,
SipException.GENERAL_ERROR);
}
state = COMPLETED;
return;
} else if (sipDialog != null && !isRefreshRequest) {
// if (method.equals(Request.BYE) ||
// method.equals(Request.NOTIFY)) {
// If the request is a BYE, we must send it with the dialog
// If the dialog is established, all further requests should
// be sent within it.
SipDialogImpl sipDialogImpl = (SipDialogImpl) sipDialog;
if (method.equals(Request.SUBSCRIBE) ||
method.equals(Request.REFER)) {
// Add a subscription
sipDialogImpl.addSubscription(
new Subscription(sipDialogImpl.getDialog(), request));
} else if (method.equals(Request.INVITE)) {
sipDialogImpl.setWaitForBye(true);
}
sipDialogImpl.dialog.sendRequest(clientTransaction);
state = PROCEEDING;
return;
} else {
clientTransaction.sendRequest();
}
// An INVITE, SUBSCRIBE or REFER has been sent, so a dialog need to
// be created.
if (stackConnector.getSipStack().isDialogCreated(method) &&
!isRefreshRequest) {
sipDialog = new SipDialogImpl(clientTransaction.getDialog(),
sipConnectionNotifier,
classSecurityToken);
SipDialogImpl sipDialogImpl = (SipDialogImpl) sipDialog;
sipDialogImpl.setRefreshID(refreshID);
sipDialogImpl.setSipClientConnectionListener(
sipClientConnectionListener);
stackConnector.sipDialogList.addElement(sipDialog);
// Add a subscription
if (!method.equals(Request.INVITE)) {
sipDialogImpl.addSubscription(
new Subscription(sipDialogImpl.getDialog(), request));
} else {
sipDialogImpl.setWaitForBye(true);
}
}
// If the method is a REGISTER it means that we are using a
// proxy so we put put the route of the proxy in the router
if (request.getMethod().equals(Request.REGISTER)) {
SipURI sipURI = (SipURI) request.getRequestURI();
int requestPort = sipURI.getPort();
if (requestPort == -1) { // get port from sipConnectionNotifier
requestPort = sipConnectionNotifier.getLocalPort();
}
String requestTransport = sipURI.getTransportParam();
if ((requestTransport == null) ||
(requestTransport.length() < 1)) {
// get transport from sipConnectionNotifier
requestTransport =
((SipConnectionNotifierImpl) sipConnectionNotifier).
getSipProvider().getListeningPoint().getTransport();
}
stackConnector.sipStack.getRouter().setOutboundProxy(
sipURI.getHost()
+ ":" + requestPort
+ "/"
+ requestTransport);
// outboundProxy = true;
}
// Refresh must be scheduled after receiving a response,
// refer the comments at the end of notifyResponseReceived().
// scheduleRefresh(request.getMethod(), request, false);
state = PROCEEDING;
}
/**
* Sets header value in SIP message. If the header does not exist
* it will be added to the message, otherwise the existing header is
* overwritten. If multiple header field values exist the topmost is
* overwritten. The implementations MAY restrict the access to some headers
* according to RFC 3261.
* @param name - name of the header, either in full or compact form.
* RFC 3261 p.32
* @param value - the header value
* @throws SipException - INVALID_STATE if header can not be set in
* this state. <br> INVALID_OPERATION if the system does not allow to set
* this header.
* @throws IllegalArgumentException - MAY be thrown if the header or
* value is invalid
*/
public void setHeader(String name, String value) throws SipException,
IllegalArgumentException {
if (state != INITIALIZED) {
throw new SipException("the Header can not be set,"
+ " because of wrong state.",
SipException.INVALID_STATE);
}
if (name == null) {
throw new
IllegalArgumentException("The header name can not be null");
}
if (value == null) {
throw new
IllegalArgumentException("The header value can not be null");
}
Header header = null;
try {
header = StackConnector.headerFactory.createHeader(name, value);
// name = header.getName(); // The source name might be expanded
} catch (ParseException pe) {
throw new IllegalArgumentException(pe.getMessage());
}
if (header == null) {
throw new IllegalArgumentException("null header!");
}
// Response doesn't exist in the INITIALIZED state, so here
// it's clear which message (request or response) to use.
request.attachHeader(header, true, true);
/*
if (request.getHeader(name) == null) {
request.addHeader(header);
} else {
request.attachHeader(header, true, true);
}
*/
}
/**
* Adds a header to the SIP message. If multiple header field values exist
* the header value is added topmost of this type of headers.
* The implementations MAY restrict the access to some headers
* according to RFC 3261.
* @param name - name of the header, either in full or compact form.
* RFC 3261 p.32
* @param value - the header value
* @throws SipException - INVALID_STATE if header can not be added in
* this state. <br> INVALID_OPERATION if the system does not allow to add
* this header.
* @throws IllegalArgumentException - MAY be thrown if the header or
* value is invalid
*/
public void addHeader(String name, String value) throws SipException,
IllegalArgumentException {
if (state != INITIALIZED) {
throw new SipException("the Header can not be add,"
+ " because of wrong state.",
SipException.INVALID_STATE);
}
if (name == null) {
throw new
IllegalArgumentException("The header name can not be null");
}
if (value == null) {
throw new
IllegalArgumentException("The header value can not be null");
}
Header header = null;
try {
header = StackConnector.headerFactory.createHeader(name, value);
} catch (ParseException pe) {
throw new IllegalArgumentException("The header can not be created,"
+ " check if it is correct");
}
request.addHeader(header);
}
/**
* Reomves header from the SIP message. If multiple header field
* values exist
* the topmost is removed.
* The implementations MAY restrict the access to some headers
* according to RFC 3261.
* If the named header is not found this method does nothing.
* @param name - name of the header to be removed, either int
* full or compact form RFC 3261 p.32.
* @throws SipException - INVALID_STATE if header can not be removed in
* this state. <br> INVALID_OPERATION if the system does not allow to remove
* this header.
*/
public void removeHeader(String name) throws SipException,
IllegalArgumentException {
if (state != INITIALIZED) {
throw new SipException("the Header can not be removed,"
+ " because of wrong state.",
SipException.INVALID_STATE);
}
if (name == null) {
throw new
IllegalArgumentException("The header name can not be null");
}
request.removeHeader(name, true);
}
/**
* Gets the header field value(s) of specified header type
* @param name - name of the header, either in full or compact form.
* RFC 3261 p.32
* @return array of header field values (topmost first), or null if the
* current message does not have such a header or the header is for other
* reason not available (e.g. message not initialized).
*
* Javadoc is not clear on whether this method should be applied to
* request or response. The NIST implementation uses response only to
* calculate the size; but that seems to be wrong.
*
*/
public String[] getHeaders(String name) {
Message currentMessage = useRequest ? (Message) request :
(Message) responseReceived;
if (currentMessage == null) {
// There 'request' may absent in the CREATED state
return null;
}
HeaderList nameList = currentMessage.getHeaderList(name);
if (nameList == null) {
return null;
}
int size = nameList.size();
if (size < 1) {
return null;
}
String[] headerValues = new String[size];
for (int count = 0; count < size; count++) {
headerValues[count] =
((Header) nameList.elementAt(count)).getHeaderValue();
}
return headerValues;
}
/**
* Gets the header field value of specified header type.
* @param name - name of the header type, either in full or compact form.
* RFC 3261 p.32
* @return topmost header field value, or null if the
* current message does not have such a header or the header is for other
* reason not available (e.g. message not initialized).
*
* Javadoc is not clear on whether this method should be applied to
* request or response. The NIST implementation uses response; but that
* seems to be wrong.
*
*/
public String getHeader(String name) {
Message currentMessage = useRequest ? (Message) request :
(Message) responseReceived;
if (currentMessage == null) {
// There 'request' may absent in the CREATED state
return null;
}
Header nameHeader = currentMessage.getHeader(name);
if (nameHeader == null) {
return null;
}
return nameHeader.getHeaderValue();
}
/**
* Gets the SIP method. Applicable when a message has been
* initialized or received.
* @return SIP method name REGISTER, INVITE, NOTIFY, etc. Returns null if
* the method is not available.
*/
public String getMethod() {
// another implementation returns null in terminated state; so do we
if (null == request || TERMINATED == state) {
return null;
} else {
return request.getMethod();
}
}
/**
* Gets Request-URI. Available when SipClientConnection is in Initialized
* state or when SipServerConnection is in Request Received state.
* Built from the original URI given in Connector.open().
* See RFC 3261 p.35 (8.1.1.1 Request-URI)
* @return Request-URI of the message. Returns null if the Request-URI
* is not available.
*/
public String getRequestURI() {
if (state != INITIALIZED || request == null) {
return null;
} else {
return request.getRequestURI().toString();
}
}
/**
* Gets SIP response status code. Available when SipClientConnection is in
* Proceeding or Completed state or when SipServerConnection is in
* Initialized state.
* @return status code 1xx, 2xx, 3xx, 4xx, ... Returns 0 if the status code
* is not available.
*/
public int getStatusCode() {
if (responseReceived == null) {
return 0;
} else {
return responseReceived.getStatusCode();
}
}
/**
* Gets SIP response reason phrase. Available when SipClientConnection is in
* Proceeding or Completed state or when SipServerConnection is in
* Initialized state.
* @return reason phrase. Returns null if the reason phrase is
* not available.
*/
public String getReasonPhrase() {
if (state != PROCEEDING && state != COMPLETED && state != UNAUTHORIZED
|| responseReceived == null) {
return null;
} else {
return responseReceived.getReasonPhrase();
}
}
/**
* Returns the current SIP dialog. This is available when the SipConnection
* belongs to a created SipDialog and the system has received (or sent)
* provisional (101-199) or final response (200).
* @return SipDialog object if this connection belongs to a dialog,
* otherwise returns null.
*/
public SipDialog getDialog() {
if (sipDialog != null) {
byte dialogState = sipDialog.getState();
if ((dialogState != SipDialog.EARLY) &&
(dialogState != SipDialog.CONFIRMED)) {
return null;
}
}
return sipDialog;
}
/**
* Returns InputStream to read SIP message body content.
* @return InputStream to read body content
* @throws java.io.IOException - if the InputStream can not be opened,
* because of an I/O error occurred.
* @throws SipException - INVALID_STATE the InputStream can not be opened
* in this state (e.g. no message received).
*/
public InputStream openContentInputStream() throws IOException,
SipException {
String errStateMsg = "the content input stream can not be open, " +
"because of wrong state.";
if ((state != COMPLETED) && (state != PROCEEDING)) {
throw new SipException(errStateMsg, SipException.INVALID_STATE);
}
if (!connectionOpen) {
throw new IOException("The Connection has been closed!");
}
if (responseReceived == null) {
// Although openContentInputStream() is called in the correct
// user-level state, we may have a situation when this method
// is called before the response was received. In this case
// the internal state of SCC is invalid and the proper exception
// to throw is SipException.INVALID_STATE.
throw new SipException(errStateMsg, SipException.INVALID_STATE);
}
ContentLengthHeader contentLengthHeader =
responseReceived.getContentLengthHeader();
if (contentLengthHeader == null) {
throw new
IOException("Response contains no content length header.");
}
int bodyLength = contentLengthHeader.getContentLength();
if (bodyLength == 0) {
throw new IOException("Response's body has zero length.");
}
byte[] buf = responseReceived.getRawContent();
if (buf == null) { // body is empty
throw new IOException("Body of SIP response is empty.");
}
contentInputStream = new ByteArrayInputStream(buf);
return contentInputStream;
}
/**
* Returns OutputStream to fill the SIP message body content.
* When calling close() on OutputStream the message will be sent
* to the network. So it is equivalent to call send(). Again send() must
* not be called after closing the OutputStream, since it will throw
* Exception because of calling the method in wrong state.
* Before opening OutputStream the Content-Length and Content-Type headers
* has to se set. If not SipException.UNKNOWN_LENGTH or
* SipException.UNKNOWN_TYPE will be thrown respectively.
* @return OutputStream to write body content
* @throws IOException if the OutputStream can not be opened,
* because of an I/O error occurred.
* @throws SipException INVALID_STATE the OutputStream can not be opened
* in this state (e.g. no message initialized).
* UNKNOWN_LENGTH Content-Length header not set.
* UNKNOWN_TYPE Content-Type header not set.
*/
public OutputStream openContentOutputStream() throws IOException,
SipException {
if (state != INITIALIZED) {
throw new SipException("the content output stream can not be open,"
+ " because of wrong state.",
SipException.INVALID_STATE);
}
if (request.getHeader(Header.CONTENT_TYPE) == null) {
throw new SipException(
"Content-Type unknown, set the content-type "
+ "header first",
SipException.UNKNOWN_TYPE);
}
if (request.getHeader(Header.CONTENT_LENGTH) == null) {
throw new SipException("Content-Length unknown, set the "
+ "content-length header first",
SipException.UNKNOWN_LENGTH);
}
if (!connectionOpen) {
throw new IOException("The Connection has been closed!");
}
contentOutputStream = new SDPOutputStream(this);
state = STREAM_OPEN;
return contentOutputStream;
}
/**
* Close the clientconnection.
* @see javax.microedition.io.Connection#close()
*/
public void close() throws IOException {
responses.removeAllElements(); // clear queue
// cleanup
if (isNotifierCreated && (sipConnectionNotifier != null)) {
// try {
sipConnectionNotifier.close();
// } catch (IOException exc) { // ignore
// }
}
// Removing the connection from the connection list held by
// the stackConnector
// StackConnector.getInstance().
// clientConnectionList.removeElement(this);
connectionOpen = false;
listeningThread = null;
state = TERMINATED;
}
/**
* Reoriginate the request with the proper credentials
*/
private void reoriginateRequest() {
// clear dialog
if (sipDialog != null) {
if (sipDialog.getState() == SipDialog.TERMINATED) {
sipDialog = null;
}
}
DigestClientAuthentication authentication =
new DigestClientAuthentication(credentials);
// Reoriginate the request with the proper credentials
Request newRequest = authentication.createNewRequest(
stackConnector.sipStack,
this.request,
this.responseReceived,
this.countReoriginateRequest);
if (newRequest != null) {
this.countReoriginateRequest++;
this.request = newRequest;
// The request has been reinitialized...
state = INITIALIZED;
useRequest = true;
// ...so it is sent out
try {
this.send();
} catch (IOException ioe) {
// ioe.printStackTrace();
}
}
}
/**
* Change the state of this Client Connection due to an incoming response.
*/
private void changeClientConnectionState() {
// Change the Client Connection state
// If it's a trying, the state is PROCEEDING
if (responseReceived.getStatusCode() / 100 == 1
&& state == PROCEEDING) {
state = PROCEEDING;
}
// If it's a 401 or 407, the state is UNAUTHORIZED
else if (state == PROCEEDING &&
(responseReceived.getStatusCode() == Response.UNAUTHORIZED ||
responseReceived.getStatusCode() ==
Response.PROXY_AUTHENTICATION_REQUIRED)) {
state = UNAUTHORIZED;
}
// Otherwise this is COMPLETED
else {
state = COMPLETED;
}
}
/**
* Change the state of the dialog due to an incoming response.
*/
private void changeDialogState() {
// Change the dialog state
// REGISTER method doesn't establish a dialog, so sipDialog
// should be null in this case.
// IMPL_NOTE: check if it is really null as supposed to be.
if (sipDialog == null) {
return;
}
SipDialogImpl sipDialogImpl = (SipDialogImpl) sipDialog;
String method = responseReceived.getCSeqHeader().getMethod();
if (!stackConnector.getSipStack().allowDialogStateChange(method)) {
return;
}
int statusCode = responseReceived.getStatusCode();
if (statusCode / 100 == 2) {
// RFC 3261, section 13.2.2.4:
// If the dialog identifier in the 2xx response matches the dialog
// identifier of an existing dialog, the dialog MUST be transitioned
// to the "confirmed" state.
// sipDialog cannot be null here - this check was done
// at the beginning of the function
sipDialogImpl.setState(SipDialog.CONFIRMED);
sipDialogImpl.setDialogID(responseReceived.getDialogId(false));
if (statusCode == 200) {
if (method.equals(Request.NOTIFY)) {
sipDialogImpl.handleNotify(request, null,
responseReceived.getDialogId(false));
return;
}
// handle the un-Subscribe state
if (method.equals(Request.SUBSCRIBE)) {
ExpiresHeader expiresHeader = (ExpiresHeader)
responseReceived.getHeader(ExpiresHeader.NAME);
// According to RFC3265, p. 6:
// "200-class responses to SUBSCRIBE requests also
// MUST contain an "Expires" header."
//
// But we have to handle a situation when it is missing.
// RFC 3265, p. 8:
// "200-class responses indicate that the subscription
// has been accepted".
//
// So, the dialog state is set to CONFIRMED even if
// no Expires header is present.
if (expiresHeader != null &&
expiresHeader.getExpires() == 0) { // unsubscribe
sipDialogImpl.terminateIfNoSubscriptions();
} else { // subscribe confirmation
sipDialogImpl.setState(SipDialog.CONFIRMED);
sipDialogImpl.setDialogID(
responseReceived.getDialogId(false));
}
} else if (method.equals(Request.BYE)) {
// IMPL_NOTE: check the RFC 3261. Probably we have to terminate
// the dialog for the responses other than 200 OK.
sipDialogImpl.setWaitForBye(false);
sipDialogImpl.terminateIfNoSubscriptions();
}
}
} else if (statusCode / 100 == 1) {
// provisional response
if (statusCode != 100) {
if (sipDialog.getState() == SipDialogImpl.INITIALIZED) {
// switch to EARLY state
sipDialogImpl.setState(SipDialog.EARLY);
}
/*
* Add a check to verify if it is reliable provisional
* response.
*/
Header requireHeader =
(ParameterLessHeader) responseReceived.getHeader(
Header.REQUIRE);
if (requireHeader != null) {
sipDialogImpl.isReliableProvReceived =
Header.isReliableTagPresent(
requireHeader.getHeaderValue());
}
// RFC 3261, 12.1:
// Within this specification, only 2xx and 101-199
// responses with a To tag ... will establish a dialog.
// set the dialog ID
sipDialogImpl.setDialogID(responseReceived.getDialogId(false));
}
} else { // another response code - switch to TERMINATED state
// Remove the subscription if any
sipDialogImpl.removeSubscription(response);
// JSR180 - not from CONFIRMED state
if (sipDialog.getState() != SipDialog.CONFIRMED) {
if (method.equals(Request.INVITE)) {
sipDialogImpl.setWaitForBye(false);
}
sipDialogImpl.terminateIfNoSubscriptions();
}
}
}
/**
* Updates and sends the request from the refresh.
* @param updatedRequest the updated request
* @throws IOException if the message could not be sent or because
* of network failure
* @throws InterruptedIOException if a timeout occurs while
* either trying
* to send the message or if this Connection object is closed
* during this send operation
* @throws SipException INVALID_STATE if the message cannot be sent
* in this state. <br> INVALID_MESSAGE there was an error
* in message format
*/
protected void updateAndSendRequestFromRefresh(Request updatedRequest)
throws IOException, InterruptedIOException, SipException {
request = updatedRequest;
state = INITIALIZED;
// If the request to be refreshed creates a dialog (i.e. SUBSCRIBE),
// the next request will be sent within a dialog using the rules for
// sending in-dialog requests. To avoid it, isRefreshRequest parameter
// is used.
sendRequestImpl(true);
}
/**
* Updates the request and calls openContentOutputStream()
* to fill the new message body.
* @param updatedRequest the updated request
* @return OutputStream to write body content
* @throws IOException if the OutputStream can not be opened,
* because of an I/O error occurred.
* @throws SipException INVALID_STATE the OutputStream can not be opened
* in this state (e.g. no message initialized).
* UNKNOWN_LENGTH Content-Length header not set.
* UNKNOWN_TYPE Content-Type header not set.
*/
protected OutputStream updateRequestAndOpenOutputStream(
Request updatedRequest) throws IOException, SipException {
request = updatedRequest;
state = INITIALIZED;
return openContentOutputStream();
}
/**
* Gets the current request.
* @return the current request
*/
public Request getRequest() {
return request;
}
/**
* Return the Call Identifier of this client connection
* If there is no call id yet, this method return an empty String
* @return the call Identifier
*/
protected String getCallIdentifier() {
if (request == null) {
return "";
}
return request.getCallIdentifier();
}
/**
* The stack connector notifies this class when it receive NOTIFY
* request matching the previous SUBSCRIBE or REFER request.
* @param notifyRequest NOTIFY request to process
*/
public void handleMatchingNotify(Request notifyRequest) {
SipDialogImpl sipDialogImpl = (SipDialogImpl) sipDialog;
int state = sipDialogImpl.getState();
SubscriptionStateHeader ssh = (SubscriptionStateHeader)
notifyRequest.getHeader(Header.SUBSCRIPTION_STATE);
boolean isUnsubscribe = (ssh != null && ssh.isTerminated());
if (state == SipDialogImpl.INITIALIZED ||
(state == SipDialogImpl.CONFIRMED && isUnsubscribe)) {
String dialogId = notifyRequest.getDialogId(false);
// sipDialogImpl.setState(isUnsubscribe ?
// SipDialog.TERMINATED : SipDialog.CONFIRMED);
sipDialogImpl.setDialogID(dialogId);
sipDialogImpl.handleNotify(notifyRequest, null, dialogId);
}
}
/**
* The stack connector notifies this class when it receive a new response.
* @param response the repsonse event to be propagated
* @param inputClientTransaction client transaction of this response
*/
protected void notifyResponseReceived(Response response,
ClientTransaction
inputClientTransaction) {
// System.out.println(">>> Response received : \n" + response);
int statusCode = response.getStatusCode();
int statusGroup = statusCode / 100;
boolean ignoreResponse = false;
if (state == COMPLETED) { // 2xx responses only
if (statusGroup != 2) {
ignoreResponse = true;
}
} else if (state == PROCEEDING) {
// If there is some credentials and the client connection is in an
// UNAUTHORIZED state, the request is reoriginate automatically
if (credentials.size() > 0 &&
((statusCode == Response.UNAUTHORIZED) ||
(statusCode == Response.PROXY_AUTHENTICATION_REQUIRED))) {
this.responseReceived = response;
if (sipDialog != null) {
((SipDialogImpl) sipDialog).setState(SipDialog.TERMINATED);
}
reoriginateRequest();
ignoreResponse = true;
}
} else { // not COMPLETED and PROCEEDING
ignoreResponse = true;
}
// check the queue size
if (responses.size() > MAX_NUM_RESPONSES) {
if (Logging.REPORT_LEVEL <= Logging.ERROR) {
Logging.report(Logging.ERROR, LogChannels.LC_JSR180,
"Queue of incoming SIP packets is overflow");
}
ignoreResponse = true;
}
if (ignoreResponse) { // ignore response
return;
}
IncomingQueueElement incomingElement =
new IncomingQueueElement(response, inputClientTransaction);
responses.addElement(incomingElement); // put to queue
this.response = response;
if (state == PROCEEDING) {
if (statusGroup == 1) {
// provisional response
// JSR180: SipClientConnection: initCancel: The method is
// available when a provisional response
// has been received.
enableInitCancel = true;
} else {
// All responses from 200-699 are final
// JSR180: SipClientConnection: initCancel:
// Throws: SipException - INVALID_STATE if ... or the system
// has already got the 200 OK response (even if not read with
// receive() method).
enableInitCancel = false;
}
}
if (response.getCSeqHeaderNumber() == request.getCSeqHeaderNumber()) {
synchronized (this) {
notify();
}
// We notify the listener that a response has been received
if (sipClientConnectionListener != null) {
sipClientConnectionListener.notifyResponse(this);
}
}
String method = response.getCSeqHeader().getMethod();
if (method.equals(Request.PUBLISH)) {
// RFC 3903, p. 6:
// When updating previously published event state, PUBLISH
// requests MUST contain a single SIP-If-Match header field
// identifying the specific event state that the request is
// refreshing, modifying or removing. This header field MUST
// contain a single entity-tag that was returned by the ESC
// in the SIP-ETag header field of the response to a previous
// publication.
Header hEtag = response.getHeader(Header.SIP_ETAG);
if (hEtag != null) {
Header hIfMatch = request.getHeader(Header.SIP_IF_MATCH);
if (hIfMatch == null) {
Exception ex = null;
// Create SIP_IF_MATCH header
try {
hIfMatch = StackConnector.headerFactory.createHeader(
Header.SIP_IF_MATCH, hEtag.getHeaderValue());
request.addHeader(hIfMatch);
} catch (NullPointerException npe) {
ex = npe;
} catch (ParseException pe) {
ex = pe;
} catch (SipException se) {
ex = se;
}
if (ex != null) {
if (Logging.REPORT_LEVEL <= Logging.ERROR) {
Logging.report(Logging.ERROR,
LogChannels.LC_JSR180,
"scc.notifyResponseReceived(): can't create " +
"SIP-If-Match header:" + ex);
ex.printStackTrace();
}
}
} else {
hIfMatch.setHeaderValue(hEtag.getHeaderValue());
}
request.removeHeader(Header.SIP_ETAG);
} else {
if (Logging.REPORT_LEVEL <= Logging.WARNING) {
Logging.report(Logging.WARNING, LogChannels.LC_JSR180,
"scc.notifyResponseReceived(): response to PUBLISH " +
"doesn't contain SIP-Etag header!");
}
}
}
// Schedule the refresh if required (i.e., if the method is
// refreshable). Refresh time is taken from the response,
// either from Expires header or from "expires" parameter
// of Contact header as described in RFCs 3261, section 10.2.4
// and RFC 3265, section 3.1.1.
// If a listener for refresh event has been set, it is notified.
if (statusCode == Response.OK) {
scheduleRefresh(method);
}
// Notify the listener
if (refreshID != null) {
sipRefreshListener.refreshEvent(Integer.parseInt(refreshID),
statusCode,
response.getReasonPhrase());
}
}
/**
* Gets the current state of connection.
* @return the current state
*/
public int getState() {
return state;
}
/**
* Gets the current client transaction.
* @return the current client transaction
*/
protected ClientTransaction getClientTransaction() {
return clientTransaction;
}
/**
* Gets the current sip stack.
* @return the current sip stack
*/
protected SipStack getSipStack() {
return stackConnector.getSipStack();
}
/**
* Gets the assigned SipConnectionNotifier.
* @return the current SipConnectionNotifier
*/
protected SipConnectionNotifier getSipConnectionNotifier() {
return sipConnectionNotifier;
}
/**
* Gets the response.
* @return the response instance
*/
protected Response getResponse() {
return response;
}
/**
* Clears the current response.
*/
protected void clearResponse() {
this.response = null;
}
/**
* Schedules refreshing of the request if required.
* @param method SIP method of the message
*/
private void scheduleRefresh(String method) {
if (sipRefreshListener == null) {
return;
}
if (! (method.equals(Request.REGISTER) ||
method.equals(Request.SUBSCRIBE) ||
method.equals(Request.PUBLISH))) {
return;
}
// Remove the body of the message in case if the request is PUBLISH,
// see RFC 3903, p. 7 (section 4.1):
// +-----------+-------+---------------+---------------+
// | Operation | Body? | SIP-If-Match? | Expires Value |
// +-----------+-------+---------------+---------------+
// | Initial | yes | no | > 0 |
// | Refresh | no | yes | > 0 |
// | Modify | yes | yes | > 0 |
// | Remove | no | yes | 0 |
// +-----------+-------+---------------+---------------+
if (method.equals(Request.PUBLISH)) {
request.removeContent();
}
// If the expires is set, the refresh is scheduled for the
// duration of the expires
int expires, minExpires = Integer.MAX_VALUE;
// RFC 3265, p. 6:
// An "expires" parameter on the "Contact" header has no semantics for
// SUBSCRIBE and is explicitly not equivalent to an "Expires" header in
// a SUBSCRIBE request or response.
if (!method.equals(Request.SUBSCRIBE)) {
ContactList cl = response.getContactHeaders();
if (cl != null) {
// Take a minimal expiration time from Contact headers.
Enumeration en = cl.getElements();
while (en.hasMoreElements()) {
ContactHeader contactHeader =
(ContactHeader) en.nextElement();
if (contactHeader != null) {
try {
expires = Integer.parseInt(
contactHeader.getExpires());
if ((expires > 0) && (expires < minExpires)) {
minExpires = expires;
}
} catch (NumberFormatException e) {
// intentionally ignored
// in the worst case
// minExpires = Integer.MAX_VALUE
}
}
}
}
} // end if (not SUBSCRIBE)
// Take an expiration time from the Expires header.
ExpiresHeader expiresHeader =
(ExpiresHeader) response.getHeader(ExpiresHeader.NAME);
if (expiresHeader != null) {
expires = expiresHeader.getExpires();
if ((expires > 0) && (expires < minExpires)) {
minExpires = expires;
}
}
if (minExpires == Integer.MAX_VALUE || minExpires < 0) {
// Apply defaults.
minExpires = 3600;
}
// System.out.println(">>> Refresh time: " + minExpires);
/*
if (expiresHeader != null) {
expires = expiresHeader.getExpires();
System.out.println(">>> From header: " + expires);
}
*/
if (minExpires != 0) {
RefreshManager.getInstance().scheduleTask(refreshID, minExpires);
}
}
}
|