/*
* 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 org.jvnet.glassfish.comms.clb.core.sip;
import java.util.ListIterator;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.sip.ServletParseException;
import javax.servlet.sip.SipServletRequest;
import org.jvnet.glassfish.comms.clb.core.CLBRuntimeException;
import org.jvnet.glassfish.comms.util.LogUtil;
import com.ericsson.ssa.sip.AddressImpl;
import com.ericsson.ssa.sip.Header;
import com.ericsson.ssa.sip.SipServletMessageImpl;
import com.ericsson.ssa.sip.SipServletRequestImpl;
import com.ericsson.ssa.sip.SipServletResponseImpl;
import com.ericsson.ssa.sip.URIImpl;
import com.ericsson.ssa.sip.ViaImpl;
import com.ericsson.ssa.sip.dns.TargetTuple;
import com.ericsson.ssa.sip.dns.SipTransports;
import com.ericsson.ssa.sip.dns.TargetResolver;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.jvnet.glassfish.comms.clb.core.CLBConstants;
import org.jvnet.glassfish.comms.clb.core.ProxyKeyExtractor;
import org.jvnet.glassfish.comms.clb.core.util.LoadbalancerUtil;
/**
* This class implements the SIP load balancer routing logic. It implements both
* the front-end logic (incoming requests and responses) and back-end logic
* (both incoming and outgoing requests and responses)
*/
public class SipLoadBalancerBackend {
private static Logger logger = LogUtil.CLB_LOGGER.getLogger();
private String instanceID;
private Socket localAddress;
private SipClientToClbFEMapper sipClientToClbFEMapper;
/**
* Creates an instance and associates it with the specified hash key
* extractor and server instance lookup.
*
* @param instanceID the ID of the current instance
*/
public SipLoadBalancerBackend(String instanceID, Socket localAddress)
throws CLBRuntimeException {
this.instanceID = instanceID;
this.localAddress = localAddress;
sipClientToClbFEMapper = SipClientToClbFEMapper.getInstance();
}
/**
* Handle a request that as been received by this instance. It shall either
* be served by this instance, or be proxied to another instance.
*
* @param req the request; the request may be modified (headers added or
* changed)
*/
public void handleIncomingRequest(SipServletRequestImpl req) throws SipRoutingException {
if (logger.isLoggable(Level.FINER)) {
logger.log(Level.FINER, "Handle incoming request");
}
pushBackendLBVia(req);
sipClientToClbFEMapper.removeFEAddressMapping(req);
if (req.getBeKey() == null) {
// The SipLoadBalancerIncomingHandler was not present (this is a
// pure backend). Extract BEKey that was extracted by front-end
String beKey = ProxyKeyExtractor.extractHashKey(req);
if (logger.isLoggable(Level.FINER)) {
logger.log(Level.FINER, "Extracted BE key: " + beKey);
}
if (beKey != null) {
req.setBeKey(beKey);
}
}
saveIncomingConnection(req);
decodeClientCert(req);
decodeRemote(req);
//If request is UDP, there is no need to save incoming connection
//response can directly go back to client
if(req.getRemote().getProtocol() == SipTransports.UDP_PROT
&& LoadbalancerUtil.isAsymmetric()){
req.setSystemAttribute(CLBConstants.CONNID_ATTR,
Connection.UDP);
Header viaHeader = req.getRawHeader(Header.VIA);
if (viaHeader != null) {
ListIterator<String> viaIterator;
viaIterator = viaHeader.getValues();
if (viaIterator.hasNext()) {
ViaImpl via = new ViaImpl(viaIterator.next());
// Extract 'felb' flag
String frontEndLb = via.getParameter(CLBConstants.FE_LB_PARAM);
if (frontEndLb != null) {
// This response was received from the back-end,
// pop Via, extract connection and forward it to the client.
viaHeader.setReadOnly(false);
viaIterator.remove();
}
}
}
}
}
/**
* Handle a request that as been created or proxied by this instance.
*
* @param request the request; the request may be modified (headers added or
* changed)
* @return a possible new connection; if null continue with the connection
* in the transaction stack.
* @throws SipRoutingException thrown in case request was malformed
*/
public Connection handleOutgoingRequest(SipServletRequestImpl request)
throws SipRoutingException {
if (logger.isLoggable(Level.FINER)) {
logger.log(Level.FINER, "Handle outgoing request");
}
// Check if hash key has been previously saved
// Note! The request will not have a hashkey in case there was no application handling the request and it has been proxied.
String hashkey = request.getBeKeyFromSession();
if(hashkey == null){
logger.log(Level.WARNING, "clb.sip.be_key_is_missing_in_request",
new Object[]{request});
}
if (logger.isLoggable(Level.FINER)) {
logger.log(Level.FINER, "Hashkey: " + hashkey);
}
// Set BERoute parameter on topmost Via
// Add quotes around it, as it may contain special characters
ViaImpl via = getTopVia(request);
via.setParameter(CLBConstants.BE_ROUTE_PARAM,
CLBConstants.QUOTE + instanceID + CLBConstants.QUOTE);
sipClientToClbFEMapper.pushRouteHeader(request, localAddress.getHostName(),
localAddress.getPort());
// Check if the application has set Contact
if (request.isContactIndicated()) {
// Check if this request is initiated from this instance
Header contactHeader = request.getRawHeader(Header.CONTACT);
if (contactHeader != null) {
// There is a contact, we need to add the BE-KEY to it
try {
AddressImpl addressValue = (AddressImpl) contactHeader.getAddressValue();
if (addressValue != null) {
// Fetch current read-only status
boolean readOnly = addressValue.isReadOnly();
// Allow writes on Address object
addressValue.setReadOnly(false);
URIImpl uriImpl = (URIImpl) addressValue.getURI();
if(uriImpl != null){
uriImpl.encodeBeKey(hashkey);
}
// Reset read-only status
addressValue.setReadOnly(readOnly);
}
} catch (ServletParseException e) {
throw new SipRoutingException("Malformed Contact", e);
}
}
replaceTopVia(request, via);
if (logger.isLoggable(Level.FINER)) {
logger.log(Level.FINER,
"The request originates from this instance, altered request: " + request);
}
return null;
} else if (request.isRecordRouteIndicated()) {
// This request was proxied, save ID of incoming connection.
String encodedConnection;
if ((encodedConnection=getEncondedConnection(request)) == null) {
throw new SipRoutingException("Could not find connection information in a proxied request, shall never happen!");
} else {
via.setParameter(CLBConstants.CONNID_PARAM, encodedConnection);
}
Header rrHeader = request.getRawHeader(Header.RECORD_ROUTE);
AddressImpl rrAddr;
try {
rrAddr = (AddressImpl) rrHeader.getAddressValue();
} catch (ServletParseException e) {
throw new SipRoutingException("Malformed record-Route", e);
}
// The R-R was added by the application, thus we shall set 'bekey'
// Fetch current read-only status
boolean readOnly = rrAddr.isReadOnly();
// Allow writes on Address object
rrAddr.setReadOnly(false);
URIImpl uriImpl = (URIImpl) rrAddr.getURI();
if (uriImpl != null) {
uriImpl.encodeBeKey(hashkey);
}
// Reset read-only status
rrAddr.setReadOnly(readOnly);
replaceTopVia(request, via);
if (logger.isLoggable(Level.FINER)) {
logger.log(Level.FINER,
"The request was proxied, altered request: " + request);
}
return null;
} else {
// This request was either:
// a) proxied but not Record-Routed; save ID of incoming connection so that response can travel back on it.
// b) a non-dialog request originating from this instance; no connection information will exist.
String encodedConnection;
if ((encodedConnection=getEncondedConnection(request)) != null) {
via.setParameter(CLBConstants.CONNID_PARAM, encodedConnection);
}
replaceTopVia(request, via);
return null;
}
}
void handleIncomingResponse(SipServletResponseImpl response)
throws SipRoutingException {
// Get topmost header
Header viaHeader = response.getRawHeader(Header.VIA);
if (viaHeader != null) {
ListIterator<String> viaIterator;
viaIterator = viaHeader.getValues();
if (viaIterator.hasNext()) {
ViaImpl via = new ViaImpl(viaIterator.next());
// Extract connection
Connection connection = LoadbalancerUtil.getConnection(via);
if (connection != null) {
// The top Via contained connection information, the origin request was proxied
// by the application; the connection information either identifies
// - the connection between the front-end and this instance
// or
// - the client and this instance.
// (Note, it is a valid case that no connection ID in the response;
// this occurs if the origin request was sent from an UAC on this instance)
response.setSystemAttribute(CLBConstants.CONNID_ATTR, connection);
}
decodeClientCert(response); // TODO Can a response ever have a client cert?
decodeRemote(response);
return;
}
}
// No Via? The response must be corrupt, drop it!
throw new SipRoutingException(
"No Via on response, shall never happen; drop the response!");
}
private String getEncondedConnection(SipServletRequestImpl request) {
Connection c = (Connection) request.getAttribute(CLBConstants.CONNID_ATTR);
if (c != null) {
return c.getEncodedValue();
}
return null;
}
/**
* Handle a response that has been created or proxied by this instance.
*
* @param response the response; the response may be modified (headers added
* or changed)
* @return a possible new connection; if null continue with the connection
* in the transaction stack.
* @throws SipRoutingException thrown if message was malformed
*/
public Connection handleOutgoingResponse(SipServletResponseImpl response)
throws SipRoutingException {
if (logger.isLoggable(Level.FINER)) {
logger.log(Level.FINER, "Handle outgoing response");
}
if (response.isSerialized()) {
return null;
}
sipClientToClbFEMapper.createFEAddressMapping(response);
popBackendLBVia(response);
// Note! The response will not have a hashkey in case there was no application handling the request
String hashkey = response.getBeKeyFromSession();
if(hashkey == null){
logger.log(Level.WARNING, "clb.sip.be_key_is_missing_in_response",
new Object[]{response});
}
if (hashkey != null) {
if (response.isContactIndicated()) {
// This is a UAS.
Header contactHeader = response.getRawHeader(Header.CONTACT);
if (contactHeader != null) {
AddressImpl contactAddress;
try {
contactAddress = (AddressImpl) (contactHeader.getAddressValue());
} catch (ServletParseException e) {
throw new SipRoutingException("Malformed Contact", e);
}
URIImpl contactUri = ((URIImpl) contactAddress.getURI());
contactAddress = ((AddressImpl) contactAddress.clone(true, true));
contactUri = (URIImpl) contactUri.clone();
contactUri.encodeBeKey(hashkey);
// Allow writes on cloned Address object
contactAddress.setReadOnly(false);
contactAddress.setURI(contactUri);
contactHeader.setReadOnly(false);
contactHeader.removeValues();
contactHeader.setValue(contactAddress.toString(), true);
contactHeader.setReadOnly(true);
}
} else if (response.getMethod().equalsIgnoreCase("INVITE") && response.getStatus() >= 300){
// This is a non-2xx response to an INVITE, set bekey in the To-header
Header toHeader = response.getRawHeader(Header.TO);
if (toHeader != null) {
AddressImpl toAddress;
try {
toAddress = (AddressImpl) (toHeader.getAddressValue());
} catch (ServletParseException e) {
throw new SipRoutingException("Malformed To-header", e);
}
URIImpl toUri = ((URIImpl) toAddress.getURI());
toAddress = ((AddressImpl) toAddress.clone(true, true));
toUri = (URIImpl) toUri.clone();
toUri.encodeBeKey(hashkey);
// Allow writes on cloned Address object
toAddress.setReadOnly(false);
toAddress.setURI(toUri);
toHeader.setReadOnly(false);
toHeader.removeValues();
toHeader.setValue(toAddress.toString(), true);
toHeader.setReadOnly(true);
}
}
}
// Get the connection that was extracted from the Via in the (possible) incoming response
Connection connection = (Connection) response.getRequest().getAttribute(CLBConstants.CONNID_ATTR);
if (connection == Connection.UDP &&
LoadbalancerUtil.isAsymmetric()) {
// The origin request was sent via UDP.
// There will thus *not* exist a connection between the client and this instance.
// Resolve the Via and use the resolved address as destination.
try {
TargetTuple tt = TargetResolver.getInstance().resolveResponse(response);
connection = new Connection(tt.getProtocol(), null, new Socket(tt.getIP(), tt.getPort()));
} catch (Exception e) {
logger.log(Level.WARNING, "clb.sip.warning.resolve_failed");
if(logger.isLoggable(Level.FINE)){
logger.log(Level.FINE, "clb.caught_an_exception", e);
}
throw new SipRoutingException("Unable to resolve address", e);
}
}
if (logger.isLoggable(Level.FINER)) {
logger.log(Level.FINER, "Altered response: " + response);
}
return connection;
}
private void pushBackendLBVia(SipServletRequestImpl req) throws SipRoutingException {
if(!ProxyKeyExtractor.isSipRequestProxied(req)){
// Issue#1418
// The request has arrived at this back-end instance without passing a front-end;
// push a dummy via to make sure that branch-ID is created in the same way as if the
// request had been proxied by a front-end instance.
// This is to make sure that branch-ID for a CANCEL is the same as the initial request,
// regardless if the CANCEL and the initial request arrives to the back-end via different
// paths (directly or via front-end).
SipLoadBalancerIncomingHandler.pushVia(req,
localAddress.getHostName(), localAddress.getPort(), false);
}
}
private void popBackendLBVia(SipServletResponseImpl response) {
// Issue #1418
// Check if the origin request arrived at the back-end directly and if
// the dummy Via with the 'belb' parameter was pushed. In that case pop
// that Via.
if (getTopVia(response).getParameter(CLBConstants.BE_LB_PARAM) != null) {
popTopVia(response);
}
}
private ViaImpl getTopVia(SipServletMessageImpl msg) {
return new ViaImpl(msg.getRawHeader(Header.VIA).getValue());
}
private void popTopVia(SipServletMessageImpl msg) {
Header viaHeader = msg.getRawHeader(Header.VIA);
viaHeader.setReadOnly(false);
ListIterator<String> viaIter = viaHeader.getValues();
while (viaIter.hasNext()) {
viaIter.next();
viaIter.remove();
break;
}
viaHeader.setReadOnly(true);
}
private void replaceTopVia(SipServletMessageImpl msg, ViaImpl newTopVia) {
Header viaHeader = msg.getRawHeader(Header.VIA);
viaHeader.setReadOnly(false);
ListIterator<String> viaIter = viaHeader.getValues();
while (viaIter.hasNext()) {
viaIter.next();
viaIter.remove();
break;
}
viaHeader.setValue(newTopVia.toString(), true);
viaHeader.setReadOnly(true);
}
private void decodeRemote(SipServletMessageImpl msg)
throws SipRoutingException {
String remote = msg.getHeader(Header.PROXY_REMOTE_HEADER);
if (remote != null) {
msg.removeHeader(Header.PROXY_REMOTE_HEADER);
StringTokenizer st = new StringTokenizer(remote, ":", false);
String[] addrArr = new String[3];
int i = 0;
while (st.hasMoreTokens() && (i < addrArr.length)) {
addrArr[i++] = st.nextToken();
}
// Now addrArr[0] = transport, addrArr[1]=IP address and
// addrArr[2]=port
if (i == addrArr.length) {
try {
msg.setInitialRemote(
new TargetTuple(SipTransports.getTransport(addrArr[0]),
addrArr[1], Integer.parseInt(addrArr[2])));
} catch (NumberFormatException e) {
throw new SipRoutingException("Malformed " +
Header.PROXY_REMOTE_HEADER, e);
} catch (Exception e) {
throw new SipRoutingException("Malformed " +
Header.PROXY_REMOTE_HEADER, e);
}
}
}
}
private void decodeClientCert(SipServletMessageImpl message)
throws SipRoutingException {
ListIterator<String> headers = message.getHeaders(Header.PROXY_AUTH_CERT_HEADER);
List<X509Certificate> certs = new ArrayList<X509Certificate>();
while (headers.hasNext()) {
String header = headers.next();
message.removeHeader(Header.PROXY_AUTH_CERT_HEADER);
try {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
byte[] clientCertBytes = LoadbalancerUtil.decodeParameterToBytes(header,
true);
ByteArrayInputStream bais = new ByteArrayInputStream(clientCertBytes);
certs.add((X509Certificate) cf.generateCertificate(bais));
} catch (CertificateException e) {
throw new SipRoutingException("Could not decode " +
Header.PROXY_AUTH_CERT_HEADER, e);
} catch (IOException e) {
throw new SipRoutingException("Could not decode " +
Header.PROXY_AUTH_CERT_HEADER, e);
}
}
if (certs.size() > 0) {
if (message instanceof SipServletRequest) {
message.setCertificate(certs.toArray(new X509Certificate[certs.size()]));
} else {
message.setCertificate(certs.toArray(new X509Certificate[certs.size()]));
}
}
}
private void saveIncomingConnection(SipServletMessageImpl msg) {
msg.setSystemAttribute(CLBConstants.CONNID_ATTR,
new Connection(msg.getRemote().getProtocol(),
new Socket(msg.getLocal().getHostName(),
msg.getLocal().getPort()),
new Socket(msg.getRemote().getIP(), msg.getRemote().getPort())));
}
}