/*
* Copyright 2007 Sun Microsystems, Inc.
*
* This file is part of jVoiceBridge.
*
* jVoiceBridge is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation and distributed hereunder
* to you.
*
* jVoiceBridge 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 this program. If not, see <http://www.gnu.org/licenses/>.
*
* Sun designates this particular file as subject to the "Classpath"
* exception as provided by Sun in the License file that accompanied this
* code.
*/
package com.sun.voip.server;
import com.sun.voip.CallParticipant;
import com.sun.voip.Logger;
import java.text.ParseException;
import java.util.logging.Level;
import javax.sip.*;
import javax.sip.address.*;
import javax.sip.header.*;
import javax.sip.message.*;
import java.util.*;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import org.voicebridge.*;
/**
* The SIP Server sets up a SIP Stack and handles SIP requests and responses.
*
* It is the first point of contact for all incoming SIP messages.
*
* It listens for SIP messages on a default port of 5060
* (override by the NIST-SIP property gov.nist.jainsip.stack.enableUDP),
* and forwards the request to the appropriate SipListener according to
* the SIP CallId of the SIP message.
*
* Properties:
* gov.nist.jainsip.stack.enableUDP = 5060;
*/
public class SipServer implements SipListener {
/*
* default port for SIP communication
*/
private static final int SIP_PORT = 5060;
/*
* ip addresses of the SIP Proxy (NIST Proxy Server by default)
*/
private static String defaultSipProxy;
/*
* IP address of the Cisco SIP/VoIP gateways
*/
private static ArrayList<String> voIPGateways = new ArrayList<String>();
private static ArrayList<ProxyCredentials> voIPGatewayLoginInfo = new ArrayList<ProxyCredentials>();
private static ArrayList<RegisterProcessing> registrations = new ArrayList<RegisterProcessing>();
/*
* flag to indicate whether to send SIP Uri's to a proxy or directly
* to the target endpoint.
*/
private static boolean sendSipUriToProxy = false;
/*
* hashtable of SipListeners
*/
private static Hashtable sipListenersTable;
/*
* sip stack variables
*/
private static SipFactory sipFactory;
private static AddressFactory addressFactory;
private static HeaderFactory headerFactory;
private static MessageFactory messageFactory;
private static SipStack sipStack;
private static SipProvider sipProvider;
private static Iterator listeningPoints;
private static SipServerCallback sipServerCallback;
private static InetSocketAddress sipAddress;
public SipServer(Config config, Properties properties) {
sipListenersTable = new Hashtable();
sipServerCallback = new SipServerCallback();
setup(config, properties);
}
/**
* Sets up and initializes the sip server, including the sipstack.
*/
private void setup(Config config, Properties properties)
{
String localHostAddress = config.getPrivateHost();
properties.setProperty("javax.sip.IP_ADDRESS", localHostAddress);
/*
* get IPs of the voip gateways
*/
setVoIPGateways(config);
if (voIPGateways.size() == 0) {
Logger.println("There are no VoIP gateways. You cannot make calls to the phone system.");
}
/*
* Obtain an instance of the singleton SipFactory
*/
sipFactory = SipFactory.getInstance();
/*
* Set path name of SipFactory to implementation.
* used to setup the classpath
*/
sipFactory.setPathName("gov.nist");
try {
/*
* Create SipStack object
*/
sipStack = (SipStack)sipFactory.createSipStack(properties);
/*
* Create AddressFactory
*/
addressFactory = sipFactory.createAddressFactory();
/*
* Create HeaderFactory
*/
headerFactory = sipFactory.createHeaderFactory();
/*
* Create MessageFactory
*/
messageFactory = sipFactory.createMessageFactory();
} catch(PeerUnavailableException e) {
/*
* could not find gov.nist.ri.jain.protocol.ip.sip.
* SipStackImpl in the classpath
*/
Logger.exception("could not stsart sip stack.", e);
return;
} catch(SipException e) {
/*
* could not create SipStack for some other reason
*/
Logger.exception("could not start sip stack.", e);
return;
}
ListeningPoint udpListenPort = null;
ListeningPoint tcpListenPort = null;
try {
/*
* Create SipProvider based on the first ListeningPoint
* Note that this call will block until somebody sends us
* a message because we dont know what IP address and
* port to assign to outgoing messages from this provider
* at this point.
*/
String s = System.getProperty("com.sun.voip.server.SIP_PORT", String.valueOf(SIP_PORT));
int sipPort = Integer.parseInt(s);
Logger.println("");
Logger.println("Bridge private address: " + properties.getProperty("javax.sip.IP_ADDRESS"));
tcpListenPort = sipStack.createListeningPoint(localHostAddress, sipPort, "tcp");
udpListenPort = sipStack.createListeningPoint(localHostAddress, sipPort, "udp");
sipProvider = sipStack.createSipProvider(tcpListenPort);
sipProvider.addListeningPoint(udpListenPort);
sipProvider.addSipListener(this);
sipAddress = new InetSocketAddress(sipStack.getIPAddress(), sipPort);
/*
* get IPs of the SIP Proxy server
*/
defaultSipProxy = config.getDefaultProxy();
/*
* Initialize SipUtil class. Do this last so that
* the other sip stack variables are initialized
*/
SipUtil.initialize();
for (int i = 0; i < voIPGatewayLoginInfo.size(); i++)
{
try {
InetAddress inetAddress = InetAddress.getByName(voIPGateways.get(i));
ProxyCredentials proxyCredentials = voIPGatewayLoginInfo.get(i);
if (proxyCredentials.getAuthUserName() != null)
{
registrations.add(new RegisterProcessing(inetAddress.getHostAddress(), voIPGateways.get(i), proxyCredentials));
}
} catch (Exception e) {
System.out.println(String.format("Bad Address %s ", voIPGateways.get(i)));
continue;
}
}
} catch(TransportAlreadySupportedException e) {
Logger.exception("Stack has transport already supported", e);
return;
} catch(NullPointerException e) {
Logger.exception("Stack has no ListeningPoints", e);
return;
} catch(ObjectInUseException e) {
Logger.exception("Stack has no ListeningPoints", e);
return;
} catch(TransportNotSupportedException e) {
Logger.exception("TransportNotSupportedException", e);
return;
} catch(TooManyListenersException e) {
Logger.exception("TooManyListenersException", e);
return;
} catch(InvalidArgumentException e) {
Logger.exception("InvalidArgumentException", e);
return;
}
Logger.println("Default SIP Proxy: " + defaultSipProxy);
Logger.println("");
}
public static ArrayList<String> getVoIPGateways() {
return voIPGateways;
}
public static ArrayList<ProxyCredentials> getProxyCredentials() {
return voIPGatewayLoginInfo;
}
public static void setVoIPGateways(Config config)
{
voIPGateways = config.getRegistrars();
voIPGatewayLoginInfo = config.getRegistrations();
return;
}
/**
* set flag to indicate whether to send SIP Uri's to a proxy or directly
* to the target endpoint.
*/
public static void setSendSipUriToProxy(boolean sendSipUriToProxy) {
SipServer.sendSipUriToProxy = sendSipUriToProxy;
}
public static boolean getSendSipUriToProxy() {
return sendSipUriToProxy;
}
/**
* Get the IP Address of the SIP Proxy.
* @return SipProxy String with dotted IP address
*/
public static String getDefaultSipProxy() {
return defaultSipProxy;
}
/**
* Set the IP address of the SIP Proxy.
* @param ip String with dotted IP address
*/
public static void setDefaultSipProxy(String defaultSipProxy) {
SipServer.defaultSipProxy = defaultSipProxy;
}
public static InetSocketAddress getSipAddress() {
return sipAddress;
}
/**
* Process transaction timeout. Forwards the event to
* the appropriate sipListener
* @param transactionTimeOutEvent The timeout event
*/
public void processTimeout(javax.sip.TimeoutEvent timeoutEvent) {
try {
/*
* FIXME: Possible BUG - getMessage() for a transaction
* timed out event returns null, so we're unable to
* determine the SIP callId of the event.
*
* Workaround: enumerate through the agents table and
* call processTimeOut() of all agents that are in
* the ONE_PARTY_INVITED state.
*/
//Enumeration e = sipListenersTable.elements();
//while (e.hasMoreElements()) {
//CallSetupAgent callSetupAgent = (CallSetupAgent)e.nextElement();
//int state = callSetupAgent.getState();
//if (state == CallSetupAgent.CALL_PARTICIPANT_INVITED) {
// // callSetupAgent.processTimeOut(timeoutEvent);
// Logger.error("timeout: " + callSetupAgent);
//}
//}
} catch (Exception e) {
/*
* FIXME: if any exception happens at this stage,
* we should send back a 500 Internal Server Error
*/
Logger.exception("processTimeout", e);
}
}
/**
* Process requests received. Forwards the request to
* the appropriate SipListener.
* @param requestEvent the request event
*/
public void processRequest(RequestEvent requestEvent) {
try {
Request request = requestEvent.getRequest();
CallIdHeader callIdHeader = (CallIdHeader)
request.getHeader(CallIdHeader.NAME);
String sipCallId = callIdHeader.getCallId();
SipListener sipListener = findSipListener(requestEvent);
/*
* If there's an existing listener pass the request there.
*/
if (sipListener != null) {
if (request.getMethod().equals(Request.INVITE)) {
duplicateInvite(request);
return;
}
sipListener.processRequest(requestEvent);
return;
} else {
if (request.getMethod().equals(Request.REGISTER)) {
handleRegister(request, requestEvent);
} else if (request.getMethod().equals(Request.OPTIONS)) {
Response res = messageFactory.createResponse(Response.OK, request);
sipProvider.sendResponse(res);
return;
} else if (!request.getMethod().equals(Request.INVITE)) {
Logger.writeFile("sipListener could not be found for " + sipCallId + " " + request.getMethod() + ". Ignoring");
return;
}
}
/*
* An INVITE for an incoming call goes to the IncomingCallHandler.
*/
if (request.getMethod().equals(Request.INVITE)) {
if (SipIncomingCallAgent.addSipCallId(sipCallId) == false) {
FromHeader fromHeader = (FromHeader)
request.getHeader(FromHeader.NAME);
ToHeader toHeader = (ToHeader)
request.getHeader(ToHeader.NAME);
String from = fromHeader.getAddress().toString();
String to = toHeader.getAddress().toString();
Logger.writeFile("SipServer: duplicate INVITE from " + from + " to " + to);
return;
}
CallParticipant cp = new CallParticipant();
String s = SipUtil.getCallIdFromSdp(request);
if (s != null) {
if (Logger.logLevel >= Logger.LOG_MOREINFO) {
Logger.println("Using callId from SDP in INVITE: "
+ s);
}
cp.setCallId(s);
}
s = SipUtil.getConferenceIdFromSdp(request);
if (s != null) {
String[] tokens = s.split(":");
cp.setConferenceId(tokens[0].trim());
if (tokens.length > 1) {
cp.setMediaPreference(tokens[1]);
}
if (tokens.length > 2) {
cp.setConferenceDisplayName(tokens[2]);
}
}
if (SipUtil.getUserNameFromSdp(request) != null) {
cp.setName(SipUtil.getUserNameFromSdp(request));
} else {
cp.setName(SipUtil.getFromName(requestEvent));
}
cp.setDistributedBridge(SipUtil.getDistributedBridgeFromSdp(request));
cp.setPhoneNumber(SipUtil.getFromPhoneNumber(requestEvent));
cp.setToPhoneNumber(SipUtil.getToPhoneNumber(requestEvent));
new IncomingCallHandler(cp, requestEvent);
return;
}
} catch (Exception e) {
/*
* FIXME: if any exception happens at this stage,
* we should send back a 500 Internal Server Error
*/
Logger.exception("processRequest", e);
e.printStackTrace();
}
}
private void duplicateInvite(Request request)
{
try
{
FromHeader fromHeader = (FromHeader) request.getHeader(FromHeader.NAME);
ToHeader toHeader = (ToHeader) request.getHeader(ToHeader.NAME);
String from = fromHeader.getAddress().toString();
String to = toHeader.getAddress().toString();
Response response = messageFactory.createResponse(Response.OK, request);
Logger.writeFile("SipServer: duplicate INVITE from " + from + " to " + to);
Logger.println("RESPONSE " + response);
sipProvider.sendResponse(response);
}
catch (SipException ex)
{
java.util.logging.Logger.getLogger(SipServer.class.getName()).log(Level.SEVERE, null, ex);
}
catch (ParseException ex)
{
java.util.logging.Logger.getLogger(SipServer.class.getName()).log(Level.SEVERE, null, ex);
}
}
private void handleRegister(Request request, RequestEvent requestEvent)
throws Exception {
if (Logger.logLevel >= Logger.LOG_INFO) {
Logger.println(request.toString());
}
Response response = messageFactory.createResponse(
Response.OK, request);
ServerTransaction serverTransaction = requestEvent.getServerTransaction();
if (Logger.logLevel >= Logger.LOG_INFO) {
Logger.println("Response " + response);
}
if (serverTransaction != null) {
serverTransaction.sendResponse(response);
} else {
sipProvider.sendResponse(response);
}
}
/**
* Process responses received. Forward the responses to
* the appropriate SipListener.
* @param responseReceivedEvent the response event
*/
public void processResponse(ResponseEvent responseReceivedEvent)
{
Response response = responseReceivedEvent.getResponse();
if (responseReceivedEvent.getClientTransaction() == null)
{
Logger.error("SipServer processResponse: clientTransaction is null! " + responseReceivedEvent.getResponse());
return;
}
try {
SipListener sipListener = findSipListener(responseReceivedEvent);
if (sipListener != null) {
sipListener.processResponse(responseReceivedEvent);
} else {
/*
* a BYE message could come from a party that already
* has its entry removed from the SipListenersTable. Ignoring.
* This is the desired behaviour if we wished to send BYE
* requests to a party that just got hung up on (e.g.
* if party A hangs up, we send a BYE to party B).
*/
if (response.getStatusCode() != Response.OK && response.getStatusCode() != 201)
{
CallIdHeader callIdHeader = (CallIdHeader) response.getHeader(CallIdHeader.NAME);
//Logger.writeFile("sipListener could not be found for " + callIdHeader.getCallId() + " " + response.getStatusCode() + ". Ignoring");
}
}
} catch (Exception e) {
/* FIXME: if any exception happens at this stage,
* we should send back a 500 Internal Server Error
*/
Logger.exception("processResponse", e);
}
}
/**
* Finds the SipListener responsible for handling the SIP transaction
* associated with the SIP callId
* @param event the EventObject
* @throws Exception general exception when an error occurs
* @return the SipListener for this sipEvent
*/
private SipListener findSipListener(EventObject event) {
String sipCallId = null;
try {
CallIdHeader callIdHeader;
if (event instanceof RequestEvent) {
Request request = ((RequestEvent)event).getRequest();
callIdHeader = (CallIdHeader)
request.getHeader(CallIdHeader.NAME);
} else if (event instanceof ResponseEvent) {
Response response = ((ResponseEvent)event).getResponse();
callIdHeader = (CallIdHeader)
response.getHeader(CallIdHeader.NAME);
} else {
Logger.error("Invalid event object " + event);
return null;
}
sipCallId = callIdHeader.getCallId();
synchronized (sipListenersTable) {
return (SipListener)sipListenersTable.get(sipCallId);
}
} catch (NullPointerException e) {
/*
* most likely due to a null sipCallId
*/
if (sipCallId == null || "".equals(sipCallId)) {
Logger.exception("could not get SIP CallId from incoming"
+ " message. Dropping message", e);
}
throw e;
}
}
/**
* returns the sipStack
* @return the sipStack
*/
public static SipStack getSipStack() {
return sipStack;
}
/**
* returns the headerFactory
* @return the headerFactory
*/
public static HeaderFactory getHeaderFactory() {
return headerFactory;
}
/**
* returns the addressFactory
* @return addressFactory the addressFactory
*/
public static AddressFactory getAddressFactory() {
return addressFactory;
}
/**
* returns the messageFactory
* @return the messageFactory
*/
public static MessageFactory getMessageFactory() {
return messageFactory;
}
/**
* returns the sipProvider
* @return the sipProvider
*/
public static SipProvider getSipProvider() {
return sipProvider;
}
/**
* returns the sipServerCallback
* @return the sipServerCallback
*/
public static SipServerCallback getSipServerCallback() {
return SipServer.sipServerCallback;
}
/**
* Inner class responsible for handling SIP agent registrations and
* unregistrations. Used for callback purposes
*/
class SipServerCallback {
/**
* registers a SIP agent with the given key.
* @param key the key used to distinguish the SIP agent
* @param agent the SIP agent
*/
public void addSipListener(String key, SipListener sipListener) {
synchronized (sipListenersTable) {
if (!sipListenersTable.containsKey(key)) {
sipListenersTable.put(key, sipListener);
} else {
Logger.error("key: " + key + " already mapped!");
}
}
}
/**
* unregisters a SIP agent with the given key.
* @param key the key used to distinguish the SIP agent
*/
public void removeSipListener(String key) {
synchronized (sipListenersTable) {
if (sipListenersTable.containsKey(key)) {
sipListenersTable.remove(key);
} else {
Logger.println("could not find a SipListener "
+ "entry to remove with the key:" + key);
}
}
}
}
public static ServerTransaction getServerTransaction(
RequestEvent requestEvent) throws TransactionDoesNotExistException,
TransactionUnavailableException {
Request request = requestEvent.getRequest();
ServerTransaction st = null;
try {
getSipProvider().getNewServerTransaction(request);
} catch (TransactionAlreadyExistsException e) {
Logger.println("Server transaction already exists for " + request);
st = requestEvent.getServerTransaction();
if (st == null) {
Logger.println("st still null!");
//st = sipStack.findTransaction((SIPRequest) request, true);
}
}
if (st == null) {
Logger.println("Server transaction not found for " + request);
throw new TransactionDoesNotExistException(
"Server transaction not found for " + request);
}
return st;
}
public void processDialogTerminated(DialogTerminatedEvent dte) {
if (Logger.logLevel >= Logger.LOG_SIP) {
Logger.println("processDialogTerminated called");
}
}
public void processTransactionTerminated(TransactionTerminatedEvent tte) {
if (Logger.logLevel >= Logger.LOG_SIP) {
Logger.println("processTransactionTerminated called");
}
}
public void processIOException(IOExceptionEvent ioee) {
if (Logger.logLevel >= Logger.LOG_SIP) {
Logger.println("processTransactionTerminated called");
}
}
public synchronized static ClientTransaction handleChallenge(Response challenge, ClientTransaction challengedTransaction, ProxyCredentials proxyCredentials)
{
try {
String branchID = challengedTransaction.getBranchId();
Request challengedRequest = challengedTransaction.getRequest();
Request reoriginatedRequest = (Request) challengedRequest.clone();
ListIterator authHeaders = null;
if (challenge == null || reoriginatedRequest == null)
throw new NullPointerException("A null argument was passed to handle challenge.");
// CallIdHeader callId =
// (CallIdHeader)challenge.getHeader(CallIdHeader.NAME);
if (challenge.getStatusCode() == Response.UNAUTHORIZED)
authHeaders = challenge.getHeaders(WWWAuthenticateHeader.NAME);
else if (challenge.getStatusCode() == Response.PROXY_AUTHENTICATION_REQUIRED)
authHeaders = challenge.getHeaders(ProxyAuthenticateHeader.NAME);
if (authHeaders == null)
throw new SecurityException(
"Could not find WWWAuthenticate or ProxyAuthenticate headers");
// Remove all authorization headers from the request (we'll re-add
// them
// from cache)
reoriginatedRequest.removeHeader(AuthorizationHeader.NAME);
reoriginatedRequest.removeHeader(ProxyAuthorizationHeader.NAME);
reoriginatedRequest.removeHeader(ViaHeader.NAME);
// rfc 3261 says that the cseq header should be augmented for the
// new
// request. do it here so that the new dialog (created together with
// the new client transaction) takes it into account.
// Bug report - Fredrik Wickstrom
CSeqHeader cSeq = (CSeqHeader) reoriginatedRequest.getHeader((CSeqHeader.NAME));
cSeq.setSequenceNumber(cSeq.getSequenceNumber() + 1);
reoriginatedRequest.setHeader(cSeq);
ClientTransaction retryTran = sipProvider.getNewClientTransaction(reoriginatedRequest);
WWWAuthenticateHeader authHeader = null;
while (authHeaders.hasNext()) {
authHeader = (WWWAuthenticateHeader) authHeaders.next();
String realm = authHeader.getRealm();
FromHeader from = (FromHeader) reoriginatedRequest.getHeader(FromHeader.NAME);
URI uri = from.getAddress().getURI();
AuthorizationHeader authorization = getAuthorization(reoriginatedRequest.getMethod(),
reoriginatedRequest.getRequestURI().toString(),
reoriginatedRequest.getContent() == null ? "" : reoriginatedRequest.getContent().toString(),
authHeader, proxyCredentials);
reoriginatedRequest.addHeader(authorization);
// if there was trouble with the user - make sure we fix it
if (uri.isSipURI()) {
((SipURI) uri).setUser(proxyCredentials.getUserName());
Address add = from.getAddress();
add.setURI(uri);
from.setAddress(add);
reoriginatedRequest.setHeader(from);
if (challengedRequest.getMethod().equals(Request.REGISTER))
{
ToHeader to = (ToHeader) reoriginatedRequest.getHeader(ToHeader.NAME);
add.setURI(uri);
to.setAddress(add);
reoriginatedRequest.setHeader(to);
}
Logger.println("URI: " + uri.toString());
}
// if this is a register - fix to as well
}
return retryTran;
}
catch (Exception e) {
Logger.println("ERRO REG: " + e.toString());
e.printStackTrace();
return null;
}
}
private synchronized static AuthorizationHeader getAuthorization(String method, String uri,
String requestBody, WWWAuthenticateHeader authHeader,
ProxyCredentials proxyCredentials) throws SecurityException {
String response = null;
try {
Logger.println("getAuthorization " + proxyCredentials.getAuthUserName());
response = MessageDigestAlgorithm.calculateResponse(authHeader
.getAlgorithm(), proxyCredentials.getAuthUserName(),
authHeader.getRealm(), new String(proxyCredentials
.getPassword()), authHeader.getNonce(),
// TODO we should one day implement those two null-s
null,// nc-value
null,// cnonce
method, uri, requestBody, authHeader.getQop());
}
catch (NullPointerException exc) {
throw new SecurityException(
"The authenticate header was malformatted");
}
AuthorizationHeader authorization = null;
try {
if (authHeader instanceof ProxyAuthenticateHeader) {
authorization = headerFactory
.createProxyAuthorizationHeader(authHeader.getScheme());
} else {
authorization = headerFactory
.createAuthorizationHeader(authHeader.getScheme());
}
authorization.setUsername(proxyCredentials.getAuthUserName());
authorization.setRealm(authHeader.getRealm());
authorization.setNonce(authHeader.getNonce());
authorization.setParameter("uri", uri);
authorization.setResponse(response);
if (authHeader.getAlgorithm() != null)
authorization.setAlgorithm(authHeader.getAlgorithm());
if (authHeader.getOpaque() != null)
authorization.setOpaque(authHeader.getOpaque());
authorization.setResponse(response);
}
catch (ParseException ex) {
throw new SecurityException("Failed to create an authorization header!");
}
return authorization;
}
/*
private class CRLfKeepAliveTask extends TimerTask
{
@Override
public void run()
{
try
{
tcpListenPort.sendHeartbeat( sipAddress.getAddress().getHostAddress(), sipAddress.getPort() );
}
catch (IOException e)
{
Logger.println("Error while sending a heartbeat", e);
}
}
}
*/
}