/* * Copyright (C) 2005 Luca Veltri - University of Parma - Italy * * This file is part of MjSip (http://www.mjsip.org) * * MjSip is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * MjSip 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with MjSip; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * Author(s): * Luca Veltri (luca.veltri@unipr.it) */ /* * Greg Dorfuss - http://mhspot.com * Allow Invite Listener to be added no matter what * Added SIP OPTION message support * Added 405 to NOTIFY messages * added extra break for branch - I was getting intermittant duplicate branches * added params to control handling of addresses * 2008/10/08 added 302 redir if busy * added sychronize on invite request */ package org.zoolu.sip.provider; import org.zoolu.net.*; import org.zoolu.sip.header.*; import org.zoolu.sip.message.Message; import org.zoolu.sip.message.MessageFactory; import org.zoolu.sip.message.SipResponses; import org.zoolu.sip.address.*; import org.zoolu.sip.transaction.TransactionServer; import org.zoolu.sip.transaction.InviteTransactionServer; import org.zoolu.sip.transaction.Transaction; import org.zoolu.tools.Configure; import org.zoolu.tools.Configurable; import org.zoolu.tools.Parser; import org.zoolu.tools.Random; import org.zoolu.tools.Log; import org.zoolu.tools.LogLevel; import org.zoolu.tools.RotatingLog; //import org.zoolu.tools.MD5; import org.zoolu.tools.SimpleDigest; import org.zoolu.tools.DateFormat; import java.util.Hashtable; import java.io.IOException; import java.io.ByteArrayOutputStream; import java.io.PrintStream; //PersonalJava //import java.util.HashSet; //import java.util.Iterator; import org.zoolu.tools.HashSet; import org.zoolu.tools.Iterator; import java.util.Enumeration; import java.util.Vector; import java.util.Date; /** SipProvider implements the SIP transport layer, that is the layer responsable for * sending and receiving SIP messages. Messages are received by the callback function * defined in the interface SipProviderListener. * <p> * SipProvider implements also multiplexing/demultiplexing service through the use of * SIP interface identifiers and <i>onReceivedMessage()<i/> callback function * of specific SipProviderListener. * <p> * A SipProviderListener can be added to a SipProvider through the * addSipProviderListener(id,listener) method, where: * <b> - <i>id<i/> is the SIP interface identifier the listener has to be bound to, * <b> - <i>listener<i/> is the SipProviderListener that received messages are passed to. * <p/> * The SIP interface identifier specifies the type of messages the listener is going to * receive for. Together with the specific SipProvider, it represents the complete SIP * Service Access Point (SAP) address/identifier used for demultiplexing SIP messages * at receiving side. * <p/> * The identifier can be of one of the three following types: transaction_id, dialog_id, * or method_id. These types of identifiers characterize respectively: * <br> - messages within a specific transaction, * <br> - messages within a specific dialog, * <br> - messages related to a specific SIP method. * It is also possible to use the the identifier ANY to specify * <br> - all messages that are out of any transactions, dialogs, or already specified * method types. * <p> * When receiving a message, the SipProvider first tries to look for a matching * transaction, then looks for a matching dialog, then for a matching method type, * and finally for a default listener (i.e. that with identifier ANY). * For the matched SipProviderListener, the method <i>onReceivedMessage()</i> is fired. * <p> * Note: no 482 (Loop Detected) responses are generated for requests that does not * properly match any ongoing transactions, dialogs, nor method types. */ public class SipProvider implements Configurable, TransportListener, TcpServerListener { // **************************** Constants **************************** /** UDP protocol type */ public static final String PROTO_UDP = "udp"; /** TCP protocol type */ public static final String PROTO_TCP = "tcp"; /** TLS protocol type */ public static final String PROTO_TLS = "tls"; /** SCTP protocol type */ public static final String PROTO_SCTP = "sctp"; /** String value "auto-configuration" used for auto configuration of the host address. */ public static final String AUTO_CONFIGURATION = "AUTO-CONFIGURATION"; /** String value "auto-configuration" used for auto configuration of the host address. */ public static final String ALL_INTERFACES = "ALL-INTERFACES"; /** String value "NO-OUTBOUND" used for setting no outbound proxy. */ //public static final String NO_OUTBOUND="NO-OUTBOUND"; /** Identifier used as listener id for capturing ANY incoming messages * that does not match any active method_id, transaction_id, nor dialog_id. * <br> In this context, "active" means that there is a active listener * for that specific method, transaction, or dialog. */ public static final Identifier ANY = new Identifier("ANY"); /** Identifier used as listener id for capturing any incoming messages in PROMISQUE mode, * that means that messages are passed to the present listener regardless of * any other active SipProviderListeners for specific messages. * <p/> * More than one SipProviderListener can be added and be active concurrently * for capturing messages in PROMISQUE mode. */ public static final Identifier PROMISQUE = new Identifier("PROMISQUE"); public static final Identifier INVITE = new Identifier("INVITE"); /** Minimum length for a valid SIP message. */ private static final int MIN_MESSAGE_LENGTH = 12; // ***************** Readable/configurable attributes ***************** String sipBusyUrl = null; /** Via address/name. * Use 'auto-configuration' for auto detection, or let it undefined. */ String via_addr = null; /** Local SIP port */ int host_port = 0; /** Network interface (IP address) used by SIP. * Use 'ALL-INTERFACES' for binding SIP to all interfaces (or let it undefined). */ String host_ifaddr = null; /** Transport protocols (the first protocol is used as default) */ String[] transport_protocols = null; /** Max number of (contemporary) open connections */ int nmax_connections = 0; /** Outbound proxy (host_addr[:host_port]). * Use 'NONE' for not using an outbound proxy (or let it undefined). */ SocketAddress outbound_proxy = null; /** Whether logging all packets (including non-SIP keepalive tokens). */ boolean log_all_packets = false; private String inviteLock = ""; // new params to control address handling private boolean sendResponseUsingOutboundProxy = false; private boolean useViaReceived = true; private boolean useViaRport = true; // for backward compatibility: /** Outbound proxy addr (for backward compatibility). */ private String outbound_addr = null; /** Outbound proxy port (for backward compatibility). */ private int outbound_port = -1; private OptionHandler optionHandler = null; // ********************* Non-readable attributes ********************* /** Event Loger */ protected Log event_log = null; /** Message Loger */ protected Log message_log = null; /** Network interface (IP address) used by SIP. */ IpAddress host_ipaddr = null; /** Default transport */ String default_transport = null; static long breaker = 0; private boolean initComplete = false; /** Whether using UDP as transport protocol */ boolean transport_udp = false; /** Whether using TCP as transport protocol */ boolean transport_tcp = false; /** Whether using TLS as transport protocol */ boolean transport_tls = false; /** Whether using SCTP as transport protocol */ boolean transport_sctp = false; /** Whether adding 'rport' parameter on outgoing requests. */ boolean rport=true; /** Whether forcing 'rport' parameter on incoming requests ('force-rport' mode). */ boolean force_rport=false; /** List of provider listeners */ SpcHashtable listeners=null; /** List of exception listeners */ HashSet exception_listeners=null; /** UDP transport */ UdpTransport udp=null; /** Tcp server */ TcpServer tcp_server=null; /** Connections */ Hashtable connections=null; // *************************** Costructors *************************** /** Creates a void SipProvider. */ /*protected SipProvider() { }*/ /** Creates a new SipProvider. */ public SipProvider(String via_addr, int port) { init(via_addr,port,null,null); initlog(); startTrasport(); } /** Creates a new SipProvider. * Costructs the SipProvider, initializing the SipProviderListeners, the transport protocols, and other attributes. */ public SipProvider(String via_addr, int port, String[] protocols, String ifaddr) { init(via_addr,port,protocols,ifaddr); initlog(); startTrasport(); } /** Creates a new SipProvider. * The SipProvider attributres are read from file. */ public SipProvider(String file) { if (!SipStack.isInit()) SipStack.init(file); new Configure(this,file); init(via_addr,host_port,transport_protocols,host_ifaddr); initlog(); startTrasport(); } /** Inits the SipProvider, initializing the SipProviderListeners, the transport protocols, the outbound proxy, and other attributes. */ private void init(String viaddr, int port, String[] protocols, String ifaddr) { if (!SipStack.isInit()) SipStack.init(); via_addr = viaddr; if (via_addr == null || via_addr.equalsIgnoreCase(AUTO_CONFIGURATION)) via_addr = IpAddress.getLocalHostAddress().toString(); host_port = port; if (host_port <= 0) host_port = SipStack.default_port; host_ipaddr = null; if (ifaddr != null && !ifaddr.equalsIgnoreCase(ALL_INTERFACES)) { try { host_ipaddr = IpAddress.getByName(ifaddr); } catch (IOException e) { e.printStackTrace(); host_ipaddr = null; } } transport_protocols = protocols; if (transport_protocols==null) transport_protocols=SipStack.default_transport_protocols; default_transport=transport_protocols[0]; for (int i=0; i<transport_protocols.length; i++) { transport_protocols[i]=transport_protocols[i].toLowerCase(); if (transport_protocols[i].equals(PROTO_UDP)) transport_udp=true; else if (transport_protocols[i].equals(PROTO_TCP)) transport_tcp=true; /* else if (transport_protocols[i].equals(PROTO_TLS)) transport_tls=true; else if (transport_protocols[i].equals(PROTO_SCTP)) transport_sctp=true; */ } if (nmax_connections<=0) nmax_connections=SipStack.default_nmax_connections; // just for backward compatibility.. if (outbound_port<0) outbound_port=SipStack.default_port; if (outbound_addr!=null) { if (outbound_addr.equalsIgnoreCase(Configure.NONE) || outbound_addr.equalsIgnoreCase("NO-OUTBOUND")) outbound_proxy=null; else outbound_proxy=new SocketAddress(outbound_addr,outbound_port); } rport=SipStack.use_rport; force_rport=SipStack.force_rport; exception_listeners=new HashSet(); //listeners=new Hashtable(); Hashtable dupeKeys=new Hashtable(); dupeKeys.put(INVITE, ""); listeners=new SpcHashtable(dupeKeys); connections=new Hashtable(); } /** Inits logs. */ private void initlog() { if (SipStack.debug_level>0) { String filename=SipStack.log_path+"//"+via_addr+"."+host_port; event_log=new RotatingLog(filename+"_events.log",null,SipStack.debug_level,SipStack.max_logsize*1024,SipStack.log_rotations,SipStack.rotation_scale,SipStack.rotation_time); message_log=new RotatingLog(filename+"_messages.log",null,SipStack.debug_level,SipStack.max_logsize*1024,SipStack.log_rotations,SipStack.rotation_scale,SipStack.rotation_time); } printLog("Date: "+DateFormat.formatHHMMSS(new Date()),LogLevel.HIGH); printLog("SipStack: "+SipStack.release,LogLevel.HIGH); printLog("new SipProvider(): "+toString(),LogLevel.HIGH); } /** Starts the transport services. */ private void startTrasport() { // start udp if (transport_udp) { try { if (host_ipaddr==null) udp=new UdpTransport(host_port,this); else udp=new UdpTransport(host_port,host_ipaddr,this); printLog("udp is up",LogLevel.MEDIUM); } catch (Exception e) { printException(e,LogLevel.HIGH); } } // start tcp if (transport_tcp) { try { if (host_ipaddr==null) tcp_server=new TcpServer(host_port,this); else tcp_server=new TcpServer(host_port,host_ipaddr,this); printLog("tcp is up",LogLevel.MEDIUM); } catch (Exception e) { printException(e,LogLevel.HIGH); } } //printLog("transport is up",LogLevel.MEDIUM); } /** Stops the transport services. */ private void stopTrasport() { // stop udp if (udp!=null) { printLog("udp is going down",LogLevel.LOWER); udp.halt(); udp=null; } // stop tcp if (tcp_server!=null) { printLog("tcp is going down",LogLevel.LOWER); tcp_server.halt(); tcp_server=null; } if (connections!=null) { printLog("connections are going down",LogLevel.LOWER); for (Enumeration e=connections.elements(); e.hasMoreElements(); ) { ConnectedTransport c=(ConnectedTransport)e.nextElement(); c.halt(); } connections=null; } } /** Stops the SipProviders. */ public void halt() { printLog("halt: SipProvider is going down",LogLevel.MEDIUM); stopTrasport(); //listeners=new Hashtable(); listeners=new SpcHashtable(null); exception_listeners=new HashSet(); } /** Parses a single line (loaded from the config file) */ public void parseLine(String line) { String attribute; Parser par; int index=line.indexOf("="); if (index>0) { attribute=line.substring(0,index).trim(); par=new Parser(line,index+1); } else { attribute=line; par=new Parser(""); } char[] delim={' ',','}; if (attribute.equals("via_addr")) { via_addr=par.getString(); return; } if (attribute.equals("host_port")) { host_port=par.getInt(); return; } if (attribute.equals("host_ifaddr")) { host_ifaddr=par.getString(); return; } if (attribute.equals("transport_protocols")) { transport_protocols=par.getWordArray(delim); return; } if (attribute.equals("nmax_connections")) { nmax_connections=par.getInt(); return; } if (attribute.equals("outbound_proxy")) { String soaddr=par.getString(); if (soaddr==null || soaddr.length()==0 || soaddr.equalsIgnoreCase(Configure.NONE) || soaddr.equalsIgnoreCase("NO-OUTBOUND")) outbound_proxy=null; else outbound_proxy=new SocketAddress(soaddr); return; } if (attribute.equals("log_all_packets")) { log_all_packets=(par.getString().toLowerCase().startsWith("y")); return; } // old parameters if (attribute.equals("host_addr")) System.err.println("WARNING: parameter 'host_addr' is no more supported; use 'via_addr' instead."); if (attribute.equals("all_interfaces")) System.err.println("WARNING: parameter 'all_interfaces' is no more supported; use 'host_iaddr' for setting a specific interface or let it undefined."); if (attribute.equals("use_outbound")) System.err.println("WARNING: parameter 'use_outbound' is no more supported; use 'outbound_proxy' for setting an outbound proxy or let it undefined."); if (attribute.equals("outbound_addr")) { System.err.println("WARNING: parameter 'outbound_addr' has been deprecated; use 'outbound_proxy=<host_addr>[:<host_port>]' instead."); outbound_addr=par.getString(); return; } if (attribute.equals("outbound_port")) { System.err.println("WARNING: parameter 'outbound_port' has been deprecated; use 'outbound_proxy=<host_addr>[:<host_port>]' instead."); outbound_port=par.getInt(); return; } if (attribute.equals("sendResponseUsingOutboundProxy")) { sendResponseUsingOutboundProxy=(par.getString().toLowerCase().startsWith("y")); return; } if (attribute.equals("useViaReceived")) { useViaReceived=(par.getString().toLowerCase().startsWith("y")); return; } if (attribute.equals("useViaRport")) { useViaRport=(par.getString().toLowerCase().startsWith("y")); return; } if (attribute.equals("SipInboundAllChannelsBusyAction")) { String sipInboundAllChannelsBusyAction=(par.getRemainingString()); if (sipInboundAllChannelsBusyAction.startsWith("transferto:")) // redirect the call elsewhere sipBusyUrl=sipInboundAllChannelsBusyAction.replaceAll("transferto:",""); else sipBusyUrl=null; return; } } /** Converts the entire object into lines (to be saved into the config file) */ protected String toLines() { // currently not implemented.. return toString(); } /** Gets a String with the list of transport protocols. */ private String transportProtocolsToString() { String list=transport_protocols[0]; for (int i=1; i<transport_protocols.length; i++) list+="/"+transport_protocols[i]; return list; } // ************************** Public methods ************************* /** Gets via address. */ public String getViaAddress() { return via_addr; } /** Sets via address. */ /*public void setViaAddress(String addr) { via_addr=addr; }*/ /** Gets host port. */ public int getPort() { return host_port; } /** Whether binding the sip provider to all interfaces or only on the specified host address. */ public boolean isAllInterfaces() { return host_ipaddr==null; } /** Gets host interface IpAddress. */ public IpAddress getInterfaceAddress() { return host_ipaddr; } /** Gets array of transport protocols. */ public String[] getTransportProtocols() { return transport_protocols; } /** Gets the default transport protocol. */ public String getDefaultTransport() { return default_transport; } /** Gets the default transport protocol. */ public void setDefaultTransport(String proto) { default_transport=proto; } /** Sets rport support. */ public void setRport(boolean flag) { rport=flag; } /** Whether using rport. */ public boolean isRportSet() { return rport; } /** Sets 'force-rport' mode. */ public void setForceRport(boolean flag) { force_rport=flag; } /** Whether using 'force-rport' mode. */ public boolean isForceRportSet() { return force_rport; } /** Whether has outbound proxy. */ public boolean hasOutboundProxy() { return outbound_proxy!=null; } /** Gets the outbound proxy. */ public SocketAddress getOutboundProxy() { return outbound_proxy; } /** Sets the outbound proxy. Use 'null' for not using any outbound proxy. */ public void setOutboundProxy(SocketAddress soaddr) { outbound_proxy=soaddr; } /** Removes the outbound proxy. */ /*public void removeOutboundProxy() { setOutboundProxy(null); }*/ /** Gets the max number of (contemporary) open connections. */ public int getNMaxConnections() { return nmax_connections; } /** Sets the max number of (contemporary) open connections. */ public void setNMaxConnections(int n) { nmax_connections=n; } /** Gets event log. */ public Log getLog() { return event_log; } /** Returns the list (Hashtable) of active listener_IDs. */ public SpcHashtable getListeners() { return listeners; } /** Adds a new listener to the SipProvider for caputering any message in PROMISQUE mode. * It is the same as using method addSipProviderListener(SipProvider.PROMISQUE,listener). * <p/> * When capturing messages in promisque mode all messages are passed to the SipProviderListener * before passing them to the specific listener (if present). * <br/> Note that more that one SipProviderListener can be active in promisque mode * at the same time;in that case the same message is passed to all PROMISQUE * SipProviderListeners. * @param listener is the SipProviderListener. * @return It returns <i>true</i> if the SipProviderListener is added, * <i>false</i> if the listener_ID is already in use. */ public boolean addSipProviderPromisqueListener(SipProviderListener listener) { return addSipProviderListener(PROMISQUE,listener); } /** Adds a new listener to the SipProvider for caputering ANY message. * It is the same as using method addSipProviderListener(SipProvider.ANY,listener). * @param listener is the SipProviderListener. * @return It returns <i>true</i> if the SipProviderListener is added, * <i>false</i> if the listener_ID is already in use. */ public boolean addSipProviderListener(SipProviderListener listener) { return addSipProviderListener(ANY,listener); } /** Adds a new listener to the SipProvider. * @param id is the unique identifier for the messages which the listener * as to be associated to. It is used as key. * It can identify a method, a transaction, or a dialog. * Use SipProvider.ANY to capture all messages. * Use SipProvider.PROMISQUE if you want to capture all message in promisque mode * (letting other listeners to capture the same received messages). * @param listener is the SipProviderListener for this message id. * @return It returns <i>true</i> if the SipProviderListener is added, * <i>false</i> if the listener_ID is already in use. */ public boolean addSipProviderListener(Identifier key, SipProviderListener listener) { printLog("adding SipProviderListener: "+key,LogLevel.MEDIUM); boolean ret; //Identifier key=id; if (listeners.containsKey(key) && !key.toString().equals("INVITE")) { printWarning("trying to add a SipProviderListener with a id that is already in use.",LogLevel.HIGH); ret=false; } else { listeners.put(key,listener); ret=true; } if (listeners!=null) { String list=""; for (Enumeration e=listeners.keys(); e.hasMoreElements();) list+=e.nextElement()+", "; printLog(listeners.size()+" listeners: "+list,LogLevel.LOW); } return ret; } /** Removes a SipProviderListener. * @param id is the unique identifier used to select the listened messages. * @return It returns <i>true</i> if the SipProviderListener is removed, * <i>false</i> if the identifier is missed. */ public boolean removeSipProviderListener(Identifier key) { printLog("removing SipProviderListener: "+key,LogLevel.MEDIUM); boolean ret; //Identifier key=id; if (!listeners.containsKey(key)) { printWarning("trying to remove a missed SipProviderListener.",LogLevel.HIGH); ret=false; } else { listeners.remove(key); ret=true; } if (listeners!=null) { String list=""; for (Enumeration e=listeners.keys(); e.hasMoreElements();) list+=e.nextElement()+", "; printLog(listeners.size()+" listeners: "+list,LogLevel.LOW); } return ret; } public boolean removeSipProviderListener(Identifier key,Object listener) { printLog("removing SipProviderListener Pair: "+key ,LogLevel.HIGH); boolean ret; if (!listeners.containsPair(key,listener)) { printLog("trying to remove a missed SipProviderListener Pair." ,LogLevel.LOW); ret=false; } else { listeners.removePair(key,listener); ret=true; } if (listeners!=null) { String list=""; for (Enumeration e=listeners.keys(); e.hasMoreElements();) list+=e.nextElement()+", "; printLog(listeners.size()+" listeners: "+list+"\n"+listeners.toString() ,LogLevel.LOW); } return ret; } /** Sets the SipProviderExceptionListener. * The SipProviderExceptionListener is the listener for all exceptions * thrown by the SipProviders. * @param e_listener is the SipProviderExceptionListener. * @return It returns <i>true</i> if the SipProviderListener has been correctly set, * <i>false</i> if the SipProviderListener was already set. */ public boolean addSipProviderExceptionListener(SipProviderExceptionListener e_listener) { printLog("adding SipProviderExceptionListener",LogLevel.MEDIUM); if (exception_listeners.contains(e_listener)) { printWarning("trying to add an already present SipProviderExceptionListener.",LogLevel.HIGH); return false; } else { exception_listeners.add(e_listener); return true; } } /** Removes a SipProviderExceptionListener. * @param e_listener is the SipProviderExceptionListener. * @return It returns <i>true</i> if the SipProviderExceptionListener has been correctly removed, * <i>false</i> if the SipProviderExceptionListener is missed. */ public boolean removeSipProviderExceptionListener(SipProviderExceptionListener e_listener) { printLog("removing SipProviderExceptionListener",LogLevel.MEDIUM); if (!exception_listeners.contains(e_listener)) { printWarning("trying to remove a missed SipProviderExceptionListener.",LogLevel.HIGH); return false; } else { exception_listeners.remove(e_listener); return true; } } /** Sends a Message, specifing the transport portocol, nexthop address and port. * <p> This is a low level method and * forces the message to be routed to a specific nexthop address, port and transport, * regardless whatever the Via, Route, or request-uri, address to. * <p> * In case of connection-oriented transport, the connection is selected as follows: * <br> - if an existing connection is found matching the destination * end point (socket), such connection is used, otherwise * <br> - a new connection is established * * @return It returns a Connection in case of connection-oriented delivery * (e.g. TCP) or null in case of connection-less delivery (e.g. UDP) */ public ConnectionIdentifier sendMessage(Message msg, String proto, String dest_addr, int dest_port, int ttl) { if (log_all_packets || msg.getLength()>MIN_MESSAGE_LENGTH) printLog("Resolving host address '"+dest_addr+"'",LogLevel.MEDIUM); try { IpAddress dest_ipaddr=IpAddress.getByName(dest_addr); return sendMessage(msg,proto,dest_ipaddr,dest_port,ttl); } catch (Exception e) { printException(e,LogLevel.HIGH); return null; } } /** Sends a Message, specifing the transport portocol, nexthop address and port. */ private ConnectionIdentifier sendMessage(Message msg, String proto, IpAddress dest_ipaddr, int dest_port, int ttl) { ConnectionIdentifier conn_id=new ConnectionIdentifier(proto,dest_ipaddr,dest_port); if (log_all_packets || msg.getLength()>MIN_MESSAGE_LENGTH) printLog("Sending message to "+conn_id,LogLevel.MEDIUM); if (transport_udp && proto.equals(PROTO_UDP)) { // UDP //printLog("using UDP",LogLevel.LOW); conn_id=null; try { // if (ttl>0 && multicast_address) do something? udp.sendMessage(msg,dest_ipaddr,dest_port); } catch (IOException e) { printException(e,LogLevel.HIGH); return null; } } else if (transport_tcp && proto.equals(PROTO_TCP)) { // TCP //printLog("using TCP",LogLevel.LOW); if (!connections.containsKey(conn_id)) { printLog("no active connection found matching "+conn_id,LogLevel.MEDIUM); printLog("open "+proto+" connection to "+dest_ipaddr+":"+dest_port,LogLevel.MEDIUM); TcpTransport conn=null; try { conn=new TcpTransport(dest_ipaddr,dest_port,this); } catch (Exception e) { printLog("connection setup FAILED",LogLevel.HIGH); return null; } printLog("connection "+conn+" opened",LogLevel.HIGH); addConnection(conn); } else { printLog("active connection found matching "+conn_id,LogLevel.MEDIUM); } ConnectedTransport conn=(ConnectedTransport)connections.get(conn_id); if (conn!=null) { printLog("sending data through conn "+conn,LogLevel.MEDIUM); try { conn.sendMessage(msg); conn_id=new ConnectionIdentifier(conn); } catch (IOException e) { printException(e,LogLevel.HIGH); return null; } } else { // this point has not to be reached printLog("ERROR: conn "+conn_id+" not found: abort.",LogLevel.MEDIUM); return null; } } else { // otherwise printWarning("Unsupported protocol ("+proto+"): Message discarded",LogLevel.HIGH); return null; } // logs String dest_addr=dest_ipaddr.toString(); printMessageLog(proto,dest_addr,dest_port,msg.getLength(),msg,"sent"); return conn_id; } /** Sends the message <i>msg</i>. * <p> * The destination for the request is computed as follows: * <br> - if <i>outbound_addr</i> is set, <i>outbound_addr</i> and * <i>outbound_port</i> are used, otherwise * <br> - if message has Route header with lr option parameter (i.e. RFC3261 compliant), * the first Route address is used, otherwise * <br> - the request's Request-URI is considered. * <p> * The destination for the response is computed based on the sent-by parameter in * the Via header field (RFC3261 compliant) * <p> * As transport it is used the protocol specified in the 'via' header field * <p> * In case of connection-oriented transport: * <br> - if an already established connection is found matching the destination * end point (socket), such connection is used, otherwise * <br> - a new connection is established * * @return Returns a ConnectionIdentifier in case of connection-oriented delivery * (e.g. TCP) or null in case of connection-less delivery (e.g. UDP) */ public ConnectionIdentifier sendMessage(Message msg) { printLog("Sending message:\r\n"+msg.toString(),LogLevel.LOWER); // select the transport protocol ViaHeader via=msg.getViaHeader(); String proto=via.getProtocol().toLowerCase(); printLog("using transport "+proto,LogLevel.MEDIUM); // select the destination address and port String dest_addr=null; int dest_port=0; int ttl=0; if (msg.isRequest()) { // REQUESTS if (outbound_proxy!=null) { dest_addr=outbound_proxy.getAddress().toString(); dest_port=outbound_proxy.getPort(); } else { if (msg.hasRouteHeader() && msg.getRouteHeader().getNameAddress().getAddress().hasLr()) { SipURL url=msg.getRouteHeader().getNameAddress().getAddress(); dest_addr=url.getHost(); dest_port=url.getPort(); } else { SipURL url=msg.getRequestLine().getAddress(); dest_addr=url.getHost(); dest_port=url.getPort(); if (url.hasMaddr()) { dest_addr=url.getMaddr(); if (url.hasTtl()) ttl=url.getTtl(); // update the via header by adding maddr and ttl params via.setMaddr(dest_addr); if (ttl>0) via.setTtl(ttl); msg.removeViaHeader(); msg.addViaHeader(via); } } } } else { // RESPONSES if (outbound_proxy!=null && this.sendResponseUsingOutboundProxy) { dest_addr=outbound_proxy.getAddress().toString(); dest_port=outbound_proxy.getPort(); } else { SipURL url=via.getSipURL(); if (via.hasReceived()) dest_addr=via.getReceived(); else dest_addr=url.getHost(); if (via.hasRport()) dest_port=via.getRport(); if (dest_port<=0) dest_port=url.getPort(); } } if (dest_port<=0) dest_port=SipStack.default_port; return sendMessage(msg,proto,dest_addr,dest_port,ttl); } /** Sends the message <i>msg</i> using the specified connection. */ public ConnectionIdentifier sendMessage(Message msg, ConnectionIdentifier conn_id) { if (log_all_packets || msg.getLength()>MIN_MESSAGE_LENGTH) printLog("Sending message through conn "+conn_id,LogLevel.HIGH); printLog("message:\r\n"+msg.toString(),LogLevel.LOWER); if (conn_id!=null && connections.containsKey(conn_id)) { // connection exists printLog("active connection found matching "+conn_id,LogLevel.MEDIUM); ConnectedTransport conn=(ConnectedTransport)connections.get(conn_id); try { conn.sendMessage(msg); // logs //String proto=conn.getProtocol(); String proto=conn.getProtocol(); String dest_addr=conn.getRemoteAddress().toString(); int dest_port=conn.getRemotePort(); printMessageLog(proto,dest_addr,dest_port,msg.getLength(),msg,"sent"); return conn_id; } catch (Exception e) { printException(e,LogLevel.HIGH); } } //else printLog("no active connection found matching "+conn_id,LogLevel.MEDIUM); return sendMessage(msg); } /** Processes the message received. * It is called each time a new message is received by the transport layer, and * it performs the actual message processing. */ protected void processReceivedMessage(Message msg) { try { // logs printMessageLog(msg.getTransportProtocol(),msg.getRemoteAddress(),msg.getRemotePort(),msg.getLength(),msg,"received"); // discard too short messages if (msg.getLength()<=2) { if (log_all_packets) printLog("message too short: discarded\r\n",LogLevel.LOW); return; } // discard non-SIP messages String first_line=msg.getFirstLine(); if (first_line==null || first_line.toUpperCase().indexOf("SIP/2.0")<0) { if (log_all_packets) printLog("NOT a SIP message: discarded\r\n",LogLevel.LOW); return; } printLog("received new SIP message",LogLevel.HIGH); printLog("message:\r\n"+msg.toString(),LogLevel.LOWER); // if a request, handle "received" and "rport" parameters if (msg.isRequest()) { ViaHeader vh=msg.getViaHeader(); boolean via_changed=false; String src_addr=msg.getRemoteAddress(); int src_port=msg.getRemotePort(); String via_addr=vh.getHost(); int via_port=vh.getPort(); if (via_port<=0) via_port=SipStack.default_port; if (!via_addr.equals(src_addr) && this.useViaReceived) { vh.setReceived(src_addr); via_changed=true; } if (this.useViaRport) { if (vh.hasRport()) { vh.setRport(src_port); via_changed=true; } else { if (force_rport && via_port!=src_port) { vh.setRport(src_port); via_changed=true; } } } if (via_changed) { msg.removeViaHeader(); msg.addViaHeader(vh); } } // is there any listeners? if (listeners==null || listeners.size()==0) { if (initComplete) printLog("no listener found: meesage discarded.",LogLevel.HIGH); return; } // try to look for a UA in promisque mode if (listeners.containsKey(PROMISQUE)) { printLog("message passed to uas: "+PROMISQUE,LogLevel.MEDIUM); ((SipProviderListener)listeners.get(PROMISQUE)).onReceivedMessage(this,msg); } // after the callback check if the message is still valid if (!msg.isRequest() && !msg.isResponse()) { printLog("No valid SIP message: message discarded.",LogLevel.HIGH); return; } // this was the promisque listener; now keep on looking for a tighter listener.. if (msg.isRequest() && msg.isOption()) { // handle SIP OPTIONS message int optStat=486; // default busy if (listeners.containsKey(INVITE)) // if existing INVITE key then we might be available { if (this.optionHandler!=null) { String optBody=optionHandler.onOptionMsgReceived(); if (optBody!=null) { optStat=200; // add capabilities to msg to be sent printLog("OPTION Request - response status="+optStat , LogLevel.HIGH); TransactionServer ts=new TransactionServer(this,msg,null); ts.respondWith(MessageFactory.createResponse(msg,optStat,SipResponses.reasonOf(optStat),null,null,"application/sdp",optBody)); return; } } else optStat=200; // assume available if no option handler } printLog("OPTION Request - response status="+optStat,LogLevel.MEDIUM ); TransactionServer ts=new TransactionServer(this,msg,null); ts.respondWith(MessageFactory.createResponse(msg,optStat,SipResponses.reasonOf(optStat),null)); return; } // try to look for a transaction Identifier key=msg.getTransactionId(); printLog("DEBUG: transaction-id: "+key,LogLevel.MEDIUM); if (listeners.containsKey(key)) { printLog("message passed to transaction: "+key,LogLevel.MEDIUM); ((SipProviderListener)listeners.get(key)).onReceivedMessage(this,msg); return; } // try to look for a dialog key=msg.getDialogId(); printLog("DEBUG: dialog-id: "+key,LogLevel.MEDIUM); if (listeners.containsKey(key)) { printLog("message passed to dialog: "+key,LogLevel.MEDIUM); ((SipProviderListener)listeners.get(key)).onReceivedMessage(this,msg); return; } // try to look for a UAS key=msg.getMethodId(); if (msg.isRequest() && msg.isInvite()) { // need to synchronize invite requests to avoid issues synchronized (inviteLock) { if (listeners.containsKey(key)) { printLog("message passed to uas: "+key,LogLevel.MEDIUM); ((SipProviderListener)listeners.get(key)).onReceivedMessage(this,msg); return; } } } else if (listeners.containsKey(key)) { printLog("message passed to uas: "+key ,LogLevel.LOW); ((SipProviderListener)listeners.get(key)).onReceivedMessage(this,msg); return; } // try to look for a default UA if (listeners.containsKey(ANY)) { printLog("message passed to uas: "+ANY,LogLevel.MEDIUM); ((SipProviderListener)listeners.get(ANY)).onReceivedMessage(this,msg); return; } if (msg.isRequest() && msg.isInvite()) { // we are busy or can't answer - Invite would have been picked up above already if we could handle it if (sipBusyUrl!=null) { // redirect the call elsewhere String targetId=msg.getToHeader().getNameAddress().toString().replaceAll("(?i).*sip:<?([^@<]+)@.*", "$1"); String redirect_Url=sipBusyUrl.replaceAll("calleeid", targetId); printLog("Incoming SIP Call - Channel busy - Redirect to: "+redirect_Url ,LogLevel.LOW); Message resp=MessageFactory.createResponse(msg,302,SipResponses.reasonOf(302),new NameAddress(redirect_Url)); InviteTransactionServer ts=new InviteTransactionServer(this,msg,null); ts.respondWith(resp); return; } printLog("Invite Request - Sending busy response (486)",LogLevel.MEDIUM); InviteTransactionServer ts=new InviteTransactionServer(this,msg,null); ts.respondWith(MessageFactory.createResponse(msg,486,SipResponses.reasonOf(486),null)); return; } if (msg.isRequest() && msg.isNotify()) { // nobody handled it, just respond 405 (method not allowed) to get it to go away printLog("Notify Request - Sending unsupported response (405)",LogLevel.MEDIUM); TransactionServer ts=new TransactionServer(this,msg,null); ts.respondWith(MessageFactory.createResponse(msg,405,SipResponses.reasonOf(405),null)); return; } // if we are here, no listener_ID matched.. printLog("No SipListener found matching that message: message DISCARDED",LogLevel.HIGH); //printLog("Pending SipProviderListeners= "+getListeners().size(),3); printLog("Pending SipProviderListeners= "+listeners.size(),LogLevel.MEDIUM); } catch (Exception e) { printWarning("Error handling a new incoming message",LogLevel.HIGH); printException(e,LogLevel.MEDIUM); if (exception_listeners==null || exception_listeners.size()==0) { System.err.println("Error handling a new incoming message"); e.printStackTrace(); } else { for (Iterator i=exception_listeners.iterator(); i.hasNext(); ) try { ((SipProviderExceptionListener)i.next()).onMessageException(msg,e); } catch (Exception e2) { printWarning("Error handling handling the Exception",LogLevel.HIGH); printException(e2,LogLevel.MEDIUM); } } } } public void setOPTIONHandler(OptionHandler oh) { this.optionHandler=oh; } /** Adds a new Connection */ private void addConnection(ConnectedTransport conn) { ConnectionIdentifier conn_id=new ConnectionIdentifier(conn); if (connections.containsKey(conn_id)) { // remove the previous connection printLog("trying to add the already established connection "+conn_id,LogLevel.HIGH); printLog("connection "+conn_id+" will be replaced",LogLevel.HIGH); ConnectedTransport old_conn=(ConnectedTransport)connections.get(conn_id); old_conn.halt(); connections.remove(conn_id); } else if (connections.size()>=nmax_connections) { // remove the older unused connection printLog("reached the maximum number of connection: removing the older unused connection",LogLevel.HIGH); long older_time=System.currentTimeMillis(); ConnectionIdentifier older_id=null; for (Enumeration e=connections.elements(); e.hasMoreElements(); ) { ConnectedTransport co=(ConnectedTransport)e.nextElement(); if (co.getLastTimeMillis()<older_time) older_id=new ConnectionIdentifier(co); } if (older_id!=null) removeConnection(older_id); } connections.put(conn_id,conn); conn_id=new ConnectionIdentifier(conn); conn=(ConnectedTransport)connections.get(conn_id); // DEBUG log: printLog("active connenctions:",LogLevel.LOW); for (Enumeration e=connections.keys(); e.hasMoreElements(); ) { ConnectionIdentifier id=(ConnectionIdentifier)e.nextElement(); printLog("conn-id="+id+": "+((ConnectedTransport)connections.get(id)).toString(),LogLevel.LOW); } } /** Removes a Connection */ private void removeConnection(ConnectionIdentifier conn_id) { if (connections.containsKey(conn_id)) { ConnectedTransport conn=(ConnectedTransport)connections.get(conn_id); conn.halt(); connections.remove(conn_id); // DEBUG log: printLog("active connenctions:",LogLevel.LOW); for (Enumeration e=connections.elements(); e.hasMoreElements(); ) { ConnectedTransport co=(ConnectedTransport)e.nextElement(); printLog("conn "+co.toString(),LogLevel.LOW); } } } //************************* Callback methods ************************* /** When a new SIP message is received. */ public void onReceivedMessage(Transport transport, Message msg) { processReceivedMessage(msg); } /** When Transport terminates. */ public void onTransportTerminated(Transport transport, Exception error) { printLog("transport "+transport+" terminated",LogLevel.MEDIUM); if (transport.getProtocol().equals(PROTO_TCP)) { ConnectionIdentifier conn_id=new ConnectionIdentifier((ConnectedTransport)transport); removeConnection(conn_id); } if (error!=null) printException(error,LogLevel.HIGH); } /** When a new incoming Connection is established */ public void onIncomingConnection(TcpServer tcp_server, TcpSocket socket) { printLog("incoming connection from "+socket.getAddress()+":"+socket.getPort(),LogLevel.MEDIUM); ConnectedTransport conn=new TcpTransport(socket,this); printLog("tcp connection "+conn+" opened",LogLevel.MEDIUM); addConnection(conn); } /** When TcpServer terminates. */ public void onServerTerminated(TcpServer tcp_server, Exception error) { printLog("tcp server "+tcp_server+" terminated",LogLevel.MEDIUM); } //************************** Other methods *************************** /** Picks a fresh branch value. * The branch ID MUST be unique across space and time for * all requests sent by the UA. * The branch ID always begin with the characters "z9hG4bK". These * 7 characters are used by RFC 3261 as a magic cookie. */ public static String pickBranch() { //String str=Long.toString(Math.abs(Random.nextLong()),16); //if (str.length()<5) str+="00000"; //return "z9hG4bK"+str.substring(0,5); return "z9hG4bK"+Random.nextNumString(5)+((breaker++)%100000); } /** Picks an unique branch value based on a SIP message. * This value could also be used as transaction ID */ public String pickBranch(Message msg) { StringBuffer sb=new StringBuffer(); sb.append(msg.getRequestLine().getAddress().toString()); sb.append(getViaAddress()+getPort()); ViaHeader top_via=msg.getViaHeader(); if (top_via.hasBranch()) sb.append(top_via.getBranch()); else { sb.append(top_via.getHost()+top_via.getPort()); sb.append(msg.getCSeqHeader().getSequenceNumber()); sb.append(msg.getCallIdHeader().getCallId()); sb.append(msg.getFromHeader().getTag()); sb.append(msg.getToHeader().getTag()); } //return "z9hG4bK"+(new MD5(unique_str)).asHex().substring(0,9); return "z9hG4bK"+(new SimpleDigest(5,sb.toString())).asHex(); } /** Picks a new tag. * A tag MUST be globally unique and cryptographically random * with at least 32 bits of randomness. A property of this selection * requirement is that a UA will place a different tag into the From * header of an INVITE than it would place into the To header of the * response to the same INVITE. This is needed in order for a UA to * invite itself to a session. */ public static String pickTag() { //String str=Long.toString(Math.abs(Random.nextLong()),16); //if (str.length()<8) str+="00000000"; //return str.substring(0,8); return "z9hG4bK"+Random.nextNumString(8); } /** Picks a new tag. The tag is generated uniquely based on message <i>req</i>. * This tag can be generated for responses in a stateless * manner - in a manner that will generate the same tag for the * same request consistently. */ public static String pickTag(Message req) { //return String.valueOf(tag_generator++); //return (new MD5(request.toString())).asHex().substring(0,8); return (new SimpleDigest(8,req.toString())).asHex(); } /** Picks a new call-id. * The call-id is a globally unique * identifier over space and time. It is implemented in the * form "localid@host". Call-id must be considered case-sensitive and is * compared byte-by-byte. */ public String pickCallId() { //String str=Long.toString(Math.abs(Random.nextLong()),16); //if (str.length()<12) str+="000000000000"; //return str.substring(0,12)+"@"+getViaAddress(); return Random.nextNumString(12)+"@"+getViaAddress(); } /** picks an initial CSeq */ public static int pickInitialCSeq() { return 1; } /** (<b>Deprecated</b>) Constructs a NameAddress based on an input string. * The input string can be a: * <br> - <i>user</i> name, * <br> - <i>user@address</i> url, * <br> - <i>"Name" <sip:user@address></i> address, * <p> * In the former case, * a SIP URL is costructed using the outbound proxy as host address if present, * otherwise the local via address is used. */ public NameAddress completeNameAddress(String str) { if (str.indexOf("<sip:") >= 0) return new NameAddress(str); else { SipURL url = completeSipURL(str); return new NameAddress(url); } } /** Constructs a SipURL based on an input string. */ private SipURL completeSipURL(String str) { // in case it is passed only the 'user' field, add '@'<outbound_proxy>[':'<outbound_port>] if (!str.startsWith("sip:") && str.indexOf("@")<0 && str.indexOf(".") < 0 && str.indexOf(":") < 0) { // may be it is just the user name.. String url = "sip:" + str + "@"; if (outbound_proxy != null) { url += outbound_proxy.getAddress().toString(); int port = outbound_proxy.getPort(); if (port > 0 && port != SipStack.default_port) url += ":" + port; } else { url += via_addr; if (host_port > 0 && host_port != SipStack.default_port) url += ":" + host_port; } return new SipURL(url); } else return new SipURL(str); } /** Constructs a SipURL for the given <i>username</i> on the local SIP UA. * If <i>username</i> is null, only host address and port are used. */ /*public SipURL getSipURL(String user_name) { return new SipURL(user_name,via_addr,(host_port!=SipStack.default_port)?host_port:-1); }*/ //******************************* Logs ******************************* /** Gets a String value for this object */ public String toString() { if (host_ipaddr==null) return host_port+"/"+transportProtocolsToString(); else return host_ipaddr.toString()+":"+host_port+"/"+transportProtocolsToString(); } /** Adds a new string to the default Log */ private final void printLog(String str, int level) { if (event_log!=null) { String provider_id=(host_ipaddr==null)? Integer.toString(host_port) : host_ipaddr.toString()+":"+host_port; event_log.println("SipProvider-"+provider_id+": "+str,level+SipStack.LOG_LEVEL_TRANSPORT); } } /** Adds a WARNING to the default Log */ private final void printWarning(String str, int level) { printLog("WARNING: "+str,level); } /** Adds the Exception message to the default Log */ private final void printException(Exception e, int level) { if (event_log!=null) event_log.printException(e,level+SipStack.LOG_LEVEL_TRANSPORT); } /** Adds the SIP message to the messageslog */ private final void printMessageLog(String proto, String addr, int port, int len, Message msg, String str) { if (log_all_packets || len>=MIN_MESSAGE_LENGTH) { if (message_log!=null) { message_log.printPacketTimestamp(proto,addr,port,len,str+"\r\n"+msg.toString()+"-----End-of-message-----\r\n",1); } if (event_log!=null) { String first_line=msg.getFirstLine(); if (first_line!=null) first_line=first_line.trim(); else first_line="NOT a SIP message"; event_log.print("\r\n"); event_log.printPacketTimestamp(proto,addr,port,len,first_line+", "+str,1); event_log.print("\r\n"); } } } }