org.mobicents.servlet.sip.router.DefaultApplicationRouter Maven / Gradle / Ivy
/*
* TeleStax, Open Source Cloud Communications
* Copyright 2011-2014, Telestax Inc and individual contributors
* by the @authors tag.
*
* This program is free software: you can redistribute it and/or modify
* under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation; either version 3 of
* the License, or (at your option) any later version.
*
* 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see
*
* This file incorporates work covered by the following copyright contributed under the GNU LGPL : Copyright 2007-2011 Red Hat.
*/
package org.mobicents.servlet.sip.router;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.io.StringReader;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.sip.SipServletRequest;
import javax.servlet.sip.SipSession;
import javax.servlet.sip.ar.SipApplicationRouter;
import javax.servlet.sip.ar.SipApplicationRouterInfo;
import javax.servlet.sip.ar.SipApplicationRoutingDirective;
import javax.servlet.sip.ar.SipApplicationRoutingRegion;
import javax.servlet.sip.ar.SipRouteModifier;
import javax.servlet.sip.ar.SipTargetedRequestInfo;
import org.apache.log4j.Logger;
/**
* Implementation of the default application router as defined per JSR 289 Appendix C
*
*
* As an application router component is essential for the functioning of the container the following application router logic
* SHOULD be available with every container compliant with this specification. The container implementations MAY chose to
* provide a much richer application router component. For the purpose of this discussion the application router defined in this
* appendix is termed as Default Application Router (DAR).
*
*
*
* The Application Router and the Container have a simple contract defined by the SipApplicationRouter
interface.
* The DAR MUST implement all the methods of that interface as described in this document.
*
* The DAR Configuration File
*
* The DAR works off a simple configuration text file which is modeled as a Java properties file:
*
*
* - The properties file MUST be made available to the DAR and the location/content of this file MUST be accessible from a
* hierarchical URI which itself is to be supplied as a system property "javax.servlet.sip.dar"
* - The propeties file has a simple format in which the name of the property is the SIP method and the value is a simple
* comma separated stringified value for the SipRouterInfo object.
*
*
* INVITE: (sip-router-info-1), (sip-router-info-2)..
* SUBSCRIBE: (sip-router-info-3), (sip-router-info-4)..
*
*
*
* - The properties file is first read by the DAR when the
init()
is first called on the DAR. The arguments
* passed in the init()
are ignored.
*
* - The properties file is refreshed each time
applicationDeployed()
or applicationUndeployed()
is
* called, similar to init the argument of these two invocations are ignored, these callbacks act just as a trigger to read the
* file afresh.
*
*
*
* The sip-router-info data that goes in the properties file is a stringified version of the SipRouterInfo object. It consists
* of the following information :
*
* - The name of the application as known to the container
* - The identity of the subscriber that the DAR returns. It can return any header in the SIP request using the DAR directive
* DAR:SIP_HEADER e.g "DAR:From" would return the sip uri in From header. Or alternatively it can return any string.
*
* - The routing region, one of the strings "ORIGINATING", "TERMINATING" or "NEUTRAL"
* - A SIP URI indicating the route as returned by the application route, it can be an empty string.
* - A route modifier which can be any one of the strings "ROUTE", "NO_ROUTE", or "CLEAR_ROUTE"
* - A string representing stateInfo. As stateInfo is for application router's internal use only, what goes in this is up to
* the individual DAR implementations. As a hint the stateInfo could contain the index into the list of sip-router-info that was
* returned last.
*
*
*
*
* Following is an example of the DAR configuration file:
*
*
* INVITE: ("OriginatingCallWaiting", "DAR:From", "ORIGINATING", "",
* "NO_ROUTE", "0"), ("CallForwarding", "DAR:To", "TERMINATING", "",
* NO_ROUTE", "1")
*
*
* In this example the DAR is setup to invoke two applications on INVITE request one each in the originating and the terminating
* half. The applications are identified by their names as defined in the application deployment descriptors and used here. The
* subscriber identity returned in this case is the URI from the From and To header respectively for the two applications. The
* DAR does not return any route to the container and maintains the invocation state in the stateInfo as the index of the last
* application in the list.
*
*
* The DAR Operation
* The key interaction point between the Container and the Application Router is the method
*
*
* SipApplicationRouterInfo getNextApplication(SipServletRequest initialRequest, SipApplicationRoutingRegion region,
* SipApplicationRoutingDirective directive, java.lang.String stateInfo);
*
*
* This method is invoked when an initial request is received by the container. When this method is invoked on DAR it will make
* use of the stateInfo and the initialRequest parameters and find out what SIP method is in the request. Next it will create
* the object
* SipApplicationRouterInfo
from the sip-router-info information in the properties file, starting from the first in the
* list. The stateInfo could contain the index of the last sip-router-info returned so on next invocation of getNextApplication
* the DAR proceeds to the next sip-router- info in the list. The order of declaration of sip-router-info becomes the priority
* order of invocation.
*
*
* As you would notice this is a minimalist application router with no processing logic besides the declaration of the
* application order. It is expected that in the real world deployments the application router shall play an extremely important
* role in application orchestration and composition and shall make use of complex rules and diverse data repositories. The DAR
* is specified as being a very simple implementation that shall be available as part of the reference implementation of this
* specification and can be used in place of an application router.
*
*
*/
public class DefaultApplicationRouter implements SipApplicationRouter, ManageableApplicationRouter {
private static final String DIRECTION_PARAMETER = "DIRECTION";
private static final String REGEX_PARAMETER = "REGEX";
private static final String REGEX_POPPED_ROUTE_PARAMETER = "REGEX_POPPED_ROUTE";
private static final String DIRECTION_OUTBOUND = "OUTBOUND";
private static final String DIRECTION_INBOUND = "INBOUND";
private static final String DIRECTION_UAC_ROUTE_BACK = "UAC_ROUTE_BACK";
// the logger
private static Logger log = Logger.getLogger(DefaultApplicationRouter.class);
// the prefix used in the dar configuration file to specify the subscriber URI to use
// when reusing the information from one header
private static final String DAR_SUSCRIBER_PREFIX = "DAR:";
private static final String FROM = "From";
private static final String TO = "To";
private static final int DAR_SUSCRIBER_PREFIX_LENGTH = DAR_SUSCRIBER_PREFIX.length();
private static final String METHOD_WILDCARD = "ALL";
// the parser for the properties file
private DefaultApplicationRouterParser defaultApplicationRouterParser;
// Applications deployed within the container
Set containerDeployedApplicationNames = null;
// List of applications defined in the defautl application router properties file
Map> defaultSipApplicationRouterInfos;
List conditions;
/**
* Default Constructor
*/
public DefaultApplicationRouter() {
containerDeployedApplicationNames = Collections.newSetFromMap(new ConcurrentHashMap());
defaultApplicationRouterParser = new DefaultApplicationRouterParser();
defaultSipApplicationRouterInfos = new ConcurrentHashMap>();
conditions = new ArrayList();
conditions.add(new HeaderRegexCondition());
}
/**
* {@inheritDoc}
*/
public void applicationDeployed(List newlyDeployedApplicationNames) {
init();
containerDeployedApplicationNames.addAll(newlyDeployedApplicationNames);
}
/**
* {@inheritDoc}
*/
public void applicationUndeployed(List undeployedApplicationNames) {
init();
containerDeployedApplicationNames.removeAll(undeployedApplicationNames);
}
/**
* {@inheritDoc}
*/
public void destroy() {
containerDeployedApplicationNames.clear();
}
/**
* {@inheritDoc}
*/
public SipApplicationRouterInfo getNextApplication(SipServletRequest initialRequest, SipApplicationRoutingRegion region,
SipApplicationRoutingDirective directive, SipTargetedRequestInfo targetedRequestInfo, Serializable stateInfo) {
// Minimalist application router implementation with no processing logic
// besides the declaration of the application order as specified in JSR 289 - Appendix C
SipApplicationRouterInfo sipApplicationRouterInfo = null;
if (initialRequest != null) {
if (log.isDebugEnabled()) {
log.debug(this + " checking for next application for request " + initialRequest + " , region=" + region
+ " , directive=" + directive + ", targetedRequestInfo=" + targetedRequestInfo + ", stateinfo="
+ stateInfo + " with following dar " + defaultApplicationRouterParser.getProperties());
}
List extends SipApplicationRouterInfo> defaultSipApplicationRouterInfoList = defaultSipApplicationRouterInfos
.get(initialRequest.getMethod());
sipApplicationRouterInfo = getNextApplication(initialRequest, stateInfo, defaultSipApplicationRouterInfoList);
if (sipApplicationRouterInfo == null) {
defaultSipApplicationRouterInfoList = defaultSipApplicationRouterInfos.get(METHOD_WILDCARD);
sipApplicationRouterInfo = getNextApplication(initialRequest, stateInfo, defaultSipApplicationRouterInfoList);
}
if (sipApplicationRouterInfo != null) {
return sipApplicationRouterInfo;
}
}
return new SipApplicationRouterInfo(null, null, null, null, null, null);
}
/*
* This method is checking if the application that initiated the request is currently configured to be called for this
* method. Apps that initiate request may not be in the list.
*/
private DefaultSipApplicationRouterInfo getFirstRequestApplicationEntry(
List extends SipApplicationRouterInfo> defaultSipApplicationRouterInfoList, SipServletRequest initialRequest) {
SipSession sipSession = initialRequest.getSession(false);
if (sipSession != null) {
String appName = sipSession.getApplicationSession().getApplicationName();
Iterator extends SipApplicationRouterInfo> iterator = defaultSipApplicationRouterInfoList.iterator();
while (iterator.hasNext()) {
DefaultSipApplicationRouterInfo next = (DefaultSipApplicationRouterInfo) iterator.next();
if (next.getApplicationName().equals(appName))
return next;
}
}
return null;
}
private SipApplicationRouterInfo getNextApplication(SipServletRequest initialRequest, Serializable stateInfo,
List extends SipApplicationRouterInfo> defaultSipApplicationRouterInfoList) {
if (defaultSipApplicationRouterInfoList != null && defaultSipApplicationRouterInfoList.size() > 0) {
int previousAppOrder = 0;
if (stateInfo != null) {
previousAppOrder = (Integer) stateInfo;
if (log.isDebugEnabled()) {
log.debug("The previous app order was : " + previousAppOrder);
}
}
ListIterator extends SipApplicationRouterInfo> defaultSipApplicationRouterInfoIt = defaultSipApplicationRouterInfoList
.listIterator(previousAppOrder++);
while (defaultSipApplicationRouterInfoIt.hasNext()) {
/*
* Fix for http://code.google.com/p/mobicents/issues/detail?id=987 Issue 987
*
* INBOUND and OUTBOUND request routing logic is as follows: Determine if the request was initiated by the
* previous application by either see the application missing in the DAR or by looking at the DIRECTION hint in
* the optional parameters. If the request was initiated by the app then we will call only applications without
* INBOUND direction. All applications without hint will be called to keep backward compatibility.
*/
DefaultSipApplicationRouterInfo defaultSipApplicationRouterInfo = (DefaultSipApplicationRouterInfo) defaultSipApplicationRouterInfoIt
.next();
/*
* This method is checking if the application that initiated the request is currently configured to be called
* for this method. Apps that initiate request may not be in the list thus params must be assumed for them.
*/
DefaultSipApplicationRouterInfo requestSipApplicationRouterInfo = (DefaultSipApplicationRouterInfo) getFirstRequestApplicationEntry(
defaultSipApplicationRouterInfoList, initialRequest);
String currentDirection = defaultSipApplicationRouterInfo.getOptionalParameters().get(DIRECTION_PARAMETER);
String requestDirection = null;
if (requestSipApplicationRouterInfo != null) {
requestDirection = requestSipApplicationRouterInfo.getOptionalParameters().get(DIRECTION_PARAMETER);
} else {
if (initialRequest.getSession(false) != null) {
// If this request comes from outside (was not initiated by some app) the session will be null,
// thus if it's not null we can assume the request already has a session and was initiated by
// the application...
requestDirection = DIRECTION_OUTBOUND;
}
}
if (DIRECTION_OUTBOUND.equalsIgnoreCase(requestDirection)) { // If the request was initiated by the previous app
// or the previous app has out marker
if (DIRECTION_INBOUND.equalsIgnoreCase(currentDirection)) { // but the new application is handling only
// INBOUND request
if (log.isDebugEnabled()) {
log.debug(defaultSipApplicationRouterInfo.getApplicationName()
+ " will not be called because we are routing the request out and the application has 'DIRECTION=INBOUND' hint.");
}
continue; // .. then just don't call the application
}
}
String regEx = defaultSipApplicationRouterInfo.getOptionalParameters().get(REGEX_PARAMETER);
if (regEx != null) {
Pattern pattern = Pattern.compile(regEx);
Matcher matcher = pattern.matcher(initialRequest.toString());
if (matcher.find()) {
if (log.isDebugEnabled()) {
log.debug("initialRequest " + initialRequest + " matching regex pattern " + regEx + "begin index "
+ matcher.start() + " and ending at index " + matcher.end() + " for application "
+ defaultSipApplicationRouterInfo.getApplicationName());
}
} else {
if (log.isDebugEnabled()) {
log.debug("initialRequest " + initialRequest + " not matching regex pattern " + regEx
+ " skipping application " + defaultSipApplicationRouterInfo.getApplicationName());
}
continue; // pattern not matching, just don't call the application
}
}
// https://code.google.com/p/sipservlets/issues/detail?id=43
String regExPoppedRoute = defaultSipApplicationRouterInfo.getOptionalParameters().get(
REGEX_POPPED_ROUTE_PARAMETER);
if (regExPoppedRoute != null) {
Pattern pattern = Pattern.compile(regExPoppedRoute);
Matcher matcher = pattern.matcher(initialRequest.getPoppedRoute().toString());
if (matcher.find()) {
if (log.isDebugEnabled()) {
log.debug("initialRequest Popped Route" + initialRequest.getPoppedRoute()
+ " matching regex pattern " + regExPoppedRoute + "begin index " + matcher.start()
+ " and ending at index " + matcher.end() + " for application "
+ defaultSipApplicationRouterInfo.getApplicationName());
}
} else {
if (log.isDebugEnabled()) {
log.debug("initialRequest Popped Route" + initialRequest.getPoppedRoute()
+ " matching regex pattern " + regExPoppedRoute + " skipping application "
+ defaultSipApplicationRouterInfo.getApplicationName());
}
continue; // pattern not matching, just don't call the application
}
}
if (!checkConditions(initialRequest, defaultSipApplicationRouterInfo)) {
continue;
}
boolean isApplicationPresentInContainer = false;
if (containerDeployedApplicationNames.contains(defaultSipApplicationRouterInfo.getApplicationName())) {
isApplicationPresentInContainer = true;
if (log.isDebugEnabled()) {
log.debug(defaultSipApplicationRouterInfo.getApplicationName() + " is present in the container.");
}
} else {
if(log.isDebugEnabled()) {
log.debug("this " + this + " " + defaultSipApplicationRouterInfo.getApplicationName() + " is NOT present in the container.");
}
}
if (log.isDebugEnabled()) {
log.debug("Route Modifier : " + defaultSipApplicationRouterInfo.getRouteModifier());
log.debug("Previous App Name : "
+ ((SipApplicationRouterInfo) defaultSipApplicationRouterInfoList.listIterator(previousAppOrder)
.previous()).getNextApplicationName());
log.debug("Previous App Route Region : "
+ ((SipApplicationRouterInfo) defaultSipApplicationRouterInfoList.listIterator(previousAppOrder)
.previous()).getRoutingRegion());
log.debug("Current App Name : " + defaultSipApplicationRouterInfo.getNextApplicationName());
log.debug("Current App Route Region : " + defaultSipApplicationRouterInfo.getRoutingRegion());
}
// if application is deployed in the container or if the intention is to route outside even if the application
// is not deployed
if (isApplicationPresentInContainer
|| !SipRouteModifier.NO_ROUTE.equals(defaultSipApplicationRouterInfo.getRouteModifier())) {
// prevents to route twice in a row to the same application with the same routing region
SipSession initialSession = initialRequest.getSession(false);
String initialAppName = null;
String initialRoutingRegion = null;
String defaultSipApplicationRouterAppName = null;
String defaultSipApplicationRouterRoutingRegion = null;
if (initialSession != null) {
initialAppName = initialRequest.getSession(false).getApplicationSession().getApplicationName();
initialRoutingRegion = ((SipApplicationRouterInfo) defaultSipApplicationRouterInfoList.listIterator(
previousAppOrder).previous()).getRoutingRegion().toString();
defaultSipApplicationRouterAppName = defaultSipApplicationRouterInfo.getApplicationName();
defaultSipApplicationRouterRoutingRegion = defaultSipApplicationRouterInfo.getRoutingRegion()
.toString();
}
if (initialSession == null || !defaultSipApplicationRouterAppName.equals(initialAppName) ||
// https://code.google.com/p/sipservlets/issues/detail?id=273 allowing to route to the same app with
// different routing regions
((defaultSipApplicationRouterAppName.equals(initialAppName) && !(initialRoutingRegion
.equals(defaultSipApplicationRouterRoutingRegion)))) ||
// https://github.com/Mobicents/sip-servlets/issues/94
((requestDirection != null && defaultSipApplicationRouterAppName.equals(initialAppName) &&
DIRECTION_UAC_ROUTE_BACK.equals(requestDirection) && stateInfo == null))) {
String subscriberIdentity = defaultSipApplicationRouterInfo.getSubscriberIdentity();
if (subscriberIdentity.indexOf(DAR_SUSCRIBER_PREFIX) != -1) {
String headerName = subscriberIdentity.substring(DAR_SUSCRIBER_PREFIX_LENGTH);
if (FROM.equalsIgnoreCase(headerName)) {
subscriberIdentity = initialRequest.getFrom().getURI().toString();
} else if (TO.equalsIgnoreCase(headerName)) {
subscriberIdentity = initialRequest.getTo().getURI().toString();
} else {
subscriberIdentity = initialRequest.getHeader(headerName);
}
}
return new SipApplicationRouterInfo(defaultSipApplicationRouterInfo.getApplicationName(),
defaultSipApplicationRouterInfo.getRoutingRegion(), subscriberIdentity,
defaultSipApplicationRouterInfo.getRoutes(),
defaultSipApplicationRouterInfo.getRouteModifier(), defaultSipApplicationRouterInfo.getOrder());
}
}
}
}
return null;
}
private boolean checkConditions(SipServletRequest initialRequest, DefaultSipApplicationRouterInfo info) {
boolean allConditionsMet = true;
Iterator iterator = conditions.iterator();
while (allConditionsMet && iterator.hasNext()) {
boolean condMet = iterator.next().checkCondition(initialRequest, info);
allConditionsMet = allConditionsMet && condMet;
}
return allConditionsMet;
}
/**
* load the configuration file as defined in appendix C of JSR289
*/
public void init() {
defaultApplicationRouterParser.init();
try {
defaultSipApplicationRouterInfos = defaultApplicationRouterParser.parse();
} catch (ParseException e) {
log.fatal("Impossible to parse the default application router configuration file", e);
throw new IllegalArgumentException("Impossible to parse the default application router configuration file", e);
}
}
/*
* (non-Javadoc)
*
* @see javax.servlet.sip.ar.SipApplicationRouter#init(java.util.Properties)
*/
public void init(Properties properties) {
defaultApplicationRouterParser.init(properties);
try {
defaultSipApplicationRouterInfos = defaultApplicationRouterParser.parse();
} catch (ParseException e) {
log.fatal("Impossible to parse the default application router configuration file", e);
throw new IllegalArgumentException("Impossible to parse the default application router configuration file", e);
}
}
/*
* (non-Javadoc)
*
* @see org.mobicents.servlet.sip.router.ManageableApplicationRouter#configure(java.lang.Object)
*/
public void configure(Object configuration) {
if (!(configuration instanceof Properties) && !(configuration.toString() instanceof String))
throw new IllegalArgumentException("Configuration for DAR must be of type Properties or String; "
+ configuration.getClass().getName() + " was received");
Properties properties = new Properties();
if (configuration instanceof String) {
try {
properties.load(new StringReader((String) configuration));
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
} else if (configuration instanceof Properties) {
properties = (Properties) configuration;
}
try {
defaultSipApplicationRouterInfos = this.defaultApplicationRouterParser.parse(properties);
} catch (ParseException e1) {
throw new IllegalArgumentException("Failed to parse the new DAR properties", e1);
}
String configFileLocation = defaultApplicationRouterParser.getDarConfigurationFileLocation();
if (configFileLocation.startsWith("file:/")) {
int i = 5;
while (configFileLocation.charAt(i) == '/')
i++;
configFileLocation = configFileLocation.substring(i - 1);
} else {
log.warn("Can not write persist DAR configuration to "
+ configFileLocation
+ ". Make sure you have write permissions and make sure it's a local file. NOTE THAT THE NEW DAR CONFIGURATION IS LOADED AND EFFECTIVE.");
return;
}
if (configFileLocation == null || configFileLocation.length() < 1)
throw new IllegalStateException("Configuration file name is empty.");
File configFile = new File(configFileLocation);
FileOutputStream fis = null;
try {
fis = new FileOutputStream(configFile);
properties.store(fis, "Application Router Configuration");
} catch (Exception e) {
throw new IllegalStateException("Failed to store configuration file.", e);
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
log.error("fail to close the following file " + configFile.getAbsolutePath(), e);
}
}
}
log.info("Stored DAR configuration in " + configFile.getAbsolutePath());
}
/*
* (non-Javadoc)
*
* @see org.mobicents.servlet.sip.router.ManageableApplicationRouter#getCurrentConfiguration()
*/
public Object getCurrentConfiguration() {
return defaultApplicationRouterParser.getProperties();
}
/*
* (non-Javadoc)
*
* @see org.mobicents.servlet.sip.router.ManageableApplicationRouter#getCurrentConfiguration()
*/
public Map> getConfiguration() {
return defaultSipApplicationRouterInfos;
}
}