/*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.mobicents.servlet.sip.startup;
import gov.nist.javax.sip.SipStackExt;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.SocketException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import javax.management.MBeanRegistration;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.sip.ListeningPoint;
import javax.sip.SipProvider;
import javax.sip.SipStack;
import net.java.stun4j.StunAddress;
import net.java.stun4j.client.NetworkConfigurationDiscoveryProcess;
import net.java.stun4j.client.StunDiscoveryReport;
import org.apache.coyote.Adapter;
import org.apache.coyote.ProtocolHandler;
import org.apache.log4j.Logger;
import org.apache.tomcat.util.modeler.Registry;
import org.mobicents.servlet.sip.JainSipUtils;
import org.mobicents.servlet.sip.SipFactories;
import org.mobicents.servlet.sip.core.DNSAddressResolver;
import org.mobicents.servlet.sip.core.ExtendedListeningPoint;
import org.mobicents.servlet.sip.core.SipApplicationDispatcher;
/**
* This is the sip protocol handler that will get called upon creation of the
* tomcat connector defined in the server.xml.<br/> To use a sip connector, one
* need to specify a new connector in server.xml with
* org.mobicents.servlet.sip.startup.SipProtocolHandler as the value for the
* protocol attribute.<br/>
*
* Some of the fields (representing the sip stack propeties) get populated
* automatically by the container.<br/>
*
* @author Jean Deruelle
*
*/
public class SipProtocolHandler implements ProtocolHandler, MBeanRegistration {
private static final String AUTOMATIC_DIALOG_SUPPORT_STACK_PROP = "javax.sip.AUTOMATIC_DIALOG_SUPPORT";
private static final String LOOSE_DIALOG_VALIDATION = "gov.nist.javax.sip.LOOSE_DIALOG_VALIDATION";
private static final String SERVER_LOG_STACK_PROP = "gov.nist.javax.sip.SERVER_LOG";
private static final String DEBUG_LOG_STACK_PROP = "gov.nist.javax.sip.DEBUG_LOG";
private static final String IS_SIP_CONNECTOR = "isSipConnector";
private static final String DEFAULT_SIP_PATH_NAME = "gov.nist";
// the logger
private static transient Logger logger = Logger.getLogger(SipProtocolHandler.class.getName());
// *
protected ObjectName tpOname = null;
// *
protected ObjectName rgOname = null;
/**
* The random port number generator that we use in getRandomPortNumer()
*/
private static Random portNumberGenerator = new Random();
private Adapter adapter = null;
private Map<String, Object> attributes = new HashMap<String, Object>();
private SipStack sipStack;
/*
* the extended listening point with global ip address and port for it
*/
public ExtendedListeningPoint extendedListeningPoint;
// sip stack attributes defined by the server.xml
private String sipStackPropertiesFileLocation;
// defining sip stack properties
private Properties sipStackProperties;
/**
* the sip stack signaling transport
*/
private String sipPathName;
/**
* the sip stack signaling transport
*/
private String signalingTransport;
/**
* the sip stack listening port
*/
private int port;
/*
* IP address for this protocol handler.
*/
private String ipAddress;
/*
* use Stun
*/
private boolean useStun;
/*
* Stun Server Address
*/
private String stunServerAddress;
/*
* Stun Server Port
*/
private int stunServerPort;
/*
* use Pretty Encoding
*/
private boolean usePrettyEncoding = true;
/*
* These settings staticServerAddress, staticServerPort will override all stun settings and will
* put the server address in the Via/RR/Contact headers
*/
private String staticServerAddress;
private String staticServerPort;
private boolean useStaticAddress;
/**
* {@inheritDoc}
*/
public void destroy() throws Exception {
if(logger.isDebugEnabled()) {
logger.debug("Stopping a sip protocol handler");
}
//Jboss specific unloading case
SipApplicationDispatcher sipApplicationDispatcher = (SipApplicationDispatcher)
getAttribute(SipApplicationDispatcher.class.getSimpleName());
if(sipApplicationDispatcher != null) {
if(logger.isDebugEnabled()) {
logger.debug("Removing the Sip Application Dispatcher as a sip listener for listening point " + extendedListeningPoint);
}
extendedListeningPoint.getSipProvider().removeSipListener(sipApplicationDispatcher);
sipApplicationDispatcher.getSipNetworkInterfaceManager().removeExtendedListeningPoint(extendedListeningPoint);
}
// removing listening point and sip provider
if(logger.isDebugEnabled()) {
logger.debug("Removing the following Listening Point " + extendedListeningPoint);
}
sipStack.deleteSipProvider(extendedListeningPoint.getSipProvider());
if(logger.isDebugEnabled()) {
logger.debug("Removing the sip provider");
}
sipStack.deleteListeningPoint(extendedListeningPoint.getListeningPoint());
extendedListeningPoint = null;
// stopping the sip stack
if(!sipStack.getListeningPoints().hasNext() && !sipStack.getSipProviders().hasNext()) {
sipStack.stop();
sipStack = null;
logger.info("Sip stack stopped");
}
if (tpOname!=null)
Registry.getRegistry(null, null).unregisterComponent(tpOname);
if (rgOname != null)
Registry.getRegistry(null, null).unregisterComponent(rgOname);
}
public Adapter getAdapter() {
return adapter;
}
/**
* {@inheritDoc}
*/
public Object getAttribute(String attribute) {
return attributes.get(attribute);
}
/**
* {@inheritDoc}
*/
public Iterator getAttributeNames() {
return attributes.keySet().iterator();
}
/**
* {@inheritDoc}
*/
public void init() throws Exception {
if(logger.isInfoEnabled()) {
logger.info("Pretty encoding of headers enabled ? " + usePrettyEncoding);
}
if(sipPathName == null) {
sipPathName = DEFAULT_SIP_PATH_NAME;
}
SipFactories.initialize(sipPathName, usePrettyEncoding);
setAttribute(IS_SIP_CONNECTOR,Boolean.TRUE);
}
public void pause() throws Exception {
//This is optionnal, no implementation there
}
public void resume() throws Exception {
// This is optionnal, no implementation there
}
public void setAdapter(Adapter adapter) {
this.adapter = adapter;
}
/**
* {@inheritDoc}
*/
public void setAttribute(String arg0, Object arg1) {
attributes.put(arg0, arg1);
}
public void start() throws Exception {
if(logger.isDebugEnabled()) {
logger.debug("Starting a sip protocol handler");
}
String catalinaHome = System.getProperty("catalina.home");
if (catalinaHome == null) {
catalinaHome = System.getProperty("catalina.base");
}
if(catalinaHome == null) {
catalinaHome = ".";
}
if(sipStackPropertiesFileLocation != null && !sipStackPropertiesFileLocation.startsWith("file:///")) {
sipStackPropertiesFileLocation = "file:///" + catalinaHome.replace(File.separatorChar, '/') + "/" + sipStackPropertiesFileLocation;
}
sipStackProperties = new Properties();
boolean isPropsLoaded = false;
if (logger.isDebugEnabled()) {
logger.debug("Loading SIP stack properties from following file : " + sipStackPropertiesFileLocation);
}
if(sipStackPropertiesFileLocation != null) {
//hack to get around space char in path see http://weblogs.java.net/blog/kohsuke/archive/2007/04/how_to_convert.html,
// we create a URL since it's permissive enough
File sipStackPropertiesFile = null;
URL url = null;
try {
url = new URL(sipStackPropertiesFileLocation);
} catch (MalformedURLException e) {
logger.fatal("Cannot find the sip stack properties file ! ",e);
throw new IllegalArgumentException("The Default Application Router file Location : "+sipStackPropertiesFileLocation+" is not valid ! ",e);
}
try {
sipStackPropertiesFile = new File(new URI(sipStackPropertiesFileLocation));
} catch (URISyntaxException e) {
//if the uri contains space this will fail, so getting the path will work
sipStackPropertiesFile = new File(url.getPath());
}
FileInputStream sipStackPropertiesInputStream = null;
try {
sipStackPropertiesInputStream = new FileInputStream(sipStackPropertiesFile);
sipStackProperties.load(sipStackPropertiesInputStream);
} catch (Exception e) {
logger.warn("Could not find or problem when loading the sip stack properties file : " + sipStackPropertiesFileLocation, e);
} finally {
if(sipStackPropertiesInputStream != null) {
try {
sipStackPropertiesInputStream.close();
} catch (IOException e) {
logger.error("fail to close the following file " + sipStackPropertiesFile.getAbsolutePath(), e);
}
}
}
String debugLog = sipStackProperties.getProperty(DEBUG_LOG_STACK_PROP);
if(debugLog != null && debugLog.length() > 0 && !debugLog.startsWith("file:///")) {
sipStackProperties.setProperty(DEBUG_LOG_STACK_PROP,
catalinaHome + "/" + debugLog);
}
String serverLog = sipStackProperties.getProperty(SERVER_LOG_STACK_PROP);
if(serverLog != null && serverLog.length() > 0 && !serverLog.startsWith("file:///")) {
sipStackProperties.setProperty(SERVER_LOG_STACK_PROP,
catalinaHome + "/" + serverLog);
}
// The whole MSS is built upon those assumptions, so those properties are not overrideable
sipStackProperties.setProperty(AUTOMATIC_DIALOG_SUPPORT_STACK_PROP, "off");
sipStackProperties.setProperty(LOOSE_DIALOG_VALIDATION, "true");
isPropsLoaded = true;
} else {
logger.warn("no sip stack properties file defined ");
}
if(!isPropsLoaded) {
logger.warn("loading default Mobicents Sip Servlets sip stack properties");
// Silently set default values
sipStackProperties.setProperty("gov.nist.javax.sip.LOG_MESSAGE_CONTENT",
"true");
sipStackProperties.setProperty("gov.nist.javax.sip.TRACE_LEVEL",
"32");
sipStackProperties.setProperty(DEBUG_LOG_STACK_PROP,
catalinaHome + "/" + "mss-jsip-" + ipAddress + "-" + port+"-debug.txt");
sipStackProperties.setProperty(SERVER_LOG_STACK_PROP,
catalinaHome + "/" + "mss-jsip-" + ipAddress + "-" + port+"-messages.xml");
sipStackProperties.setProperty("javax.sip.STACK_NAME", "mss-" + ipAddress + "-" + port);
sipStackProperties.setProperty(AUTOMATIC_DIALOG_SUPPORT_STACK_PROP, "off");
sipStackProperties.setProperty("gov.nist.javax.sip.DELIVER_UNSOLICITED_NOTIFY", "true");
sipStackProperties.setProperty("gov.nist.javax.sip.THREAD_POOL_SIZE", "64");
sipStackProperties.setProperty("gov.nist.javax.sip.REENTRANT_LISTENER", "true");
sipStackProperties.setProperty(LOOSE_DIALOG_VALIDATION, "true");
}
try {
//checking the external ip address if stun enabled
String globalIpAddress = null;
int globalPort = -1;
if (useStun) {
if(InetAddress.getByName(ipAddress).isLoopbackAddress()) {
logger.warn("The Ip address provided is the loopback address, stun won't be enabled for it");
} else {
//chooses stun port randomly
DatagramSocket randomSocket = initRandomPortSocket();
int randomPort = randomSocket.getLocalPort();
randomSocket.disconnect();
randomSocket.close();
randomSocket = null;
StunAddress localStunAddress = new StunAddress(ipAddress,
randomPort);
StunAddress serverStunAddress = new StunAddress(
stunServerAddress, stunServerPort);
NetworkConfigurationDiscoveryProcess addressDiscovery = new NetworkConfigurationDiscoveryProcess(
localStunAddress, serverStunAddress);
addressDiscovery.start();
StunDiscoveryReport report = addressDiscovery
.determineAddress();
if(report.getPublicAddress() != null) {
globalIpAddress = report.getPublicAddress().getSocketAddress().getAddress().getHostAddress();
globalPort = report.getPublicAddress().getPort();
//TODO set a timer to retry the binding and provide a callback to update the global ip address and port
} else {
useStun = false;
logger.error("Stun discovery failed to find a valid public ip address, disabling stun !");
}
logger.info("Stun report = " + report);
addressDiscovery.shutDown();
}
}
if(logger.isInfoEnabled()) {
logger.info("Mobicents Sip Servlets sip stack properties : " + sipStackProperties);
}
// Create SipStack object
sipStack = SipFactories.sipFactory.createSipStack(sipStackProperties);
ListeningPoint listeningPoint = sipStack.createListeningPoint(ipAddress,
port, signalingTransport);
if(useStun) {
// TODO: (ISSUE-CONFUSION) Check what is the confusion here, why not use the globalport. It ends up putting the local port everywhere
// listeningPoint.setSentBy(globalIpAddress + ":" + globalPort);
listeningPoint.setSentBy(globalIpAddress + ":" + port);
}
if(useStaticAddress) {
// TODO: (ISSUE-CONFUSION) Check what is the confusion here as above
listeningPoint.setSentBy(staticServerAddress + ":" + globalPort);
}
SipProvider sipProvider = sipStack.createSipProvider(listeningPoint);
sipStack.start();
//creating the extended listening point
extendedListeningPoint = new ExtendedListeningPoint(sipProvider, listeningPoint);
extendedListeningPoint.setUseStaticAddress(useStaticAddress);
if(useStaticAddress) {
extendedListeningPoint.setGlobalIpAddress(staticServerAddress);
extendedListeningPoint.setGlobalPort(Integer.parseInt(staticServerPort));
// TODO: (ISSUE-CONFUSION) This is a bit of a hack related to the same issue
extendedListeningPoint.setPort(Integer.parseInt(staticServerPort));
if(logger.isInfoEnabled()) {
logger.info("Using static server address: " + staticServerAddress
+ " and port: " + staticServerPort);
}
} else {
extendedListeningPoint.setGlobalIpAddress(globalIpAddress);
extendedListeningPoint.setGlobalPort(globalPort);
}
//TODO add it as a listener for global ip address changes if STUN rediscover a new addess at some point
//made the sip stack and the extended listening Point available to the service implementation
setAttribute(SipStack.class.getSimpleName(), sipStack);
setAttribute(ExtendedListeningPoint.class.getSimpleName(), extendedListeningPoint);
//Jboss specific loading case
SipApplicationDispatcher sipApplicationDispatcher = (SipApplicationDispatcher)
getAttribute(SipApplicationDispatcher.class.getSimpleName());
if(sipApplicationDispatcher != null) {
if(logger.isDebugEnabled()) {
logger.debug("Adding the Sip Application Dispatcher as a sip listener for connector listening on port " + port);
}
sipProvider.addSipListener(sipApplicationDispatcher);
sipApplicationDispatcher.getSipNetworkInterfaceManager().addExtendedListeningPoint(extendedListeningPoint);
// for nist sip stack set the DNS Address resolver allowing to make DNS SRV lookups
if(sipStack instanceof SipStackExt) {
if(logger.isDebugEnabled()) {
logger.debug(sipStack.getStackName() +" will be using DNS SRV lookups as AddressResolver");
}
((SipStackExt) sipStack).setAddressResolver(new DNSAddressResolver(sipApplicationDispatcher));
}
}
logger.info("Sip Connector started on ip address : " + ipAddress
+ ",port " + port + ", useStun " + useStun + ", stunAddress " + stunServerAddress + ", stunPort : " + stunServerPort);
if (this.domain != null) {
// try {
// tpOname = new ObjectName
// (domain + ":" + "type=ThreadPool,name=" + getName());
// Registry.getRegistry(null, null)
// .registerComponent(endpoint, tpOname, null );
// } catch (Exception e) {
// logger.error("Can't register endpoint");
// }
rgOname=new ObjectName
(domain + ":type=GlobalRequestProcessor,name=" + getName());
Registry.getRegistry(null, null).registerComponent
( sipStack, rgOname, null );
}
} catch (Exception ex) {
logger.fatal(
"Bad shit happened -- check server.xml for tomcat. ", ex);
throw ex;
}
}
/**
* Initializes and binds a socket that on a random port number. The method
* would try to bind on a random port and retry 5 times until a free port
* is found.
*
* @return the socket that we have initialized on a randomport number.
*/
private DatagramSocket initRandomPortSocket() {
int bindRetries = 5;
int currentlyTriedPort =
getRandomPortNumber(JainSipUtils.MIN_PORT_NUMBER, JainSipUtils.MAX_PORT_NUMBER);
DatagramSocket resultSocket = null;
//we'll first try to bind to a random port. if this fails we'll try
//again (bindRetries times in all) until we find a free local port.
for (int i = 0; i < bindRetries; i++) {
try {
resultSocket = new DatagramSocket(currentlyTriedPort);
//we succeeded - break so that we don't try to bind again
break;
}
catch (SocketException exc) {
if (exc.getMessage().indexOf("Address already in use") == -1) {
logger.fatal("An exception occurred while trying to create"
+ "a local host discovery socket.", exc);
return null;
}
//port seems to be taken. try another one.
logger.debug("Port " + currentlyTriedPort + " seems in use.");
currentlyTriedPort =
getRandomPortNumber(JainSipUtils.MIN_PORT_NUMBER, JainSipUtils.MAX_PORT_NUMBER);
logger.debug("Retrying bind on port " + currentlyTriedPort);
}
}
return resultSocket;
}
/**
* Returns a random local port number, greater than min and lower than max.
*
* @param min the minimum allowed value for the returned port number.
* @param max the maximum allowed value for the returned port number.
*
* @return a random int located between greater than min and lower than max.
*/
public static int getRandomPortNumber(int min, int max) {
return portNumberGenerator.nextInt(max - min) + min;
}
/**
* @return the signalingTransport
*/
public String getSignalingTransport() {
return signalingTransport;
}
/**
* @param signalingTransport
* the signalingTransport to set
*/
public void setSignalingTransport(String transport) {
this.signalingTransport = transport;
}
/**
* @return the port
*/
public int getPort() {
return port;
}
/**
* @param port
* the port to set
*/
public void setPort(int port) {
this.port = port;
}
public void setIpAddress(String ipAddress) {
this.ipAddress = ipAddress;
}
public String getIpAddress() {
return ipAddress;
}
/**
* @return the stunServerAddress
*/
public String getStunServerAddress() {
return stunServerAddress;
}
/**
* @param stunServerAddress the stunServerAddress to set
*/
public void setStunServerAddress(String stunServerAddress) {
this.stunServerAddress = stunServerAddress;
}
/**
* @return the stunServerPort
*/
public int getStunServerPort() {
return stunServerPort;
}
/**
* @param stunServerPort the stunServerPort to set
*/
public void setStunServerPort(int stunServerPort) {
this.stunServerPort = stunServerPort;
}
/**
* @return the useStun
*/
public boolean isUseStun() {
return useStun;
}
/**
* @param useStun the useStun to set
*/
public void setUseStun(boolean useStun) {
this.useStun = useStun;
}
/**
* @param usePrettyEncoding the usePrettyEncoding to set
*/
public void setUsePrettyEncoding(boolean usePrettyEncoding) {
this.usePrettyEncoding = usePrettyEncoding;
}
/**
* @return the usePrettyEncoding
*/
public boolean isUsePrettyEncoding() {
return usePrettyEncoding;
}
public InetAddress getAddress() {
try {
return InetAddress.getByName(ipAddress);
} catch (UnknownHostException e) {
logger.error("unexpected exception while getting the ipaddress of the sip protocol handler", e);
return null;
}
}
public void setAddress(InetAddress ia) { ipAddress = ia.getHostAddress(); }
public String getName() {
String encodedAddr = "";
if (getAddress() != null) {
encodedAddr = "" + getAddress();
if (encodedAddr.startsWith("/"))
encodedAddr = encodedAddr.substring(1);
encodedAddr = URLEncoder.encode(encodedAddr) + "-";
}
return ("sip-" + encodedAddr + getPort());
}
/**
* @param sipStackPropertiesFile the sipStackPropertiesFile to set
*/
public void setSipStackPropertiesFile(String sipStackPropertiesFile) {
this.sipStackPropertiesFileLocation = sipStackPropertiesFile;
}
/**
* @return the sipStackPropertiesFile
*/
public String getSipStackPropertiesFile() {
return sipStackPropertiesFileLocation;
}
// -------------------- JMX related methods --------------------
public String getStaticServerAddress() {
return staticServerAddress;
}
public void setStaticServerAddress(String staticServerAddress) {
this.staticServerAddress = staticServerAddress;
}
public String getStaticServerPort() {
return staticServerPort;
}
public void setStaticServerPort(String staticServerPort) {
this.staticServerPort = staticServerPort;
}
public boolean isUseStaticAddress() {
return useStaticAddress;
}
public void setUseStaticAddress(boolean useStaticAddress) {
this.useStaticAddress = useStaticAddress;
}
// *
protected String domain;
protected ObjectName oname;
protected MBeanServer mserver;
public ObjectName getObjectName() {
return oname;
}
public String getDomain() {
return domain;
}
public ObjectName preRegister(MBeanServer server,
ObjectName name) throws Exception {
oname=name;
mserver=server;
domain=name.getDomain();
return name;
}
public void postRegister(Boolean registrationDone) {
}
public void preDeregister() throws Exception {
}
public void postDeregister() {
}
/**
* @param sipPathName the sipPathName to set
*/
public void setSipPathName(String sipPathName) {
this.sipPathName = sipPathName;
}
/**
* @return the sipPathName
*/
public String getSipPathName() {
return sipPathName;
}
}