/*
* 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.core;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.text.ParseException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import javax.servlet.sip.SipURI;
import javax.sip.ListeningPoint;
import javax.sip.address.Hop;
import javax.sip.address.URI;
import javax.sip.header.RouteHeader;
import javax.sip.header.ViaHeader;
import javax.sip.message.Message;
import javax.sip.message.Request;
import org.apache.log4j.Logger;
import org.mobicents.servlet.sip.JainSipUtils;
import org.mobicents.servlet.sip.SipFactories;
import org.mobicents.servlet.sip.address.SipURIImpl;
import org.mobicents.servlet.sip.utils.Inet6Util;
/**
* This class will be a placeholder for all sip network interfaces mapped to a sip
* standard service. It should allow one to query for local and external address
* (discovered by STUN) of a specific interface. <br/>
*
* It will also allow various queries against its network interfaces to discover
* the right one to use. Those queries could be cached in order to improve performance
*
* @author <A HREF="mailto:jean.deruelle@gmail.com">Jean Deruelle</A>
*
*/
public class SipNetworkInterfaceManager {
private static transient Logger logger = Logger.getLogger(SipNetworkInterfaceManager.class);
/**
* The maximum int value that could correspond to a port nubmer.
*/
public static final int MAX_PORT_NUMBER = 65535;
/**
* The minimum int value that could correspond to a port nubmer bindable
* by the SIP Communicator.
*/
public static final int MIN_PORT_NUMBER = 1024;
List<ExtendedListeningPoint> extendedListeningPointList = null;
List<SipURI> outboundInterfaces = null;
//related to google code issue 563, will be used to check if a request is aimed at a local network or outside
Set<String> outboundInterfacesIpAddresses = null;
//use only to lookup the hosts for issue 563, need to find a better way and reduce coupling
private SipApplicationDispatcher sipApplicationDispatcher;
//those maps are present to improve the performance of finding a listening point either from a transport
// or from a triplet ipaddress, port and transport
Map<String, List<ExtendedListeningPoint>> transportMappingCacheMap = null;
Map<String, ExtendedListeningPoint> extendedListeningPointsCacheMap = null;
Lock lock = null;
/**
* Default Constructor
*/
public SipNetworkInterfaceManager(SipApplicationDispatcher sipApplicationDispatcher) {
this.sipApplicationDispatcher = sipApplicationDispatcher;
extendedListeningPointList = new CopyOnWriteArrayList<ExtendedListeningPoint>();
outboundInterfaces = new CopyOnWriteArrayList<SipURI>();
outboundInterfacesIpAddresses = new CopyOnWriteArraySet<String>();
// creating and populating the transport cache map with transports
transportMappingCacheMap = new HashMap<String, List<ExtendedListeningPoint>>();
transportMappingCacheMap.put(ListeningPoint.TCP.toLowerCase(), new CopyOnWriteArrayList<ExtendedListeningPoint>());
transportMappingCacheMap.put(ListeningPoint.UDP.toLowerCase(), new CopyOnWriteArrayList<ExtendedListeningPoint>());
transportMappingCacheMap.put(ListeningPoint.SCTP.toLowerCase(), new CopyOnWriteArrayList<ExtendedListeningPoint>());
transportMappingCacheMap.put(ListeningPoint.TLS.toLowerCase(), new CopyOnWriteArrayList<ExtendedListeningPoint>());
// creating the ipaddress/port/transport cache map
extendedListeningPointsCacheMap = new ConcurrentHashMap<String, ExtendedListeningPoint>();
lock = new ReentrantLock();
}
/**
* Retrieve the listening points for this manager
* @return the listening points for this manager
*/
public Iterator<ExtendedListeningPoint> getExtendedListeningPoints() {
return extendedListeningPointList.iterator();
}
/**
*
* @param extendedListeningPoint
*/
public void addExtendedListeningPoint(ExtendedListeningPoint extendedListeningPoint) {
extendedListeningPointList.add(extendedListeningPoint);
computeOutboundInterfaces();
// Adding to the transport cache map
List<ExtendedListeningPoint> extendedListeningPoints =
transportMappingCacheMap.get(extendedListeningPoint.getTransport().toLowerCase());
extendedListeningPoints.add(extendedListeningPoint);
// Adding private ipaddress to the triplet cache map
for(String ipAddress : extendedListeningPoint.getIpAddresses()) {
extendedListeningPointsCacheMap.put(ipAddress + "/" + extendedListeningPoint.getPort() + ":" + extendedListeningPoint.getTransport().toLowerCase(), extendedListeningPoint);
}
// Adding public address if any to the triplet cache map
if(extendedListeningPoint.getGlobalIpAddress() != null) {
extendedListeningPointsCacheMap.put(extendedListeningPoint.getGlobalIpAddress() + "/" + extendedListeningPoint.getPort() + ":" + extendedListeningPoint.getTransport().toLowerCase(), extendedListeningPoint);
extendedListeningPointsCacheMap.put(extendedListeningPoint.getGlobalIpAddress() + "/" + extendedListeningPoint.getGlobalPort() + ":" + extendedListeningPoint.getTransport().toLowerCase(), extendedListeningPoint);
}
}
/**
*
* @param extendedListeningPoint
*/
public void removeExtendedListeningPoint(ExtendedListeningPoint extendedListeningPoint) {
extendedListeningPointList.add(extendedListeningPoint);
computeOutboundInterfaces();
// removing from the transport cache map
List<ExtendedListeningPoint> extendedListeningPoints =
transportMappingCacheMap.get(extendedListeningPoint.getTransport().toLowerCase());
extendedListeningPoints.remove(extendedListeningPoint);
// Removing private ipaddress from the triplet cache map for listening point
for(String ipAddress : extendedListeningPoint.getIpAddresses()) {
extendedListeningPointsCacheMap.remove(ipAddress + "/" + extendedListeningPoint.getPort() + ":" + extendedListeningPoint.getTransport().toLowerCase());
}
// Removing public address if any from the triplet cache map
if(extendedListeningPoint.getGlobalIpAddress() != null) {
extendedListeningPointsCacheMap.remove(extendedListeningPoint.getGlobalIpAddress() + "/" + extendedListeningPoint.getPort() + ":" + extendedListeningPoint.getTransport().toLowerCase());
extendedListeningPointsCacheMap.remove(extendedListeningPoint.getGlobalIpAddress() + "/" + extendedListeningPoint.getGlobalPort() + ":" + extendedListeningPoint.getTransport().toLowerCase());
}
}
/**
* Retrieve the first matching listening point corresponding to the transport.
* @param transport the transport
* @param strict if true, it will search only for this transport otherwise it will look for any transport if no
* valid listening point could be found for the transport in parameter
* @return Retrieve the first matching listening point corresponding to the transport.
* If none has been found, null is returned if strict is true.
* If none has been found, an exception is thrown is strict is false and no listening point could be found.
*/
public ExtendedListeningPoint findMatchingListeningPoint(String transport, boolean strict) {
if(transport == null) {
transport = ListeningPoint.UDP;
}
List<ExtendedListeningPoint> extendedListeningPoints = transportMappingCacheMap.get(transport.toLowerCase());
if(extendedListeningPoints.size() > 0) {
return extendedListeningPoints.get(0);
}
if(strict) {
return null;
} else {
if(extendedListeningPointList.size() > 0) {
return extendedListeningPointList.get(0);
} else {
throw new RuntimeException("no valid sip connectors could be found to create the sip application session !!!");
}
}
}
/**
* Retrieve the first matching listening Point corresponding to the
* ipAddress port and transport given in parameter.
*
* @param ipAddress the ip address
* @param port the port
* @param transport the transport
* @return Retrieve the first matching listening point corresponding to the ipAddress port and transport.
* If none has been found, null is returned.
*/
public ExtendedListeningPoint findMatchingListeningPoint(String ipAddress, int port, String transport) {
int portChecked = checkPortRange(port, transport);
if(transport == null) {
transport = ListeningPoint.UDP;
}
// we check first if a listening point can be found (we only do the host resolving if not found to have better perf )
ExtendedListeningPoint listeningPoint = extendedListeningPointsCacheMap.get(ipAddress + "/" + portChecked + ":" + transport.toLowerCase());
if(listeningPoint == null && !Inet6Util.isValidIP6Address(ipAddress)
&& !Inet6Util.isValidIPV4Address(ipAddress)) {
// if no listening point has been found and the ipaddress is not a valid IP6 address nor a valid IPV4 address
// then we try to resolve it as a hostname
InetAddress[] inetAddresses = new InetAddress[0];
try {
inetAddresses = InetAddress.getAllByName(ipAddress);
} catch (UnknownHostException e) {
// not important it can mean that the ipAddress provided is not a hostname
// but an ip address not found in the searched listening points above
}
for (InetAddress inetAddress : inetAddresses) {
listeningPoint = extendedListeningPointsCacheMap.get(inetAddress.getHostAddress() + "/" + portChecked + ":" + transport.toLowerCase());
if(listeningPoint != null) {
return listeningPoint;
}
}
}
return listeningPoint;
}
/**
* Checks if the port is in the UDP-TCP port numbers (0-65355) range
* otherwise defaulting to 5060 if UDP, TCP or SCTP or to 5061 if TLS
* @param port port to check
* @return the smae port number if in range, otherwise 5060 if UDP, TCP or SCTP or to 5061 if TLS
*/
public static int checkPortRange(int port, String transport) {
if(port < MIN_PORT_NUMBER || port > MAX_PORT_NUMBER) {
if((ListeningPoint.TLS).equalsIgnoreCase(transport)) {
return ListeningPoint.PORT_5061;
} else {
return ListeningPoint.PORT_5060;
}
}
else {
return port;
}
}
/**
* Returns An immutable instance of the java.util.List interface containing
* the SipURI representation of IP addresses which are used by the container to send out the messages.
* @return immutable List containing the SipURI representation of IP addresses
*/
public List<SipURI> getOutboundInterfaces() {
return Collections.unmodifiableList(outboundInterfaces);
}
/**
* Compute all the outbound interfaces for this manager
*/
protected void computeOutboundInterfaces() {
if(logger.isDebugEnabled()) {
logger.debug("Outbound Interface List : ");
}
List<SipURI> newlyComputedOutboundInterfaces = new CopyOnWriteArrayList<SipURI>();
Set<String> newlyComputedOutboundInterfacesIpAddresses = new CopyOnWriteArraySet<String>();
Iterator<ExtendedListeningPoint> it = getExtendedListeningPoints();
while (it.hasNext()) {
ExtendedListeningPoint extendedListeningPoint = it
.next();
for(String ipAddress : extendedListeningPoint.getIpAddresses()) {
try {
newlyComputedOutboundInterfacesIpAddresses.add(ipAddress);
javax.sip.address.SipURI jainSipURI = SipFactories.addressFactory.createSipURI(
null, ipAddress);
jainSipURI.setPort(extendedListeningPoint.getPort());
jainSipURI.setTransportParam(extendedListeningPoint.getTransport());
SipURI sipURI = new SipURIImpl(jainSipURI);
newlyComputedOutboundInterfaces.add(sipURI);
if(logger.isDebugEnabled()) {
logger.debug("Outbound Interface : " + jainSipURI);
}
} catch (ParseException e) {
logger.error("cannot add the following listening point " +
ipAddress + ":" +
extendedListeningPoint.getPort() + ";transport=" +
extendedListeningPoint.getTransport() + " to the outbound interfaces", e);
}
}
}
lock.lock();
try {
outboundInterfaces = newlyComputedOutboundInterfaces;
outboundInterfacesIpAddresses = newlyComputedOutboundInterfacesIpAddresses;
} finally {
lock.unlock();
}
}
public static boolean findUsePublicAddress(Message message) {
// TODO add a caching mechanism to increase perf
boolean usePublicAddress = true;
String host = null;
String transport = JainSipUtils.findTransport(message);
if(message instanceof Request) {
// Get the first route header (it will be used before the request uri for request routing as per RFC3261)
// or the request uri if it is null
Request request = (Request) message;
RouteHeader routeHeader = (RouteHeader) request.getHeader(RouteHeader.NAME);
URI uri = null;
if(routeHeader != null) {
uri = routeHeader.getAddress().getURI();
} else {
uri = request.getRequestURI();
}
if(uri instanceof javax.sip.address.SipURI) {
javax.sip.address.SipURI sipUri = (javax.sip.address.SipURI) uri;
host = sipUri.getHost();
}
} else {
// Get the first via header (it will be used for response routing as per RFC3261)
ViaHeader topmostViaHeader = (ViaHeader) message.getHeader(ViaHeader.NAME);
if(topmostViaHeader != null) {
host = topmostViaHeader.getHost();
}
}
// check if it the host is part of a private network as defined per RFC 1918 (see http://en.wikipedia.org/wiki/Private_network)
if(host != null) {
if(Inet6Util.isValidIP6Address(host)) {
usePublicAddress = false;
if(logger.isDebugEnabled()) {
logger.debug("host " + host + " is a numeric IPV6 address, " +
"no DNS SRV lookup to be done and no need for STUN");
}
} else if(Inet6Util.isValidIPV4Address(host)) {
if(logger.isDebugEnabled()) {
logger.debug("host " + host + " is a numeric IPV4 address, " +
"no DNS SRV lookup to be done");
}
usePublicAddress = !isIPAddressPartOfPrivateNetwork(host);
} else {
logger.debug("host " + host + " is a hostname, " +
"doing DNS SRV lookup");
//hostname : DNS lookup to do
Hop hop = new HopImpl(host, -1, transport);
Hop lookedupHop = DNSAddressResolver.resolveHostByDnsSrvLookup(hop);
if(!hop.equals(lookedupHop)) {
usePublicAddress = !isIPAddressPartOfPrivateNetwork(lookedupHop.getHost());
}
}
}
if(logger.isDebugEnabled()) {
logger.debug("STUN enabled : use public adress " + usePublicAddress + " for following host " + host);
}
return usePublicAddress;
}
public static boolean isIPAddressPartOfPrivateNetwork(String ipAddress) {
int addressOutboundness = JainSipUtils.getAddressOutboundness(ipAddress);
// check if it part of a private network
if(addressOutboundness < 4) {
if(logger.isDebugEnabled()) {
logger.debug("host " + ipAddress + " is part of a private network we will not use the public address resolved by STUN");
}
return true;
} else {
if(logger.isDebugEnabled()) {
logger.debug("host " + ipAddress + " is not part of a private network we will use the public address resolved by STUN");
}
return false;
}
}
}