/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 1997-2009 Sun Microsystems, Inc. All rights reserved.
* Copyright (c) Ericsson AB, 2004-2008. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can obtain
* a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html
* or glassfish/bootstrap/legal/LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
* Sun designates this particular file as subject to the "Classpath" exception
* as provided by Sun in the GPL Version 2 section of the License file that
* accompanied this code. If applicable, add the following below the License
* Header, with the fields enclosed by brackets [] replaced by your own
* identifying information: "Portions Copyrighted [year]
* [name of copyright owner]"
*
* Contributor(s):
*
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
package com.ericsson.ssa.sip;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.security.AccessController;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.ListIterator;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.Servlet;
import javax.servlet.sip.Address;
import javax.servlet.sip.ServletParseException;
import javax.servlet.sip.SipApplicationSession;
import javax.servlet.sip.SipServletMessage;
import javax.servlet.sip.SipServletRequest;
import javax.servlet.sip.SipServletResponse;
import javax.servlet.sip.URI;
import org.apache.catalina.Globals;
import org.jvnet.glassfish.comms.util.LogUtil;
/**
* Responsible of the UAC and UAS behaviour.
*
* @author ehsroha
* @reviewed ejoelbi 2006-oct-19
*/
public class UA implements PathNode, Externalizable {
private static final long serialVersionUID = -8507246342015896916L;
private static final Logger m_Log = LogUtil.SIP_LOGGER.getLogger();
private transient SipSessionManager m_sipSessionManager;
private String m_sipApplicationSessionId;
FSM m_InviteFsm = null;
private final LinkedList<URI> m_RouteSet = new LinkedList<URI>();
private Type m_Type = Type.Caller;
private String m_sipSessionId;
private int m_RemoteCSeq = -1;
private Object _CSeqSynch = new Object();
private int _inviteCSeq = -1;
public UA(SipApplicationSessionImpl appSession, boolean isCaller) {
this(appSession.getId(), isCaller, appSession.getSipSessionManager());
}
public UA(String appSessionId, boolean isCaller,
SipSessionManager sipSessionManager) {
m_sipApplicationSessionId = appSessionId;
m_sipSessionManager = sipSessionManager;
if (isCaller) {
m_Type = Type.Caller;
} else {
m_Type = Type.Callee;
}
}
// For serialization support
public UA() {
}
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(m_InviteFsm);
out.writeInt(m_RouteSet.size());
Iterator<URI> ui = m_RouteSet.iterator();
while (ui.hasNext()) {
out.writeObject(ui.next());
}
// TODO check if needed used for multidialogs e.g. within a subscribe
// session
// need to know how many concurrent sessions that are ongoing
// private final Map<String, String> m_Sessions = new
// ConcurrentHashMap<String, String>();
if (m_Type == Type.Caller) {
out.writeInt(1);
} else if (m_Type == Type.Callee) {
out.writeInt(2);
} else {
out.writeInt(3);
}
out.writeUTF(m_sipSessionManager.getApplicationId());
out.writeUTF(m_sipSessionId);
out.writeUTF(m_sipApplicationSessionId);
out.writeInt(m_RemoteCSeq);
// m_RemoteCSeqSynch = new Object();
}
public void readExternal(ObjectInput in)
throws IOException, ClassNotFoundException {
m_InviteFsm = (INVITESession) in.readObject();
int i = in.readInt();
for (int c = 0; c < i; c++) {
m_RouteSet.add((URI) in.readObject());
}
switch (in.readInt()) {
case 1:
m_Type = Type.Caller;
break;
case 2:
m_Type = Type.Callee;
break;
default:
m_Type = Type.Undefined;
}
SipSessionManagerRegistry reg = null;
if (Globals.IS_SECURITY_ENABLED) {
reg = (SipSessionManagerRegistry) AccessController.doPrivileged(new PrivilegedGetSipSessionManagerRegistry());
} else {
reg = SipSessionManagerRegistry.getInstance();
}
if (reg != null) {
m_sipSessionManager = reg.get(in.readUTF());
}
m_sipSessionId = in.readUTF();
m_sipApplicationSessionId = in.readUTF();
m_RemoteCSeq = in.readInt();
_CSeqSynch = new Object();
}
private SipSessionBase findSipSession() {
if (m_sipSessionId == null) {
return null;
}
SipSessionBase result = null;
try {
result = m_sipSessionManager.findSipSession(m_sipSessionId);
} catch (RemoteLockException e) {
throw new RemoteLockRuntimeException(e);
}
return result;
}
private SipApplicationSessionImpl findSipApplicationSession() {
if (m_sipApplicationSessionId == null) {
return null;
}
SipApplicationSessionImpl result = null;
try {
result = m_sipSessionManager.findSipApplicationSession(m_sipApplicationSessionId);
} catch (RemoteLockException e) {
throw new RemoteLockRuntimeException(e);
}
return result;
}
public Type getType() {
return m_Type;
}
private boolean isCaller() {
return m_Type == Type.Caller;
}
/**
* Returns whether all dialog creational sessions have been terminated or not
*
* @return whether all dialog creational sessions have been terminated or not
*/
public boolean isEmptyDialogSession() {
// Not implemented, optimization for application pgm
// return m_Sessions.isEmpty();
return true;
}
/**
* If the touple method and id matches an existing session of this dialog
* true is returned otherwise false.
*
* @param method
* is one of the touples of the key to find a session
* @param id
* is one of the touples of the key to find a session. Some messages
* could create many sessions and to distinguish one session from
* another a unique id is used and together with the method a unique
* touple is formed.
* @return whether the method and id touple will match an existing session
*/
public boolean isDialogSession(String method, String id) {
// Not implemented, optimization for application pgm
// boolean isDialogSession = false;
// if (method.equals("INVITE"))
// {
// isDialogSession = m_Sessions.contains(method);
// }
// else
// {
// isDialogSession = m_Sessions.contains(method + id);
// }
// return isDialogSession;
return true;
}
/**
* A message that is able to create a dialog (e.g. INVITE, SUBSCRIBE, REFER,
* etc.) should be stored as a session and later removed when a terminating
* message arrives. All methods that create dialog (INVITE, SUBSCRIBE, REFER,
* etc) must use this function to inform that a session has been established.
* When terminating a session the removeDialogSession MUST be used. A
* SUBSCRIBE should also indicate which session id that has been used, which
* is not necessary for an INVITE. NOTE: The re-INVITE and a SUBSCRIBE
* updating the expire timer MAY not use this function.
*
* @param method
* is one of the touples of the key to store a session
* @param id
* is one of the touples of the key to store a session. Some messages
* could create many sessions and to distinguish one session from
* another a unique id is used and together with the method a unique
* touple is formed.
* @return the prevoius method that match if any, otherwise null
*/
public boolean addDialogSession(String method, String id) {
// Not implemented, optimization for application pgm
// boolean value = false;
// if (method.equals("INVITE"))
// {
// value = m_Sessions.add(method);
// }
// else
// {
// value = m_Sessions.add(method + id);
// }
// return value;
return true;
}
/**
* Removes the session.
*
* @param method
* is one of the touples of the key to remove a session
* @param id
* is one of the touples of the key to remove a session. Some messages
* could create many sessions and to distinguish one session from
* another a unique id is used and together with the method a unique
* touple is formed.
* @return the session that have been removed or if no match null is
* returned.
*/
public boolean removeDialogSession(String method, String id) {
// Not implemented, optimization for application pgm
// boolean value = false;
// if (method.equals("INVITE"))
// {
// value = m_Sessions.remove(method);
// }
// else
// {
// value = m_Sessions.remove(method + id);
// }
// return value;
return true;
}
/**
* Will shallow copy most of the member variables and returns a cloned
* ua. If ua is invalid null is returned.
*
* @return a cloned ua. If ua is invalid null is returned.
*/
public Object clone() {
UA clone = new UA(m_sipApplicationSessionId, isCaller(),
m_sipSessionManager);
clone.m_InviteFsm = (m_InviteFsm != null) ? (FSM) m_InviteFsm.clone()
: null;
clone.m_sipSessionId = m_sipSessionId;
return clone;
}
/**
* Returns the InviteFSM. Implemented for B2buaHelper.createCancel handling.
*/
public INVITESession getInviteFSM() {
if (this.m_InviteFsm != null) {
return INVITESession.class.cast(this.m_InviteFsm);
} else {
return null;
}
}
/**
* Returns a state machine that could handle this message or null otherwise
*
* @param m
* the message indicating which state machine to use
* @return a valid state machine that could handle this message or null
* otherwise
*/
private FSM getFSM(SipServletMessage m) {
FSM fsm = SUBSCRIBE_REFERSession.createFSM(m);
if (fsm != null) {
return fsm;
}
fsm = GeneralSession.createFSM(m);
if (fsm != null) {
return fsm;
}
if (m_InviteFsm == null) {
synchronized (this) {
if (m_InviteFsm == null) {
m_InviteFsm = INVITESession.createFSM(m);
}
}
}
return m_InviteFsm;
}
private void saveRemoteTarget(SipServletMessageImpl m,
boolean contactIsMandatory) throws ServletParseException {
Address contact = m.getAddressHeaderImpl(Header.CONTACT);
if (contact != null) {
findSipSession()
.setRemoteTarget(m.getAddressHeaderImpl(Header.CONTACT).getURI());
} else {
if (contactIsMandatory) {
if (m_Log.isLoggable(Level.FINE)) {
m_Log.log(Level.FINE,
"No Contact Header found, unable to continue this session, invalidating session: ");
}
findSipSession().invalidate();
throw new IllegalStateException("Missing Contact header field");
}
}
}
/**
* adds the route set to the message
*
* @param m
*/
private void addRouteSet(SipServletMessageImpl m) {
Iterator<URI> iter = m_RouteSet.iterator();
while (iter.hasNext()) {
Header routeHeader = new MultiLineHeader(Header.ROUTE, true);
URI u = iter.next();
routeHeader.setValue("<" + u.toString() + ">", false);
m.addHeader(routeHeader);
}
}
/**
* Saves the route set in order from Record-Route
*/
private void saveRouteSet(SipServletRequest req)
throws ServletParseException {
String dummy = req.getHeader(Header.RECORD_ROUTE);
if (dummy == null) {
return;
}
//Remove after fixing issue 85
ListIterator<Address> iter = req.getAddressHeaders(Header.RECORD_ROUTE);
Address addr = null;
// RFC 12.1.1
// If no Record-Route header field is present in the
// request, 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.
m_RouteSet.clear();
while (iter.hasNext()) {
addr = (Address) iter.next();
m_RouteSet.add(addr.getURI());
}
}
/**
* Saves the route set in reverse order from Record-Route
*/
private void saveRouteSet(SipServletResponse resp)
throws ServletParseException {
ListIterator<Address> iter = resp.getAddressHeaders("Record-Route");
Address addr = null;
// RFC 12.1.2
// If no Record-Route header field is present in the
// request, 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.
m_RouteSet.clear();
while (iter.hasNext()) {
addr = (Address) iter.next();
m_RouteSet.addFirst(addr.getURI());
}
}
private boolean isLooseRoute() {
boolean toReturn = false;
if ((m_RouteSet != null) && !m_RouteSet.isEmpty()) {
URI uri = m_RouteSet.getFirst();
URIImpl uriImpl = (URIImpl) uri;
toReturn = uriImpl.getLrParam();
}
return toReturn;
}
/**
* Always used by the servlet entity acting as a UAC for the particular
* request.
*
* @param req
* @throws java.io.IOException
* @throws java.lang.IllegalStateException
*/
public void send(SipServletRequestImpl req)
throws java.io.IOException, java.lang.IllegalStateException {
OutboundInterface.resolve(req);
FSM fsm = getFSM(req);
if (fsm != null) {
if (req.isInitial()) {
m_sipSessionId = req.getSessionImpl().getId();
} else {
// RFC 3261, 12.2.1.1
if (!req.getMethod().equals("CANCEL")) {
if (m_RouteSet.isEmpty()) {
req.setRequestURI(getRemoteTarget());
} else if (isLooseRoute()) {
req.setRequestURI(getRemoteTarget());
addRouteSet(req);
} else {
if (m_Log.isLoggable(Level.FINE)) {
m_Log.log(Level.FINE,
"NOT IMPLEMENTED: strict routing; cannot send request: " +
req);
}
throw new IllegalStateException(
"Strict routing not implemented");
}
}
}
// invoke fsm
fsm.send(req, this);
} else {
if (m_Log.isLoggable(Level.FINE)) {
m_Log.log(Level.FINE,
"No state machine for request could be found: " +
req.toDebugString());
}
}
}
public void saveContactRouteSetRemoteTarget(SipServletRequestImpl req)
throws ServletParseException {
if (getRemoteTarget() == null) {
synchronized (this) {
// double-checked locking pattern
if (getRemoteTarget() == null) {
// save route set and remote target
// RFC 3261, 12.1.1
saveRouteSet(req);
saveRemoteTarget(req, true);
}
}
}
}
public synchronized void saveContactRemoteTarget(SipServletResponseImpl resp) throws ServletParseException
{
HeaderRequirement contactRequirement = SipFactoryImpl.getContactRequirement(resp);
if (contactRequirement != HeaderRequirement.NOT_APPLICAPLE) {
saveRemoteTarget(resp.getRequestImpl(),
contactRequirement == HeaderRequirement.MANDATORY);
}
}
public void saveContactRouteSetRemoteTarget(SipServletResponseImpl resp)
throws ServletParseException {
if (getRemoteTarget() == null) {
synchronized (this) {
// double-checked locking pattern
if (getRemoteTarget() == null) {
SipServletRequest req = resp.getRequest();
// save route set and remote target
// RFC 3261, 12.1.1
saveRouteSet(req);
saveContactRemoteTarget(resp);
}
}
}
}
/**
* Always used by the servlet entity acting as a UAS for the particular
* response.
*
* @param resp
* @throws java.io.IOException
* @throws java.lang.IllegalStateException
*/
public void send(SipServletResponseImpl resp)
throws java.io.IOException, java.lang.IllegalStateException {
OutboundInterface.resolve(resp);
FSM fsm = getFSM(resp);
if (fsm != null) {
if (resp.getRequest().isInitial() && (m_RemoteCSeq == -1)) {
setInviteCSeq(resp.getRequestImpl());
setRemoteCSeq(resp.getRequestImpl());
}
// invoke fsm
fsm.send(resp, this);
} else {
if (m_Log.isLoggable(Level.FINE)) {
m_Log.log(Level.FINE,
"No state machine for response could be found: " +
resp.toDebugString());
}
}
}
/**
* Always used by the Dispatcher to access the UAS
*/
public void dispatch(SipServletRequestImpl req) {
if(!validateSessionState()){
m_Log.log(Level.FINE,"UA is not handling request as SipSession or SipApplicationSession are missing or invalid");
return;
}
FSM fsm = getFSM(req);
if (fsm != null) {
if (m_RemoteCSeq == -1) {
// Since remote CSeq is not saved lets do it...
boolean success = setRemoteCSeq(req);
if (!success) {
// raise condition occured, lets verify..
success = verifyAndUpdateCSeq(req);
if (!success) {
// CSeq is lower than allowed, send error response...
// TR HH52078
SipServletResponseImpl resp = req.createTerminatingResponse(500);
if (resp == null) {
return;
}
resp.popDispatcher().dispatch(resp);
return;
}
}
} else {
// lets verify...
if (!req.getMethod().equals("ACK")) // FIXME, if PRACK is used CSeq
// is increased
{
boolean success = verifyAndUpdateCSeq(req);
if (!success) {
// CSeq is lower than allowed, send error response...
SipServletResponseImpl resp = req.createTerminatingResponse(500);
resp.popDispatcher().dispatch(resp);
return;
}
}
}
SecurityInterceptor si = SecurityInterceptor.getInstance();
AuthModule module = this.getApplicationSession().getServletDispatcher().getAuthModule();
String servletToInvoke = this.getSipSession().getHandler();
try {
if (!si.verifyRequest(req, module, servletToInvoke)) {
return;
}
} catch (IOException ex) {
m_Log.log(Level.SEVERE, null, ex);
return ;
}
try {
fsm.dispatch(req, this);
} catch (RemoteLockRuntimeException e) {
SipServletResponseImpl resp = req.createTerminatingResponse(500);
resp.setHeader(Header.RETRY_AFTER, "5");
resp.popDispatcher().dispatch(resp);
return;
}
} else {
if (m_Log.isLoggable(Level.FINE)) {
m_Log.log(Level.FINE,
"No state machine for request could be found: " +
req.toDebugString());
}
}
}
public void saveTargetRefreshContact(SipServletResponseImpl resp)
throws ServletParseException {
synchronized (this) {
HeaderRequirement contactRequirement = SipFactoryImpl.getContactRequirement(resp);
if (contactRequirement != HeaderRequirement.NOT_APPLICAPLE) {
saveRemoteTarget(resp,
contactRequirement == HeaderRequirement.MANDATORY);
}
}
}
public void saveRouteSetRemoteTarget(SipServletResponseImpl resp)
throws ServletParseException {
if (getRemoteTarget() == null) {
synchronized (this) {
if (getRemoteTarget() == null) // double-checked locking pattern
{
// add route set and remote target
// RFC 3261, 12.1.2
saveRouteSet(resp);
HeaderRequirement contactRequirement = SipFactoryImpl.getContactRequirement(resp);
if (contactRequirement != HeaderRequirement.NOT_APPLICAPLE) {
saveRemoteTarget(resp,
contactRequirement == HeaderRequirement.MANDATORY);
}
}
}
}
}
/**
* Always used by the Dispatcher to access the UAC
*/
public void dispatch(SipServletResponseImpl resp) {
if(!validateSessionState()){
m_Log.log(Level.FINE,"UA is not handling response as SipSession or SipApplicationSession are missing or invalid");
return;
}
FSM fsm = getFSM(resp);
if (fsm != null) {
try {
fsm.dispatch(resp, this);
} catch (RemoteLockRuntimeException e) {
if (m_Log.isLoggable(Level.FINE)) {
m_Log.log(Level.FINE,
"Could not dispatch response since the dialog was remotely locked: " +
resp.toDebugString());
}
return;
}
} else {
if (m_Log.isLoggable(Level.FINE)) {
m_Log.log(Level.FINE,
"No state machine for response could be found: " +
resp.toDebugString());
}
}
}
/**
* Checks that sessions (SipApplicationSession and SipSession) for this UA are still valid
*
* @return Whether sessions are valid or not. If false is returned, a good thing would be not to proceed.
*/
private boolean validateSessionState() {
SipSessionBase sipSession = getSipSession();
if(sipSession==null){
return false;
}
if(!sipSession.isValid()){
return false;
}
SipApplicationSession sipApplicationSession = getApplicationSession();
if(sipApplicationSession==null){
return false;
}
return sipApplicationSession.isValid();
}
public SipSessionBase getSipSession() {
return findSipSession();
}
public String getSipSessionId() {
return m_sipSessionId;
}
public SipSessionManager getSipSessionManager() {
return m_sipSessionManager;
}
public void setSipSession(SipSessionBase s) {
s.setType(getType());
m_sipSessionId = s.getId();
}
public SipApplicationSessionImpl getApplicationSession() {
return findSipApplicationSession();
}
public Servlet getServlet(String handler) {
return SipFactoryImpl.getInstance().getServiceHandler()
.getHandler(getApplicationSession().getName(),
handler);
}
/**
* Sets the INVITE CSeq value received from remote
* to later verify CANCEL or ACK.
*
* @param req
* request holding the INVITE CSeq value
* @return false if starting point was already set
* @throws NumberFormatException
*/
private void setInviteCSeq(SipServletRequestImpl req)
throws NumberFormatException {
if (req.getMethod().equals("INVITE")) {
synchronized (_CSeqSynch) {
_inviteCSeq = req.getCSeqNumber();
}
}
}
/**
* Sets the starting CSeq value received from remote
*
* @param req
* request holding the starting CSeq value
* @return false if starting point was already set
* @throws NumberFormatException
*/
private boolean setRemoteCSeq(SipServletRequestImpl req)
throws NumberFormatException {
boolean success = false;
synchronized (_CSeqSynch) {
if (m_RemoteCSeq == -1) {
m_RemoteCSeq = req.getCSeqNumber();
success = true;
}
}
return success;
}
/**
* Verifies that the CSeq value generated by the remote side is strictly
* monotonically increasing and contiguous. NOTE: ACK and CANCEL should have
* same value as INVITE.
*
* @param m
* @return false if CSeq is lower than accepted otherwise true
* @throws NumberFormatException
*/
private boolean verifyAndUpdateCSeq(SipServletRequestImpl req)
throws NumberFormatException {
boolean success = false;
int messageCSeq = req.getCSeqNumber();
int CSeq = -1;
if (req.getMethod().equals("ACK") || req.getMethod().equals("CANCEL")) {
// ACK and CANCEL should have same CSeq as INVITE
synchronized (_CSeqSynch) {
CSeq = _inviteCSeq;
success = (messageCSeq == CSeq);
}
} else if (req.getMethod().equals("INVITE")) {
if (messageCSeq > _inviteCSeq) {
synchronized (_CSeqSynch) {
_inviteCSeq = messageCSeq;
success = true;
}
}
} else {
// RFC3261, 12.2.2. If the remote sequence number was not
// empty, but the sequence number of the request is lower
// than the remote sequence number, the request is out of
// order. If the remote sequence number was not empty, and
// the sequence number of the request is greater than the
// remote sequence number, the request is in order. The UAS
// MUST then set the remote sequence number to the value of
// the sequence number in the CSeq header field value in the
// request.
synchronized (_CSeqSynch) {
CSeq = m_RemoteCSeq;
if (messageCSeq > CSeq) {
m_RemoteCSeq = messageCSeq;
success = true;
}
}
}
return success;
}
public URI getRemoteTarget() {
SipSessionBase session = findSipSession();
return (session != null) ? session.getRemoteTarget() : null;
}
/**
* Returns true if this PathNode is replicable, false otherwise.
*
* @return true if this PathNode is replicable, false otherwise
*/
public boolean isReplicable() {
return findSipApplicationSession().isReplicable();
}
/**
* Sets the remote CSEQ. Note, this method is only to be used at restore of DialogFragment.
* @param cseq
*/
void setRemoteCSeq(int cseq) {
m_RemoteCSeq = cseq;
}
/**
* Gets the remote CSEQ. Note, this method is only to be used at restore of DialogFragment.
* @return The remote CSEQ
*/
int getRemoteCSeq() {
return m_RemoteCSeq;
}
}