/*
* ResponseForwarding.java
*
* Created on April 16, 2003, 11:09 AM
*/
package org.cafesip.jiplet.sip;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.ListIterator;
import java.util.StringTokenizer;
import java.util.Vector;
import javax.sip.ClientTransaction;
import javax.sip.Dialog;
import javax.sip.DialogState;
import javax.sip.InvalidArgumentException;
import javax.sip.ListeningPoint;
import javax.sip.ServerTransaction;
import javax.sip.SipException;
import javax.sip.SipProvider;
import javax.sip.TransactionState;
import javax.sip.address.SipURI;
import javax.sip.address.URI;
import javax.sip.header.CSeqHeader;
import javax.sip.header.FromHeader;
import javax.sip.header.RecordRouteHeader;
import javax.sip.header.ToHeader;
import javax.sip.header.ViaHeader;
import javax.sip.message.Request;
import javax.sip.message.Response;
import org.cafesip.jiplet.Jiplet;
import org.cafesip.jiplet.JipletDialog;
import org.cafesip.jiplet.JipletLogger;
/**
*
* @author deruelle
*/
public class ResponseForwarding
{
private SipProvider responseProvider;
private Jiplet jiplet;
private boolean presenceServer;
/** Creates a new instance of ResponseForwarding */
public ResponseForwarding(Jiplet jiplet, SipProvider sipProvider,
boolean presenceServer)
{
this.jiplet = jiplet;
this.responseProvider = sipProvider;
this.presenceServer = presenceServer;
}
public void forwardResponse(Response response,
ClientTransaction clientTransaction) throws SipException,
ParseException
{
TransactionsMapping transactionsMapping = null;
try
{
/*
* RFC 3261: 16.7. Response Processing: When a response is received
* by an element, it first tries to locate a client transaction
* (Section 17.1.3) matching the response. If none is found, the
* element MUST process the response (even if it is an informational
* response) as a stateless jiplet (described below). If a match is
* found, the response is handed to the client transaction. 1. Find
* the appropriate response context 2. Update timer C for
* provisional responses 3. Remove the topmost Via 4. Add the
* response to the response context 5. Check to see if this response
* should be forwarded immediately 6. When necessary, choose the
* best final response from the response context 7. Aggregate
* authorization header field values if necessary 8. Optionally
* rewrite Record-Route header field values 9. Forward the response
* 10. Generate any necessary CANCEL requests
*
* 16.11: Response processing as described in Section 16.7 does not
* apply to a jiplet behaving statelessly. When a response arrives
* at a stateless jiplet, the jiplet MUST inspect the sent-by value
* in the first (topmost) Via header field value. If that address
* matches the jiplet, (it equals a value this jiplet has inserted
* into previous requests) the jiplet MUST remove that header field
* value from the response and forward the result to the location
* indicated in the next Via header field value. The jiplet MUST NOT
* add to, modify, or remove the message body. Unless specified
* otherwise, the jiplet MUST NOT remove any other header field
* values. If the address does not match the jiplet, the message
* MUST be silently discarded.
*/
if (clientTransaction == null
|| clientTransaction.getDialog() == null)
{
if (ProxyUtilities.hasTopViaHeaderProxy(jiplet, response))
{
ListIterator viaList = response.getHeaders(ViaHeader.NAME);
response.removeHeader(ViaHeader.NAME);
ArrayList v = new ArrayList();
viaList.next();
while (viaList.hasNext())
{
ViaHeader viaHeader = (ViaHeader) viaList.next();
v.add(viaHeader);
}
for (int j = 0; j < v.size(); j++)
{
ViaHeader viaHeader = (ViaHeader) v.get(j);
response.addHeader(viaHeader);
}
viaList = response.getHeaders(ViaHeader.NAME);
if (viaList == null || !viaList.hasNext())
{
return;
}
else
{
ListeningPoint defaultLP = jiplet
.getListeningPointDefault();
jiplet.getSipProvider(defaultLP).sendResponse(response);
return;
}
}
else
{
return;
}
}
if (clientTransaction.getDialog() == null)
return;
else
{
JipletDialog jd = jiplet.getDialog(clientTransaction
.getDialog(), true);
transactionsMapping = jd.getTransactionsMapping();
}
// 1. Find the appropriate response context
/*
* The jiplet locates the "response context" it created before
* forwarding the original request using the key described in
* Section 16.6. The remaining processing steps take place in this
* context.
*/
// I guess it is done by the stack....
// 2. Update timer C for provisional responses
// I guess it is done by the stack....
// 3. Via
/*
* The jiplet removes the topmost Via header field value from the
* response. If no Via header field values remain in the response,
* the response was meant for this element and MUST NOT be
* forwarded. The remainder of the processing described in this
* section is not performed on this message, the UAC processing
* rules described in Section 8.1.3 are followed instead (transport
* layer processing has already occurred).
*/
ListIterator viaList = response.getHeaders(ViaHeader.NAME);
viaList.next();
viaList.remove();
if (!viaList.hasNext())
{
return;
}
// 4. Add response to context
// The stack takes care of that...
// 5. Check response for forwarding
/*
* Until a final response has been sent on the server transaction,
* the following responses MUST be forwarded immediately: - Any
* provisional response other than 100 (Trying) - Any 2xx response
*/
if (response.getStatusCode() == Response.TRYING)
{
return;
}
if (response.getStatusCode() == Response.REQUEST_TERMINATED)
{
return;
}
CSeqHeader cseqHeader = (CSeqHeader) response
.getHeader(CSeqHeader.NAME);
// No special processing for OK related to SUBSCIRBE and NOTIFY
if (presenceServer && response.getStatusCode() == Response.OK
&& cseqHeader.getMethod().equals(Request.SUBSCRIBE))
{
return;
}
if (presenceServer && response.getStatusCode() == Response.OK
&& cseqHeader.getMethod().equals(Request.NOTIFY))
{
return;
}
// 6. Choosing the best response
/*
* A jiplet MUST NOT insert a tag into the To header field of a 1xx
* or 2xx response if the request did not contain one. A jiplet MUST
* NOT modify the tag in the To header field of a 1xx or 2xx
* response.
*
* An element SHOULD preserve the To tag when simply forwarding a
* 3-6xx response to a request that did not contain a To tag.
*
* A jiplet MUST NOT modify the To tag in any forwarded response to
* a request that contains a To tag.
*/
// 7. Aggregate Authorization Header Field Values
// 8. Record-Route (we may modify it before sending, see further
// below)
// 9. Forward response
/*
* 16.7. 9. Forward response: The jiplet MUST pass the response to
* the server transaction associated with the response context. This
* will result in the response being sent to the location now
* indicated in the topmost Via header field value. If the server
* transaction is no longer available to handle the transmission,
* the element MUST forward the response statelessly by sending it
* to the server transport. The server transaction might indicate
* failure to send the response or signal a timeout in its state
* machine. These errors would be logged for diagnostic purposes as
* appropriate, but the protocol requires no remedial action from
* the jiplet.
*/
ServerTransaction serverTransaction = transactionsMapping
.getServerTransaction(clientTransaction);
// For forking:
if ((response.getStatusCode() == Response.UNAUTHORIZED || response
.getStatusCode() == Response.DECLINE)
&& serverTransaction != null)
{
// check the busy or decline
Vector clientsTransactionList = transactionsMapping
.getClientTransactions(serverTransaction);
if (clientsTransactionList != null
&& clientsTransactionList.size() > 1)
{
transactionsMapping.removeMapping(clientTransaction);
}
}
// For forking:
if (response.getStatusCode() == Response.OK
&& serverTransaction != null)
{
Dialog peerDialog = serverTransaction.getDialog();
Vector clientsTransactionList = transactionsMapping
.getClientTransactions(serverTransaction);
if (peerDialog != null && peerDialog.getState() != null
&& peerDialog.getState().equals(DialogState.CONFIRMED)
&& clientsTransactionList != null
&& clientsTransactionList.size() > 1)
{
Dialog dialog = clientTransaction.getDialog();
Request byeRequest = dialog.createRequest(Request.BYE);
ClientTransaction ct = responseProvider
.getNewClientTransaction(byeRequest);
dialog.sendRequest(ct);
// we have to remove the transaction from the table:
transactionsMapping.removeMapping(clientTransaction);
return;
}
else
{
if (serverTransaction != null)
transactionsMapping.addMapping(serverTransaction,
clientTransaction);
}
}
if (serverTransaction == null)
{
ListeningPoint defaultLP = jiplet.getListeningPointDefault();
jiplet.getSipProvider(defaultLP).sendResponse(response);
return;
}
else
{
// we can try to modify the tags:
Dialog dialog = serverTransaction.getDialog();
if (dialog != null)
{
String localTag = dialog.getLocalTag();
String remoteTag = dialog.getRemoteTag();
ToHeader toHeader = (ToHeader) response
.getHeader(ToHeader.NAME);
FromHeader fromHeader = (FromHeader) response
.getHeader(FromHeader.NAME);
if (localTag != null && remoteTag != null)
{
if (dialog.isServer())
{
toHeader.setTag(localTag);
}
else
{
fromHeader.setTag(remoteTag);
}
}
}
// 8. Record-Route - modify record route header if needed
/*
* If the selected response contains a Record-Route header field
* value originally provided by this proxy, the proxy MAY choose
* to rewrite the value before forwarding the response. This
* allows the proxy to provide different URIs for itself to the
* next upstream and downstream elements. A proxy may choose to
* use this mechanism for any reason. For instance, it is useful
* for multi-homed hosts.
*/
ListIterator rrHeaders = response
.getHeaders(RecordRouteHeader.NAME);
while (rrHeaders.hasNext())
{
// look for the 1st one to replace, replace it & get out
RecordRouteHeader rr = (RecordRouteHeader) rrHeaders.next();
URI uri = rr.getAddress().getURI();
if (uri instanceof SipURI)
{
SipURI sipURI = (SipURI) uri;
// is this a record route header we added?
if (jiplet.hasAddress(sipURI.getHost(), sipURI
.getPort()))
{
// does this one need replacing?
String user = sipURI.getUser();
if (user != null)
{
StringTokenizer tok = new StringTokenizer(user,
"-");
if (tok.countTokens() == 2)
{
SipURI sourceURI = jiplet
.getAddressFactory().createSipURI(
null, tok.nextToken());
sourceURI.setPort(Integer.valueOf(
tok.nextToken()).intValue());
sourceURI.setLrParam();
// rewrite it back into the RR header
rr.getAddress().setURI(sourceURI);
rrHeaders.set(rr);
break;
}
}
}
}
}
try
{
serverTransaction.sendResponse(response);
}
catch (InvalidArgumentException e)
{
// this exception only happens if the Response was created
// by Dialog.createReliableProvisionalResponse(int) and the
// application calls ServerTransaction.sendResponse() to
// send it
JipletLogger
.error("Response forwarding failed - invalid send method for reliable provisional response - need to add a check for this and call dialog method sendReliableProvisionalResponse() instead. Response = \n"
+ response.toString());
}
}
/** ************************************************************************ */
/** ************ 10. Generate CANCELs ******* */
/** ************************************************************************* */
/*
* If the forwarded response was a final response, the jiplet MUST
* generate a CANCEL request for all pending client transactions
* associated with this response context. A jiplet SHOULD also
* generate a CANCEL request for all pending client transactions
* associated with this response context when it receives a 6xx
* response. A pending client transaction is one that has received a
* provisional response, but no final response (it is in the
* proceeding state) and has not had an associated CANCEL generated
* for it. Generating CANCEL requests is described in Section 9.1.
*/
if (response.getStatusCode() == Response.OK
|| (response.getStatusCode() >= Response.BUSY_EVERYWHERE && response
.getStatusCode() <= Response.SESSION_NOT_ACCEPTABLE))
{
Vector clientsTransactionList = transactionsMapping
.getClientTransactions(serverTransaction);
for (Enumeration e = clientsTransactionList.elements(); e
.hasMoreElements();)
{
ClientTransaction ctr = (ClientTransaction) e.nextElement();
if (ctr != clientTransaction)
{
TransactionState transactionState = ctr.getState();
if (transactionState == null
|| transactionState.getValue() == TransactionState.PROCEEDING
.getValue())
{
/*
* 9.1: The following procedures are used to
* construct a CANCEL request. The Request-URI,
* Call-ID, To, the numeric part of CSeq, and From
* header fields in the CANCEL request MUST be
* identical to those in the request being
* cancelled, including tags. A CANCEL constructed
* by a client MUST have only a single Via header
* field value matching the top Via value in the
* request being cancelled. Using the same values
* for these header fields allows the CANCEL to be
* matched with the request it cancels (Section 9.2
* indicates how such matching occurs). However, the
* method part of the CSeq header field MUST have a
* value of CANCEL. This allows it to be identified
* and processed as a transaction in its own right
* (See Section 17).
*
* If the request being cancelled contains a Route
* header field, the CANCEL request MUST include
* that Route header field's values.
*/
Request cancelRequest = ctr.createCancel();
// Let's keep only the top most via header:
ListIterator cancelViaList = cancelRequest
.getHeaders(ViaHeader.NAME);
cancelRequest.removeHeader(ViaHeader.NAME);
cancelRequest.addHeader((ViaHeader) cancelViaList
.next());
SipURI localAddr = (SipURI) ctr.getDialog()
.getLocalParty().getURI();
SipProvider p = jiplet.getSipProvider(localAddr
.getHost(), localAddr.getPort());
if (p == null)
{
ListeningPoint defaultLP = jiplet
.getListeningPointDefault();
p = jiplet.getSipProvider(defaultLP);
}
ClientTransaction ct = p
.getNewClientTransaction(cancelRequest);
ct.sendRequest();
}
}
}
}
}
finally
{
if (clientTransaction != null
&& clientTransaction.getState().equals(
TransactionState.COMPLETED))
{
if (clientTransaction.getDialog() != null)
{
JipletDialog jd = jiplet.getDialog(clientTransaction
.getDialog(), true);
transactionsMapping = jd.getTransactionsMapping();
if (transactionsMapping != null)
transactionsMapping.removeMapping(clientTransaction);
}
}
}
}
}