Java tutorial
/* * TeleStax, Open Source Cloud Communications * Copyright 2011-2014, Telestax Inc and individual contributors * by the @authors tag. * * This program is free software: you can redistribute it and/or modify * under the terms of the GNU Affero General Public License as * published by the Free Software Foundation; either version 3 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/> * */ package org.restcomm.connect.telephony; import akka.actor.ActorContext; import akka.actor.ActorRef; import akka.actor.ActorSystem; import akka.actor.Props; import akka.actor.ReceiveTimeout; import akka.actor.UntypedActor; import akka.actor.UntypedActorContext; import akka.actor.UntypedActorFactory; import akka.event.Logging; import akka.event.LoggingAdapter; import akka.util.Timeout; import com.google.i18n.phonenumbers.NumberParseException; import com.google.i18n.phonenumbers.PhoneNumberUtil; import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat; import gov.nist.javax.sip.header.UserAgent; import org.apache.commons.configuration.Configuration; import org.joda.time.DateTime; import org.restcomm.connect.commons.configuration.RestcommConfiguration; import org.restcomm.connect.commons.dao.Sid; import org.restcomm.connect.commons.patterns.StopObserving; import org.restcomm.connect.commons.telephony.CreateCallType; import org.restcomm.connect.commons.util.SdpUtils; import org.restcomm.connect.commons.util.UriUtils; import org.restcomm.connect.dao.AccountsDao; import org.restcomm.connect.dao.ApplicationsDao; import org.restcomm.connect.dao.CallDetailRecordsDao; import org.restcomm.connect.dao.ClientsDao; import org.restcomm.connect.dao.DaoManager; import org.restcomm.connect.dao.IncomingPhoneNumbersDao; import org.restcomm.connect.dao.NotificationsDao; import org.restcomm.connect.dao.RegistrationsDao; import org.restcomm.connect.dao.entities.Account; import org.restcomm.connect.dao.entities.Application; import org.restcomm.connect.dao.entities.CallDetailRecord; import org.restcomm.connect.dao.entities.Client; import org.restcomm.connect.dao.entities.IncomingPhoneNumber; import org.restcomm.connect.dao.entities.Notification; import org.restcomm.connect.dao.entities.Registration; import org.restcomm.connect.extension.api.ExtensionType; import org.restcomm.connect.extension.api.IExtensionCreateCallRequest; import org.restcomm.connect.extension.api.RestcommExtensionException; import org.restcomm.connect.extension.api.RestcommExtensionGeneric; import org.restcomm.connect.extension.controller.ExtensionController; import org.restcomm.connect.interpreter.StartInterpreter; import org.restcomm.connect.interpreter.StopInterpreter; import org.restcomm.connect.interpreter.VoiceInterpreterBuilder; import org.restcomm.connect.monitoringservice.MonitoringService; import org.restcomm.connect.mscontrol.api.MediaServerControllerFactory; import org.restcomm.connect.telephony.api.CallInfo; import org.restcomm.connect.telephony.api.CallManagerResponse; import org.restcomm.connect.telephony.api.CallResponse; import org.restcomm.connect.telephony.api.CallStateChanged; import org.restcomm.connect.telephony.api.CreateCall; import org.restcomm.connect.telephony.api.DestroyCall; import org.restcomm.connect.telephony.api.ExecuteCallScript; import org.restcomm.connect.telephony.api.GetActiveProxy; import org.restcomm.connect.telephony.api.GetCall; import org.restcomm.connect.telephony.api.GetCallInfo; import org.restcomm.connect.telephony.api.GetCallObservers; import org.restcomm.connect.telephony.api.GetProxies; import org.restcomm.connect.telephony.api.GetRelatedCall; import org.restcomm.connect.telephony.api.Hangup; import org.restcomm.connect.telephony.api.InitializeOutbound; import org.restcomm.connect.telephony.api.SwitchProxy; import org.restcomm.connect.telephony.api.UpdateCallScript; import org.restcomm.connect.telephony.api.util.B2BUAHelper; import org.restcomm.connect.telephony.api.util.CallControlHelper; import scala.concurrent.Await; import scala.concurrent.Future; import scala.concurrent.duration.Duration; import javax.sdp.SdpParseException; import javax.servlet.ServletContext; import javax.servlet.sip.Address; import javax.servlet.sip.AuthInfo; import javax.servlet.sip.ServletParseException; import javax.servlet.sip.SipApplicationSession; import javax.servlet.sip.SipApplicationSessionEvent; import javax.servlet.sip.SipFactory; import javax.servlet.sip.SipServletRequest; import javax.servlet.sip.SipServletResponse; import javax.servlet.sip.SipSession; import javax.servlet.sip.SipURI; import javax.servlet.sip.TelURL; import javax.sip.header.RouteHeader; import javax.sip.message.Response; import java.io.IOException; import java.net.InetAddress; import java.net.URI; import java.net.URISyntaxException; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Pattern; import static akka.pattern.Patterns.ask; import static javax.servlet.sip.SipServlet.OUTBOUND_INTERFACES; import static javax.servlet.sip.SipServletResponse.SC_ACCEPTED; import static javax.servlet.sip.SipServletResponse.SC_BAD_REQUEST; import static javax.servlet.sip.SipServletResponse.SC_FORBIDDEN; import static javax.servlet.sip.SipServletResponse.SC_NOT_FOUND; import static javax.servlet.sip.SipServletResponse.SC_OK; import static javax.servlet.sip.SipServletResponse.SC_SERVER_INTERNAL_ERROR; /** * @author quintana.thomas@gmail.com (Thomas Quintana) * @author ivelin.ivanov@telestax.com * @author jean.deruelle@telestax.com * @author gvagenas@telestax.com */ public final class CallManager extends UntypedActor { static final int ERROR_NOTIFICATION = 0; static final int WARNING_NOTIFICATION = 1; static final Pattern PATTERN = Pattern.compile("[\\*#0-9]{1,12}"); static final String EMAIL_SENDER = "restcomm@restcomm.org"; static final String EMAIL_SUBJECT = "RestComm Error Notification - Attention Required"; static final int DEFAUL_IMS_PROXY_PORT = -1; private final ActorSystem system; private final Configuration configuration; private final ServletContext context; private final MediaServerControllerFactory msControllerFactory; private final ActorRef conferences; private final ActorRef bridges; private final ActorRef sms; private final SipFactory sipFactory; private final DaoManager storage; private final ActorRef monitoring; // configurable switch whether to use the To field in a SIP header to determine the callee address // alternatively the Request URI can be used private boolean useTo; private boolean authenticateUsers; private AtomicInteger numberOfFailedCalls; private AtomicBoolean useFallbackProxy; private boolean allowFallback; private boolean allowFallbackToPrimary; private int maxNumberOfFailedCalls; private String primaryProxyUri; private String primaryProxyUsername, primaryProxyPassword; private String fallBackProxyUri; private String fallBackProxyUsername, fallBackProxyPassword; private String activeProxy; private String activeProxyUsername, activeProxyPassword; private String mediaExternalIp; private String myHostIp; private String proxyIp; private final LoggingAdapter logger = Logging.getLogger(getContext().system(), this); private CreateCall createCallRequest; private SwitchProxy switchProxyRequest; //Control whether Restcomm will patch Request-URI and SDP for B2BUA calls private boolean patchForNatB2BUASessions; //List of extensions for CallManager List<RestcommExtensionGeneric> extensions; // IMS authentication private boolean actAsImsUa; private String imsProxyAddress; private int imsProxyPort; private String imsDomain; private String imsAccount; // used for sending warning and error logs to notification engine and to the console private void sendNotification(String errMessage, int errCode, String errType, boolean createNotification) { NotificationsDao notifications = storage.getNotificationsDao(); Notification notification; if (errType == "warning") { if (logger.isDebugEnabled()) { // https://github.com/RestComm/Restcomm-Connect/issues/1419 moved to debug to avoid polluting logs logger.debug(errMessage); // send message to console } if (createNotification) { notification = notification(ERROR_NOTIFICATION, errCode, errMessage); notifications.addNotification(notification); } } else if (errType == "error") { // https://github.com/RestComm/Restcomm-Connect/issues/1419 moved to debug to avoid polluting logs if (logger.isDebugEnabled()) { logger.debug(errMessage); // send message to console } if (createNotification) { notification = notification(ERROR_NOTIFICATION, errCode, errMessage); notifications.addNotification(notification); } } else if (errType == "info") { // https://github.com/RestComm/Restcomm-Connect/issues/1419 moved to debug to avoid polluting logs if (logger.isDebugEnabled()) { logger.debug(errMessage); // send message to console } } } public CallManager(final Configuration configuration, final ServletContext context, final MediaServerControllerFactory msControllerFactory, final ActorRef conferences, final ActorRef bridges, final ActorRef sms, final SipFactory factory, final DaoManager storage) { super(); this.system = context().system(); this.configuration = configuration; this.context = context; this.msControllerFactory = msControllerFactory; this.conferences = conferences; this.bridges = bridges; this.sms = sms; this.sipFactory = factory; this.storage = storage; final Configuration runtime = configuration.subset("runtime-settings"); final Configuration outboundProxyConfig = runtime.subset("outbound-proxy"); SipURI outboundIntf = outboundInterface("udp"); if (outboundIntf != null) { myHostIp = ((SipURI) outboundIntf).getHost().toString(); } else { String errMsg = "SipURI outboundIntf is null"; sendNotification(errMsg, 14001, "error", false); if (context == null) errMsg = "SipServlet context is null"; sendNotification(errMsg, 14002, "error", false); } Configuration mediaConf = configuration.subset("media-server-manager"); mediaExternalIp = mediaConf.getString("mgcp-server.external-address"); proxyIp = runtime.subset("telestax-proxy").getString("uri").replaceAll("http://", "").replaceAll(":2080", ""); if (mediaExternalIp == null || mediaExternalIp.isEmpty()) mediaExternalIp = myHostIp; if (proxyIp == null || proxyIp.isEmpty()) proxyIp = myHostIp; this.useTo = runtime.getBoolean("use-to"); this.authenticateUsers = runtime.getBoolean("authenticate"); this.primaryProxyUri = outboundProxyConfig.getString("outbound-proxy-uri"); this.primaryProxyUsername = outboundProxyConfig.getString("outbound-proxy-user"); this.primaryProxyPassword = outboundProxyConfig.getString("outbound-proxy-password"); this.fallBackProxyUri = outboundProxyConfig.getString("fallback-outbound-proxy-uri"); this.fallBackProxyUsername = outboundProxyConfig.getString("fallback-outbound-proxy-user"); this.fallBackProxyPassword = outboundProxyConfig.getString("fallback-outbound-proxy-password"); this.activeProxy = primaryProxyUri; this.activeProxyUsername = primaryProxyUsername; this.activeProxyPassword = primaryProxyPassword; numberOfFailedCalls = new AtomicInteger(); numberOfFailedCalls.set(0); useFallbackProxy = new AtomicBoolean(); useFallbackProxy.set(false); allowFallback = outboundProxyConfig.getBoolean("allow-fallback", false); maxNumberOfFailedCalls = outboundProxyConfig.getInt("max-failed-calls", 20); allowFallbackToPrimary = outboundProxyConfig.getBoolean("allow-fallback-to-primary", false); patchForNatB2BUASessions = runtime.getBoolean("patch-for-nat-b2bua-sessions", true); //Monitoring Service this.monitoring = (ActorRef) context.getAttribute(MonitoringService.class.getName()); extensions = ExtensionController.getInstance().getExtensions(ExtensionType.CallManager); if (logger.isInfoEnabled()) { logger.info("CallManager extensions: " + (extensions != null ? extensions.size() : "0")); } if (!runtime.subset("ims-authentication").isEmpty()) { final Configuration imsAuthentication = runtime.subset("ims-authentication"); this.actAsImsUa = imsAuthentication.getBoolean("act-as-ims-ua"); if (actAsImsUa) { this.imsProxyAddress = imsAuthentication.getString("proxy-address"); this.imsProxyPort = imsAuthentication.getInt("proxy-port"); if (imsProxyPort == 0) { imsProxyPort = DEFAUL_IMS_PROXY_PORT; } this.imsDomain = imsAuthentication.getString("domain"); this.imsAccount = imsAuthentication.getString("account"); if (actAsImsUa && (imsProxyAddress == null || imsProxyAddress.isEmpty() || imsDomain == null || imsDomain.isEmpty())) { logger.warning("ims proxy-address or domain is not configured"); } this.actAsImsUa = actAsImsUa && imsProxyAddress != null && !imsProxyAddress.isEmpty() && imsDomain != null && !imsDomain.isEmpty(); } } firstTimeCleanup(); } private void firstTimeCleanup() { if (logger.isInfoEnabled()) logger.info( "Initial CallManager cleanup. Will check running state calls in DB and update state of the calls."); String instanceId = RestcommConfiguration.getInstance().getMain().getInstanceId(); Sid sid = new Sid(instanceId); final CallDetailRecordsDao callDetailRecordsDao = storage.getCallDetailRecordsDao(); callDetailRecordsDao.updateInCompleteCallDetailRecordsToCompletedByInstanceId(sid); List<CallDetailRecord> results = callDetailRecordsDao.getInCompleteCallDetailRecordsByInstanceId(sid); if (logger.isInfoEnabled()) { logger.info("There are: " + results.size() + " calls in progress after cleanup."); } } private ActorRef call(final CreateCall request) { Props props = null; if (request == null) { props = new Props(new UntypedActorFactory() { private static final long serialVersionUID = 1L; @Override public UntypedActor create() throws Exception { return new Call(sipFactory, msControllerFactory.provideCallController(), configuration, null, null, null, null); } }); } else { props = new Props(new UntypedActorFactory() { private static final long serialVersionUID = 1L; @Override public UntypedActor create() throws Exception { return new Call(sipFactory, msControllerFactory.provideCallController(), configuration, request.statusCallback(), request.statusCallbackMethod(), request.statusCallbackEvent(), request.getOutboundProxyHeaders()); } }); } return system.actorOf(props); } private boolean check(final Object message) throws IOException { final SipServletRequest request = (SipServletRequest) message; String content = null; if (request.getRawContent() != null) { content = new String(request.getRawContent()); } if (content == null && request.getContentLength() == 0 || !("application/sdp".equals(request.getContentType()) || content.contains("application/sdp"))) { final SipServletResponse response = request.createResponse(SC_BAD_REQUEST); response.send(); return false; } return true; } private void destroy(final Object message) throws Exception { final UntypedActorContext context = getContext(); final DestroyCall request = (DestroyCall) message; ActorRef call = request.call(); if (call != null) { if (logger.isInfoEnabled()) { logger.info("About to destroy call: " + request.call().path() + ", call isTerminated(): " + sender().isTerminated() + ", sender: " + sender()); } system.stop(call); } } private void invite(final Object message) throws IOException, NumberParseException, ServletParseException { final ActorRef self = self(); final SipServletRequest request = (SipServletRequest) message; // Make sure we handle re-invites properly. if (!request.isInitial()) { SipApplicationSession appSession = request.getApplicationSession(); ActorRef call = null; if (appSession.getAttribute(Call.class.getName()) != null) { call = (ActorRef) appSession.getAttribute(Call.class.getName()); } if (call != null) { if (logger.isInfoEnabled()) { logger.info("For In-Dialog INVITE dispatched to Call actor: " + call.path()); } call.tell(request, self); return; } if (logger.isInfoEnabled()) { logger.info("No call actor found will respond 200OK for In-dialog INVITE: " + request.getRequestURI().toString()); } final SipServletResponse okay = request.createResponse(SC_OK); okay.send(); return; } if (actAsImsUa) { boolean isFromIms = isFromIms(request); if (!isFromIms) { //This is a WebRTC client that dials out to IMS String user = request.getHeader("X-RestComm-Ims-User"); String pass = request.getHeader("X-RestComm-Ims-Password"); request.removeHeader("X-RestComm-Ims-User"); request.removeHeader("X-RestComm-Ims-Password"); imsProxyThroughMediaServer(request, null, request.getTo().getURI(), user, pass, isFromIms); return; } else { //This is a IMS that dials out to WebRTC client imsProxyThroughMediaServer(request, null, request.getTo().getURI(), "", "", isFromIms); return; } } //Run proInboundAction Extensions here // If it's a new invite lets try to handle it. final AccountsDao accounts = storage.getAccountsDao(); final ApplicationsDao applications = storage.getApplicationsDao(); // Try to find an application defined for the client. final SipURI fromUri = (SipURI) request.getFrom().getURI(); String fromUser = fromUri.getUser(); final ClientsDao clients = storage.getClientsDao(); final Client client = clients.getClient(fromUser); if (client != null) { // Make sure we force clients to authenticate. if (!authenticateUsers // https://github.com/Mobicents/RestComm/issues/29 Allow disabling of SIP authentication || CallControlHelper.checkAuthentication(request, storage)) { // if the client has authenticated, try to redirect to the Client VoiceURL app // otherwise continue trying to process the Client invite if (redirectToClientVoiceApp(self, request, accounts, applications, client)) { return; } // else continue trying other ways to handle the request } else { // Since the client failed to authenticate, we will take no further action at this time. return; } } // TODO Enforce some kind of security check for requests coming from outside SIP UAs such as ITSPs that are not // registered final String toUser = CallControlHelper.getUserSipId(request, useTo); final String ruri = ((SipURI) request.getRequestURI()).getHost(); final String toHost = ((SipURI) request.getTo().getURI()).getHost(); final String toHostIpAddress = InetAddress.getByName(toHost).getHostAddress(); final String toPort = String.valueOf(((SipURI) request.getTo().getURI()).getPort()).equalsIgnoreCase("-1") ? "5060" : String.valueOf(((SipURI) request.getTo().getURI()).getHost()); final String transport = ((SipURI) request.getTo().getURI()).getTransportParam() == null ? "udp" : ((SipURI) request.getTo().getURI()).getTransportParam(); SipURI outboundIntf = outboundInterface(transport); if (logger.isInfoEnabled()) { logger.info("ToUser: " + toUser); logger.info("ToHost: " + toHost); logger.info("ruri: " + ruri); logger.info("myHostIp: " + myHostIp); logger.info("mediaExternalIp: " + mediaExternalIp); logger.info("proxyIp: " + proxyIp); } Client toClient = clients.getClient(toUser); if (client != null) { // make sure the caller is a registered client and not some external SIP agent that we have little control over if (toClient != null) { // looks like its a p2p attempt between two valid registered clients, lets redirect to the b2bua if (logger.isInfoEnabled()) { logger.info("Client is not null: " + client.getLogin() + " will try to proxy to client: " + toClient); } ExtensionController ec = ExtensionController.getInstance(); IExtensionCreateCallRequest er = new CreateCall(fromUser, toUser, "", "", false, 0, CreateCallType.CLIENT, client.getAccountSid(), null, null, null, null); ec.executePreOutboundAction(er, this.extensions); if (er.isAllowed()) { if (B2BUAHelper.redirectToB2BUA(request, client, toClient, storage, sipFactory, patchForNatB2BUASessions)) { if (logger.isInfoEnabled()) { logger.info("Call to CLIENT. myHostIp: " + myHostIp + " mediaExternalIp: " + mediaExternalIp + " toHost: " + toHost + " fromClient: " + client.getUri() + " toClient: " + toClient.getUri()); } // if all goes well with proxying the invitation on to the next client // then we can end further processing of this INVITE } else { String errMsg = "Cannot Connect to Client: " + toClient.getFriendlyName() + " : Make sure the Client exist or is registered with Restcomm"; sendNotification(errMsg, 11001, "warning", true); final SipServletResponse resp = request.createResponse(SC_NOT_FOUND, "Cannot complete P2P call"); resp.send(); } } else { //Extensions didn't allowed this call if (logger.isDebugEnabled()) { final String errMsg = "Client not Allowed to make this outbound call"; logger.debug(errMsg); } String errMsg = "Cannot Connect to Client: " + toClient.getFriendlyName() + " : Make sure the Client exist or is registered with Restcomm"; sendNotification(errMsg, 11001, "warning", true); final SipServletResponse resp = request.createResponse(SC_FORBIDDEN, "Call not allowed"); resp.send(); } ec.executePostOutboundAction(er, this.extensions); return; } else { // toClient is null or we couldn't make the b2bua call to another client. check if this call is for a registered // DID (application) if (redirectToHostedVoiceApp(self, request, accounts, applications, toUser, client.getAccountSid())) { // This is a call to a registered DID (application) return; } // This call is not a registered DID (application). Try to proxy out this call. // log to console and to notification engine String errMsg = "A Restcomm Client is trying to call a Number/DID that is not registered with Restcomm"; sendNotification(errMsg, 11002, "info", true); ExtensionController ec = ExtensionController.getInstance(); IExtensionCreateCallRequest er = new CreateCall(fromUser, toUser, "", "", false, 0, CreateCallType.PSTN, client.getAccountSid(), null, null, null, null); ec.executePreOutboundAction(er, this.extensions); if (er.isAllowed()) { if (isWebRTC(request)) { //This is a WebRTC client that dials out //TODO: should we inject headers for this case? proxyThroughMediaServer(request, client, toUser); } else { // https://telestax.atlassian.net/browse/RESTCOMM-335 String proxyURI = activeProxy; String proxyUsername = activeProxyUsername; String proxyPassword = activeProxyPassword; SipURI from = null; SipURI to = null; boolean callToSipUri = false; if (er.getOutboundProxy() != null && !er.getOutboundProxy().isEmpty()) { proxyURI = er.getOutboundProxy(); } if (er.getOutboundProxyUsername() != null && !er.getOutboundProxyUsername().isEmpty()) { proxyUsername = er.getOutboundProxyUsername(); } if (er.getOutboundProxyPassword() != null && !er.getOutboundProxyPassword().isEmpty()) { proxyUsername = er.getOutboundProxyPassword(); } // proxy DID or number if the outbound proxy fields are not empty in the restcomm.xml if (proxyURI != null && !proxyURI.isEmpty()) { //FIXME: not so nice to just inject headers here addHeadersToMessage(request, er.getOutboundProxyHeaders()); proxyOut(request, client, toUser, toHost, toHostIpAddress, toPort, outboundIntf, proxyURI, proxyUsername, proxyPassword, from, to, callToSipUri); } else { errMsg = "Restcomm tried to proxy this call to an outbound party but it seems the outbound proxy is not configured."; sendNotification(errMsg, 11004, "warning", true); } } } else { //Extensions didn't allow this call final SipServletResponse response = request.createResponse(SC_FORBIDDEN, "Call request not allowed"); response.send(); if (logger.isDebugEnabled()) { logger.debug("Call request not allowed: " + er.toString()); } } ec.executePostOutboundAction(er, this.extensions); return; } } else { // Client is null, check if this call is for a registered DID (application) // //First try to check if the call is for a client if (toClient != null) { proxyDialClientThroughMediaServer(request, toClient, toClient.getLogin()); return; } if (redirectToHostedVoiceApp(self, request, accounts, applications, toUser, null)) { // This is a call to a registered DID (application) return; } } final SipServletResponse response = request.createResponse(SC_NOT_FOUND); response.send(); // We didn't find anyway to handle the call. String errMsg = "Restcomm cannot process this call because the destination number " + toUser + "cannot be found or there is application attached to that"; sendNotification(errMsg, 11005, "error", true); } /** * FIXME: duplicated code make into static function or something more optimized * Replace headers * @param SipServletRequest message * @param Map<String, ArrayList<String> > headers */ private void addHeadersToMessage(SipServletRequest message, Map<String, ArrayList<String>> headers) { if (headers != null) { for (Map.Entry<String, ArrayList<String>> entry : headers.entrySet()) { //check if header exists String headerName = entry.getKey(); StringBuilder sb = new StringBuilder(); if (entry.getValue() instanceof ArrayList) { for (String pair : entry.getValue()) { sb.append(";").append(pair); } } if (logger.isDebugEnabled()) { logger.debug("headerName=" + headerName + " headerVal=" + message.getHeader(headerName) + " concatValue=" + sb.toString()); } if (!headerName.equalsIgnoreCase("Request-URI")) { try { String headerVal = message.getHeader(headerName); if (headerVal != null && !headerVal.isEmpty()) { message.setHeader(headerName, headerVal + sb.toString()); } else { message.addHeader(headerName, sb.toString()); } } catch (IllegalArgumentException iae) { if (logger.isErrorEnabled()) { logger.error("Exception while setting message header: " + iae.getMessage()); } } } else { //handle Request-URI javax.servlet.sip.URI reqURI = message.getRequestURI(); if (logger.isDebugEnabled()) { logger.debug("ReqURI=" + reqURI.toString() + " msgReqURI=" + message.getRequestURI()); } for (String keyValPair : entry.getValue()) { String parName = ""; String parVal = ""; int equalsPos = keyValPair.indexOf("="); parName = keyValPair.substring(0, equalsPos); parVal = keyValPair.substring(equalsPos + 1); reqURI.setParameter(parName, parVal); if (logger.isDebugEnabled()) { logger.debug("ReqURI pars =" + parName + "=" + parVal + " equalsPos=" + equalsPos + " keyValPair=" + keyValPair); } } message.setRequestURI(reqURI); if (logger.isDebugEnabled()) { logger.debug("ReqURI=" + reqURI.toString() + " msgReqURI=" + message.getRequestURI()); } } if (logger.isDebugEnabled()) { logger.debug("headerName=" + headerName + " headerVal=" + message.getHeader(headerName)); } } } } private boolean proxyOut(SipServletRequest request, Client client, String toUser, String toHost, String toHostIpAddress, String toPort, SipURI outboundIntf, String proxyURI, String proxyUsername, String proxyPassword, SipURI from, SipURI to, boolean callToSipUri) throws UnknownHostException { final Configuration runtime = configuration.subset("runtime-settings"); final boolean useLocalAddressAtFromHeader = runtime.getBoolean("use-local-address", false); final boolean outboudproxyUserAtFromHeader = runtime.subset("outbound-proxy") .getBoolean("outboudproxy-user-at-from-header", true); final String fromHost = ((SipURI) request.getFrom().getURI()).getHost(); final String fromHostIpAddress = InetAddress.getByName(fromHost).getHostAddress(); // final String fromPort = String.valueOf(((SipURI) request.getFrom().getURI()).getPort()).equalsIgnoreCase("-1") ? "5060" // : String.valueOf(((SipURI) request.getFrom().getURI()).getHost()); if (logger.isInfoEnabled()) { logger.info("fromHost: " + fromHost + "fromHostIP: " + fromHostIpAddress + "myHostIp: " + myHostIp + " mediaExternalIp: " + mediaExternalIp + " toHost: " + toHost + " toHostIP: " + toHostIpAddress + " proxyUri: " + proxyURI); } if ((myHostIp.equalsIgnoreCase(toHost) || mediaExternalIp.equalsIgnoreCase(toHost)) || (myHostIp.equalsIgnoreCase(toHostIpAddress) || mediaExternalIp.equalsIgnoreCase(toHostIpAddress)) // https://github.com/RestComm/Restcomm-Connect/issues/1357 || (fromHost.equalsIgnoreCase(toHost) || fromHost.equalsIgnoreCase(toHostIpAddress)) || (fromHostIpAddress.equalsIgnoreCase(toHost) || fromHostIpAddress.equalsIgnoreCase(toHostIpAddress))) { if (logger.isInfoEnabled()) { logger.info("Call to NUMBER. myHostIp: " + myHostIp + " mediaExternalIp: " + mediaExternalIp + " toHost: " + toHost + " proxyUri: " + proxyURI); } try { if (useLocalAddressAtFromHeader) { if (outboudproxyUserAtFromHeader) { from = (SipURI) sipFactory.createSipURI(proxyUsername, mediaExternalIp + ":" + outboundIntf.getPort()); } else { from = sipFactory.createSipURI(((SipURI) request.getFrom().getURI()).getUser(), mediaExternalIp + ":" + outboundIntf.getPort()); } } else { if (outboudproxyUserAtFromHeader) { // https://telestax.atlassian.net/browse/RESTCOMM-633. Use the outbound proxy username as // the userpart of the sip uri for the From header from = (SipURI) sipFactory.createSipURI(proxyUsername, proxyURI); } else { from = sipFactory.createSipURI(((SipURI) request.getFrom().getURI()).getUser(), proxyURI); } } to = sipFactory.createSipURI(((SipURI) request.getTo().getURI()).getUser(), proxyURI); } catch (Exception exception) { if (logger.isInfoEnabled()) { logger.info("Exception: " + exception); } } } else { if (logger.isInfoEnabled()) { logger.info("Call to SIP URI. myHostIp: " + myHostIp + " mediaExternalIp: " + mediaExternalIp + " toHost: " + toHost + " proxyUri: " + proxyURI); } from = sipFactory.createSipURI(((SipURI) request.getFrom().getURI()).getUser(), outboundIntf.getHost() + ":" + outboundIntf.getPort()); to = sipFactory.createSipURI(toUser, toHost + ":" + toPort); callToSipUri = true; } if (B2BUAHelper.redirectToB2BUA(request, client, from, to, proxyUsername, proxyPassword, storage, sipFactory, callToSipUri, patchForNatB2BUASessions)) { return true; } return false; } private boolean isWebRTC(final SipServletRequest request) { String transport = request.getTransport(); String userAgent = request.getHeader(UserAgent.NAME); //The check for request.getHeader(UserAgentHeader.NAME).equals("sipunit") has been added in order to be able to test this feature with sipunit at the Restcomm testsuite if (userAgent != null && !userAgent.isEmpty() && userAgent.equalsIgnoreCase("wss-sipunit")) { return true; } if (!request.getInitialTransport().equalsIgnoreCase(transport)) { transport = request.getInitialTransport(); if ("ws".equalsIgnoreCase(transport) || "wss".equalsIgnoreCase(transport)) return true; } try { if (SdpUtils.isWebRTCSDP(request.getContentType(), request.getRawContent())) { return true; } } catch (SdpParseException e) { } catch (IOException e) { } return false; } private void proxyThroughMediaServer(final SipServletRequest request, final Client client, final String destNumber) { String rcml = "<Response><Dial>" + destNumber + "</Dial></Response>"; final VoiceInterpreterBuilder builder = new VoiceInterpreterBuilder(system); builder.setConfiguration(configuration); builder.setStorage(storage); builder.setCallManager(self()); builder.setConferenceManager(conferences); builder.setBridgeManager(bridges); builder.setSmsService(sms); builder.setAccount(client.getAccountSid()); builder.setVersion(client.getApiVersion()); final Account account = storage.getAccountsDao().getAccount(client.getAccountSid()); builder.setEmailAddress(account.getEmailAddress()); builder.setRcml(rcml); builder.setMonitoring(monitoring); final ActorRef interpreter = builder.build(); final ActorRef call = call(null); final SipApplicationSession application = request.getApplicationSession(); application.setAttribute(Call.class.getName(), call); call.tell(request, self()); interpreter.tell(new StartInterpreter(call), self()); } private void proxyDialClientThroughMediaServer(final SipServletRequest request, final Client client, final String destNumber) { String rcml = "<Response><Dial><Client>" + destNumber + "</Client></Dial></Response>"; final VoiceInterpreterBuilder builder = new VoiceInterpreterBuilder(system); builder.setConfiguration(configuration); builder.setStorage(storage); builder.setCallManager(self()); builder.setConferenceManager(conferences); builder.setBridgeManager(bridges); builder.setSmsService(sms); builder.setAccount(client.getAccountSid()); builder.setVersion(client.getApiVersion()); final Account account = storage.getAccountsDao().getAccount(client.getAccountSid()); builder.setEmailAddress(account.getEmailAddress()); builder.setRcml(rcml); builder.setMonitoring(monitoring); final ActorRef interpreter = builder.build(); final ActorRef call = call(null); final SipApplicationSession application = request.getApplicationSession(); application.setAttribute(Call.class.getName(), call); call.tell(request, self()); interpreter.tell(new StartInterpreter(call), self()); } private void info(final SipServletRequest request) throws IOException { final ActorRef self = self(); final SipApplicationSession application = request.getApplicationSession(); // if this response is coming from a client that is in a p2p session with another registered client // we will just proxy the response SipSession linkedB2BUASession = B2BUAHelper.getLinkedSession(request); if (linkedB2BUASession != null) { if (logger.isInfoEnabled()) { logger.info(String.format("B2BUA: Got INFO request: \n %s", request)); } request.getSession().setAttribute(B2BUAHelper.B2BUA_LAST_REQUEST, request); SipServletRequest clonedInfo = linkedB2BUASession.createRequest("INFO"); linkedB2BUASession.setAttribute(B2BUAHelper.B2BUA_LAST_REQUEST, clonedInfo); // Issue #307: https://telestax.atlassian.net/browse/RESTCOMM-307 SipURI toInetUri = (SipURI) request.getSession().getAttribute(B2BUAHelper.TO_INET_URI); SipURI fromInetUri = (SipURI) request.getSession().getAttribute(B2BUAHelper.FROM_INET_URI); InetAddress infoRURI = null; try { infoRURI = InetAddress.getByName(((SipURI) clonedInfo.getRequestURI()).getHost()); } catch (UnknownHostException e) { } if (patchForNatB2BUASessions) { if (toInetUri != null && infoRURI == null) { if (logger.isInfoEnabled()) { logger.info("Using the real ip address of the sip client " + toInetUri.toString() + " as a request uri of the CloneBye request"); } clonedInfo.setRequestURI(toInetUri); } else if (toInetUri != null && (infoRURI.isSiteLocalAddress() || infoRURI.isAnyLocalAddress() || infoRURI.isLoopbackAddress())) { if (logger.isInfoEnabled()) { logger.info("Using the real ip address of the sip client " + toInetUri.toString() + " as a request uri of the CloneInfo request"); } clonedInfo.setRequestURI(toInetUri); } else if (fromInetUri != null && (infoRURI.isSiteLocalAddress() || infoRURI.isAnyLocalAddress() || infoRURI.isLoopbackAddress())) { if (logger.isInfoEnabled()) { logger.info("Using the real ip address of the sip client " + fromInetUri.toString() + " as a request uri of the CloneInfo request"); } clonedInfo.setRequestURI(fromInetUri); } } clonedInfo.send(); } else { final ActorRef call = (ActorRef) application.getAttribute(Call.class.getName()); call.tell(request, self); } } private void transfer(SipServletRequest request) throws Exception { //Transferor is the one that initates the transfer String transferor = ((SipURI) request.getAddressHeader("Contact").getURI()).getUser(); //Transferee is the one that gets transfered String transferee = ((SipURI) request.getAddressHeader("To").getURI()).getUser(); //Trasnfer target, where the transferee will be transfered String transferTarget = ((SipURI) request.getAddressHeader("Refer-To").getURI()).getUser(); CallDetailRecord cdr = null; CallDetailRecordsDao dao = storage.getCallDetailRecordsDao(); SipServletResponse servletResponse = null; final SipApplicationSession appSession = request.getApplicationSession(); //Initates the transfer ActorRef transferorActor = (ActorRef) appSession.getAttribute(Call.class.getName()); if (transferorActor == null) { if (logger.isInfoEnabled()) { logger.info("Transferor Call Actor is null, cannot proceed with SIP Refer"); } servletResponse = request.createResponse(SC_NOT_FOUND); servletResponse.setHeader("Reason", "SIP REFER should be sent in dialog"); servletResponse.setHeader("Event", "refer"); servletResponse.send(); return; } final Timeout expires = new Timeout(Duration.create(60, TimeUnit.SECONDS)); Future<Object> infoFuture = (Future<Object>) ask(transferorActor, new GetCallInfo(), expires); CallResponse<CallInfo> infoResponse = (CallResponse<CallInfo>) Await.result(infoFuture, Duration.create(10, TimeUnit.SECONDS)); CallInfo callInfo = infoResponse.get(); //Call must be in-progress to accept Sip Refer if (callInfo != null && callInfo.state().equals(CallStateChanged.State.IN_PROGRESS)) { try { if (callInfo.direction().equalsIgnoreCase("inbound")) { //Transferror is the inbound leg of the call cdr = dao.getCallDetailRecord(callInfo.sid()); } else { //Transferor is the outbound leg of the call cdr = dao.getCallDetailRecord(dao.getCallDetailRecord(callInfo.sid()).getParentCallSid()); } } catch (Exception e) { if (logger.isInfoEnabled()) { logger.info("Problem while trying to get the CDR of the call"); } servletResponse = request.createResponse(SC_SERVER_INTERNAL_ERROR); servletResponse.setHeader("Reason", "SIP Refer problem during execution"); servletResponse.setHeader("Event", "refer"); servletResponse.send(); return; } } else { if (logger.isInfoEnabled()) { logger.info("CallInfo is null or call state not in-progress. Cannot proceed to call transfer"); } servletResponse = request.createResponse(SC_NOT_FOUND); servletResponse.setHeader("Reason", "SIP Refer pre-conditions failed, call info is null or call not in progress"); servletResponse.setHeader("Event", "refer"); servletResponse.send(); return; } final IncomingPhoneNumbersDao numbers = storage.getIncomingPhoneNumbersDao(); String phone = cdr.getTo(); IncomingPhoneNumber number = numbers.getIncomingPhoneNumber(phone); if (number == null) { if (phone.startsWith("+")) { //remove the (+) and check if exists phone = phone.replaceFirst("\\+", ""); number = numbers.getIncomingPhoneNumber(phone); } else { //Add "+" add check if number exists phone = "+".concat(phone); number = numbers.getIncomingPhoneNumber(phone); } } if (number == null) { number = numbers.getIncomingPhoneNumber("*"); } if (number == null || (number.getReferUrl() == null && number.getReferApplicationSid() == null)) { if (logger.isInfoEnabled()) { logger.info( "Refer URL or Refer Applicatio for incoming phone number is null, cannot proceed with SIP Refer"); } servletResponse = request.createResponse(SC_NOT_FOUND); servletResponse.setHeader("Reason", "SIP Refer failed. Set Refer URL or Refer application for incoming phone number"); servletResponse.setHeader("Event", "refer"); servletResponse.send(); return; } // Get first transferorActor leg observers Future<Object> future = (Future<Object>) ask(transferorActor, new GetCallObservers(), expires); CallResponse<List<ActorRef>> response = (CallResponse<List<ActorRef>>) Await.result(future, Duration.create(10, TimeUnit.SECONDS)); List<ActorRef> callObservers = response.get(); // Get the Voice Interpreter currently handling the transferorActor ActorRef existingInterpreter = callObservers.iterator().next(); // Get the outbound leg of this transferorActor future = (Future<Object>) ask(existingInterpreter, new GetRelatedCall(transferorActor), expires); Object answer = (Object) Await.result(future, Duration.create(10, TimeUnit.SECONDS)); //Transferee will be transfered to the transfer target ActorRef transfereeActor = null; if (answer instanceof ActorRef) { transfereeActor = (ActorRef) answer; } else { if (logger.isInfoEnabled()) { logger.info("Transferee is not a Call actor, probably call is on conference"); } servletResponse = request.createResponse(SC_NOT_FOUND); servletResponse.setHeader("Reason", "SIP Refer failed. Transferee is not a Call actor, probably this is a conference"); servletResponse.setHeader("Event", "refer"); servletResponse.send(); transferorActor.tell(new Hangup(), null); return; } servletResponse = request.createResponse(SC_ACCEPTED); servletResponse.setHeader("Event", "refer"); servletResponse.send(); if (logger.isInfoEnabled()) { logger.info("About to start Call Transfer"); logger.info("Transferor Call path: " + transferorActor.path()); if (transfereeActor != null) { logger.info("Transferee Call path: " + transfereeActor.path()); } // Cleanup all observers from both transferorActor legs logger.info("Will tell Call actors to stop observing existing Interpreters"); } if (logger.isDebugEnabled()) { logger.debug( "Call Transfer account: " + cdr.getAccountSid() + ", new RCML url: " + number.getReferUrl()); } transferorActor.tell(new StopObserving(), self()); if (transfereeActor != null) { transfereeActor.tell(new StopObserving(), self()); } if (logger.isInfoEnabled()) { logger.info("Existing observers removed from Calls actors"); // Cleanup existing Interpreter logger.info("Existing Interpreter path: " + existingInterpreter.path() + " will be stopped"); } existingInterpreter.tell(new StopInterpreter(true), null); // Build a new VoiceInterpreter final VoiceInterpreterBuilder builder = new VoiceInterpreterBuilder(system); builder.setConfiguration(configuration); builder.setStorage(storage); builder.setCallManager(self()); builder.setConferenceManager(conferences); builder.setBridgeManager(bridges); builder.setSmsService(sms); builder.setAccount(cdr.getAccountSid()); builder.setVersion(cdr.getApiVersion()); if (number.getReferApplicationSid() != null) { Application application = storage.getApplicationsDao().getApplication(number.getReferApplicationSid()); builder.setUrl(UriUtils.resolve(application.getRcmlUrl())); } else { builder.setUrl(UriUtils.resolve(number.getReferUrl())); } builder.setMethod( (number.getReferMethod() != null && number.getReferMethod().length() > 0) ? number.getReferMethod() : "POST"); builder.setReferTarget(transferTarget); builder.setTransferor(transferor); builder.setTransferee(transferee); builder.setFallbackUrl(null); builder.setFallbackMethod("POST"); builder.setStatusCallback(null); builder.setStatusCallbackMethod("POST"); builder.setMonitoring(monitoring); // Ask first transferorActor leg to execute with the new Interpreter final ActorRef interpreter = builder.build(); system.scheduler().scheduleOnce(Duration.create(500, TimeUnit.MILLISECONDS), interpreter, new StartInterpreter(transfereeActor), system.dispatcher()); if (logger.isInfoEnabled()) { logger.info("New Intepreter for transfereeActor call leg: " + interpreter.path() + " started"); } if (logger.isInfoEnabled()) { logger.info("will hangup transferorActor: " + transferorActor.path()); } transferorActor.tell(new Hangup(), null); } /** * Try to locate a hosted voice app corresponding to the callee/To address. If one is found, begin execution, otherwise * return false; * * @param self * @param request * @param accounts * @param applications * @param phone */ private boolean redirectToHostedVoiceApp(final ActorRef self, final SipServletRequest request, final AccountsDao accounts, final ApplicationsDao applications, String phone, Sid fromClientAccountSid) { boolean isFoundHostedApp = false; // Format the destination to an E.164 phone number. final PhoneNumberUtil phoneNumberUtil = PhoneNumberUtil.getInstance(); String formatedPhone = null; if (!(phone.contains("*") || phone.contains("#"))) { try { formatedPhone = phoneNumberUtil.format(phoneNumberUtil.parse(phone, "US"), PhoneNumberFormat.E164); } catch (NumberParseException e) { if (logger.isInfoEnabled()) { String msg = String.format("Problem while trying to format number %s, exception %s ", phone, e); logger.info(msg); } } } if (formatedPhone == null) { //Don't format to E.164 if phone contains # or * as this is //for a Regex or USSD short number formatedPhone = phone; } IncomingPhoneNumber number = null; try { // Try to find an application defined for the phone number. final IncomingPhoneNumbersDao numbers = storage.getIncomingPhoneNumbersDao(); number = numbers.getIncomingPhoneNumber(formatedPhone); if (number == null) { number = numbers.getIncomingPhoneNumber(phone); } if (number == null) { if (phone.startsWith("+")) { //remove the (+) and check if exists phone = phone.replaceFirst("\\+", ""); number = numbers.getIncomingPhoneNumber(phone); } else { //Add "+" add check if number exists phone = "+".concat(phone); number = numbers.getIncomingPhoneNumber(phone); } } if (number == null) { // https://github.com/Mobicents/RestComm/issues/84 using wildcard as default application number = numbers.getIncomingPhoneNumber("*"); } if (number != null) { final VoiceInterpreterBuilder builder = new VoiceInterpreterBuilder(system); builder.setConfiguration(configuration); builder.setStorage(storage); builder.setCallManager(self); builder.setConferenceManager(conferences); builder.setBridgeManager(bridges); builder.setSmsService(sms); //https://github.com/RestComm/Restcomm-Connect/issues/1939 Sid accSid = fromClientAccountSid == null ? number.getAccountSid() : fromClientAccountSid; builder.setAccount(accSid); builder.setPhone(number.getAccountSid()); builder.setVersion(number.getApiVersion()); // notifications should go to fromClientAccountSid email if not present then to number account // https://github.com/RestComm/Restcomm-Connect/issues/2011 final Account account = accounts.getAccount(accSid); builder.setEmailAddress(account.getEmailAddress()); final Sid sid = number.getVoiceApplicationSid(); if (sid != null) { final Application application = applications.getApplication(sid); builder.setUrl(UriUtils.resolve(application.getRcmlUrl())); } else { builder.setUrl(UriUtils.resolve(number.getVoiceUrl())); } final String voiceMethod = number.getVoiceMethod(); if (voiceMethod == null || voiceMethod.isEmpty()) { builder.setMethod("POST"); } else { builder.setMethod(voiceMethod); } URI uri = number.getVoiceFallbackUrl(); if (uri != null) builder.setFallbackUrl(UriUtils.resolve(uri)); else builder.setFallbackUrl(null); builder.setFallbackMethod(number.getVoiceFallbackMethod()); builder.setStatusCallback(number.getStatusCallback()); builder.setStatusCallbackMethod(number.getStatusCallbackMethod()); builder.setMonitoring(monitoring); final ActorRef interpreter = builder.build(); final ActorRef call = call(null); final SipApplicationSession application = request.getApplicationSession(); application.setAttribute(Call.class.getName(), call); call.tell(request, self); interpreter.tell(new StartInterpreter(call), self); isFoundHostedApp = true; } } catch (Exception notANumber) { String errMsg; if (number != null) { errMsg = "The number " + number.getPhoneNumber() + " does not have a Restcomm hosted application attached"; } else { errMsg = "The number does not exist" + notANumber; } sendNotification(errMsg, 11007, "error", false); logger.warning(errMsg, notANumber); isFoundHostedApp = false; } return isFoundHostedApp; } /** * If there is VoiceUrl provided for a Client configuration, try to begin execution of the RCML app, otherwise return false. * * @param self * @param request * @param accounts * @param applications * @param client */ private boolean redirectToClientVoiceApp(final ActorRef self, final SipServletRequest request, final AccountsDao accounts, final ApplicationsDao applications, final Client client) { Sid applicationSid = client.getVoiceApplicationSid(); URI clientAppVoiceUrl = null; if (applicationSid != null) { final Application application = applications.getApplication(applicationSid); clientAppVoiceUrl = UriUtils.resolve(application.getRcmlUrl()); } if (clientAppVoiceUrl == null) { clientAppVoiceUrl = client.getVoiceUrl(); } boolean isClientManaged = ((applicationSid != null && !applicationSid.toString().isEmpty() && !applicationSid.toString().equals("")) || (clientAppVoiceUrl != null && !clientAppVoiceUrl.toString().isEmpty() && !clientAppVoiceUrl.toString().equals(""))); if (isClientManaged) { final VoiceInterpreterBuilder builder = new VoiceInterpreterBuilder(system); builder.setConfiguration(configuration); builder.setStorage(storage); builder.setCallManager(self); builder.setConferenceManager(conferences); builder.setBridgeManager(bridges); builder.setSmsService(sms); builder.setAccount(client.getAccountSid()); builder.setVersion(client.getApiVersion()); final Account account = accounts.getAccount(client.getAccountSid()); builder.setEmailAddress(account.getEmailAddress()); final Sid sid = client.getVoiceApplicationSid(); builder.setUrl(clientAppVoiceUrl); builder.setMethod(client.getVoiceMethod()); URI uri = client.getVoiceFallbackUrl(); if (uri != null) builder.setFallbackUrl(UriUtils.resolve(uri)); else builder.setFallbackUrl(null); builder.setFallbackMethod(client.getVoiceFallbackMethod()); builder.setMonitoring(monitoring); final ActorRef interpreter = builder.build(); final ActorRef call = call(null); final SipApplicationSession application = request.getApplicationSession(); application.setAttribute(Call.class.getName(), call); call.tell(request, self); interpreter.tell(new StartInterpreter(call), self); } return isClientManaged; } private void pong(final Object message) throws IOException { final SipServletRequest request = (SipServletRequest) message; final SipServletResponse response = request.createResponse(SC_OK); response.send(); } @Override public void onReceive(final Object message) throws Exception { final Class<?> klass = message.getClass(); final ActorRef self = self(); final ActorRef sender = sender(); if (logger.isDebugEnabled()) { logger.debug("######### CallManager new message received, message instanceof : " + klass + " from sender : " + sender.getClass()); } if (message instanceof SipServletRequest) { final SipServletRequest request = (SipServletRequest) message; final String method = request.getMethod(); if (request != null) { if ("INVITE".equals(method)) { if (check(request)) invite(request); } else if ("OPTIONS".equals(method)) { pong(request); } else if ("ACK".equals(method)) { ack(request); } else if ("CANCEL".equals(method)) { cancel(request); } else if ("BYE".equals(method)) { bye(request); } else if ("INFO".equals(method)) { info(request); } else if ("REFER".equals(method)) { transfer(request); } } } else if (CreateCall.class.equals(klass)) { this.createCallRequest = (CreateCall) message; outbound(message, sender); } else if (ExecuteCallScript.class.equals(klass)) { execute(message); } else if (UpdateCallScript.class.equals(klass)) { try { update(message); } catch (final Exception exception) { sender.tell(new CallManagerResponse<ActorRef>(exception), self); } } else if (DestroyCall.class.equals(klass)) { destroy(message); } else if (message instanceof SipServletResponse) { response(message); } else if (message instanceof SipApplicationSessionEvent) { timeout(message); } else if (GetCall.class.equals(klass)) { sender.tell(lookup(message), self); } else if (GetActiveProxy.class.equals(klass)) { sender.tell(getActiveProxy(), self); } else if (SwitchProxy.class.equals(klass)) { this.switchProxyRequest = (SwitchProxy) message; sender.tell(switchProxy(), self); } else if (GetProxies.class.equals(klass)) { sender.tell(getProxies(message), self); } } private void ack(SipServletRequest request) throws IOException { SipServletResponse response = B2BUAHelper.getLinkedResponse(request); // if this is an ACK that belongs to a B2BUA session, then we proxy it to the other client if (response != null) { SipServletRequest ack = response.createAck(); // if (!ack.getHeaders("Route").hasNext() && patchForNatB2BUASessions) { if (patchForNatB2BUASessions) { InetAddress ackRURI = null; try { ackRURI = InetAddress.getByName(((SipURI) ack.getRequestURI()).getHost()); } catch (UnknownHostException e) { } boolean isBehindLB = false; final String initialIpBeforeLB = response.getHeader("X-Sip-Balancer-InitialRemoteAddr"); String initialPortBeforeLB = response.getHeader("X-Sip-Balancer-InitialRemotePort"); if (initialIpBeforeLB != null) { if (initialPortBeforeLB == null) initialPortBeforeLB = "5060"; if (logger.isDebugEnabled()) { logger.debug( "We are behind load balancer, checking if the request URI needs to be patched"); } isBehindLB = true; } // Issue #307: https://telestax.atlassian.net/browse/RESTCOMM-307 SipURI toInetUri = (SipURI) request.getSession().getAttribute(B2BUAHelper.TO_INET_URI); if (toInetUri != null && ackRURI == null) { if (isBehindLB) { // https://github.com/RestComm/Restcomm-Connect/issues/1357 boolean patchRURI = isLBPatchRURI(ack, initialIpBeforeLB, initialPortBeforeLB); if (patchRURI) { if (logger.isDebugEnabled()) { logger.debug( "We are behind load balancer, but Using the real ip address of the sip client " + toInetUri.toString() + " as a request uri of the ACK request"); } ack.setRequestURI(toInetUri); } else { // https://github.com/RestComm/Restcomm-Connect/issues/1357 if (logger.isDebugEnabled()) { logger.debug( "removing the toInetUri to avoid the other subsequent requests using it " + toInetUri.toString()); } request.getSession().removeAttribute(B2BUAHelper.TO_INET_URI); } } else { if (logger.isInfoEnabled()) { logger.info("Using the real ip address of the sip client " + toInetUri.toString() + " as a request uri of the ACK request"); } ack.setRequestURI(toInetUri); } } else if (toInetUri != null && (ackRURI.isSiteLocalAddress() || ackRURI.isAnyLocalAddress() || ackRURI.isLoopbackAddress())) { if (isBehindLB) { // https://github.com/RestComm/Restcomm-Connect/issues/1357 boolean patchRURI = isLBPatchRURI(ack, initialIpBeforeLB, initialPortBeforeLB); if (patchRURI) { if (logger.isDebugEnabled()) { logger.debug( "We are behind load balancer, but Using the real ip address of the sip client " + toInetUri.toString() + " as a request uri of the ACK request"); } ack.setRequestURI(toInetUri); } else { // https://github.com/RestComm/Restcomm-Connect/issues/1357 if (logger.isDebugEnabled()) { logger.debug( "removing the toInetUri to avoid the other subsequent requests using it " + toInetUri.toString()); } request.getSession().removeAttribute(B2BUAHelper.TO_INET_URI); } } else { if (logger.isInfoEnabled()) { logger.info("Using the real ip address of the sip client " + toInetUri.toString() + " as a request uri of the ACK request"); } ack.setRequestURI(toInetUri); } } else if (toInetUri == null && (ackRURI.isSiteLocalAddress() || ackRURI.isAnyLocalAddress() || ackRURI.isLoopbackAddress())) { if (logger.isInfoEnabled()) { logger.info( "Public IP toInetUri from SipSession is null, will check LB headers from last Response"); } if (isBehindLB) { String realIP = initialIpBeforeLB + ":" + initialPortBeforeLB; SipURI uri = sipFactory.createSipURI(null, realIP); boolean patchRURI = isLBPatchRURI(ack, initialIpBeforeLB, initialPortBeforeLB); if (patchRURI) { if (logger.isDebugEnabled()) { logger.debug("We are behind load balancer, will use Initial Remote Address " + initialIpBeforeLB + ":" + initialPortBeforeLB + " for the ACK request"); } ack.setRequestURI(uri); } } else { if (logger.isInfoEnabled()) { logger.info("LB Headers are also null"); } } } } ack.send(); SipApplicationSession sipApplicationSession = request.getApplicationSession(); // Defaulting the sip application session to 1h sipApplicationSession.setExpires(60); } else { if (logger.isInfoEnabled()) { logger.info("Linked Response couldn't be found for ACK request"); } final ActorRef call = (ActorRef) request.getApplicationSession().getAttribute(Call.class.getName()); if (call != null) { if (logger.isInfoEnabled()) { logger.info("Will send ACK to call actor: " + call.path()); } call.tell(request, self()); } } // else { // SipSession sipSession = request.getSession(); // SipApplicationSession sipAppSession = request.getApplicationSession(); // if(sipSession.getInvalidateWhenReady()){ // logger.info("Invalidating sipSession: "+sipSession.getId()); // sipSession.invalidate(); // } // if(sipAppSession.getInvalidateWhenReady()){ // logger.info("Invalidating sipAppSession: "+sipAppSession.getId()); // sipAppSession.invalidate(); // } // } } private boolean isLBPatchRURI(SipServletRequest request, final String initialIpBeforeLB, String initialPortBeforeLB) { try { // https://github.com/RestComm/Restcomm-Connect/issues/1336 checking if the initial IP and Port behind LB is part of the route set or not ListIterator<? extends Address> routes = request.getAddressHeaders(RouteHeader.NAME); while (routes.hasNext()) { SipURI route = (SipURI) routes.next().getURI(); String routeHost = route.getHost(); int routePort = route.getPort(); if (routePort < 0) { routePort = 5060; } if (logger.isDebugEnabled()) { logger.debug("Checking if route " + routeHost + ":" + routePort + " is matching ip and port before LB " + initialIpBeforeLB + ":" + initialPortBeforeLB + " for the " + request.getMethod() + " request"); } if (routeHost.equalsIgnoreCase(initialIpBeforeLB) && routePort == Integer.parseInt(initialPortBeforeLB)) { if (logger.isDebugEnabled()) { logger.debug("route " + route + " is matching ip and port before LB " + initialIpBeforeLB + ":" + initialPortBeforeLB + " for the " + request.getMethod() + " request, so not patching the Request-URI"); } return false; } } } catch (ServletParseException e) { logger.error("Impossible to parse the route set from the request " + request, e); } return true; } private void execute(final Object message) { final ExecuteCallScript request = (ExecuteCallScript) message; final ActorRef self = self(); final VoiceInterpreterBuilder builder = new VoiceInterpreterBuilder(system); builder.setConfiguration(configuration); builder.setStorage(storage); builder.setCallManager(self); builder.setConferenceManager(conferences); builder.setBridgeManager(bridges); builder.setSmsService(sms); builder.setAccount(request.account()); builder.setVersion(request.version()); builder.setUrl(request.url()); builder.setMethod(request.method()); builder.setFallbackUrl(request.fallbackUrl()); builder.setFallbackMethod(request.fallbackMethod()); builder.setMonitoring(monitoring); final ActorRef interpreter = builder.build(); interpreter.tell(new StartInterpreter(request.call()), self); } @SuppressWarnings("unchecked") private void update(final Object message) throws Exception { final UpdateCallScript request = (UpdateCallScript) message; final ActorRef self = self(); final ActorRef call = request.call(); final Boolean moveConnectedCallLeg = request.moveConnecteCallLeg(); // Get first call leg observers final Timeout expires = new Timeout(Duration.create(60, TimeUnit.SECONDS)); Future<Object> future = (Future<Object>) ask(call, new GetCallObservers(), expires); CallResponse<List<ActorRef>> response = (CallResponse<List<ActorRef>>) Await.result(future, Duration.create(10, TimeUnit.SECONDS)); List<ActorRef> callObservers = response.get(); // Get the Voice Interpreter currently handling the call //TODO possible bug here. Since we have more than one call observer, later there might be the case that the first one is not the VI //TODO set the VI using specific message, also get the VI using specific message. The VI will still be in the observers list but it will set/get using specific method ActorRef existingInterpreter = callObservers.iterator().next(); // Get the outbound leg of this call future = (Future<Object>) ask(existingInterpreter, new GetRelatedCall(call), expires); Object answer = (Object) Await.result(future, Duration.create(10, TimeUnit.SECONDS)); ActorRef relatedCall = null; List<ActorRef> listOfRelatedCalls = null; if (answer instanceof ActorRef) { relatedCall = (ActorRef) answer; } else if (answer instanceof List) { listOfRelatedCalls = (List<ActorRef>) answer; } if (logger.isInfoEnabled()) { logger.info("About to start Live Call Modification, moveConnectedCallLeg: " + moveConnectedCallLeg); logger.info("Initial Call path: " + call.path()); if (relatedCall != null) { logger.info("Related Call path: " + relatedCall.path()); } if (listOfRelatedCalls != null) { logger.info("List of related calls received, size of the list: " + listOfRelatedCalls.size()); } // Cleanup all observers from both call legs logger.info("Will tell Call actors to stop observing existing Interpreters"); } if (logger.isDebugEnabled()) { logger.debug("LCM account: " + request.account() + ", moveConnectedCallLeg: " + moveConnectedCallLeg + ", new RCML url: " + request.url()); } call.tell(new StopObserving(), self()); if (relatedCall != null) { relatedCall.tell(new StopObserving(), self()); } if (listOfRelatedCalls != null) { for (ActorRef branch : listOfRelatedCalls) { branch.tell(new StopObserving(), self()); } } if (logger.isInfoEnabled()) { logger.info("Existing observers removed from Calls actors"); // Cleanup existing Interpreter logger.info("Existing Interpreter path: " + existingInterpreter.path() + " will be stopped"); } existingInterpreter.tell(new StopInterpreter(true), null); // Build a new VoiceInterpreter final VoiceInterpreterBuilder builder = new VoiceInterpreterBuilder(system); builder.setConfiguration(configuration); builder.setStorage(storage); builder.setCallManager(self); builder.setConferenceManager(conferences); builder.setBridgeManager(bridges); builder.setSmsService(sms); builder.setAccount(request.account()); builder.setVersion(request.version()); builder.setUrl(request.url()); builder.setMethod(request.method()); builder.setFallbackUrl(request.fallbackUrl()); builder.setFallbackMethod(request.fallbackMethod()); builder.setStatusCallback(request.callback()); builder.setStatusCallbackMethod(request.callbackMethod()); builder.setMonitoring(monitoring); // Ask first call leg to execute with the new Interpreter final ActorRef interpreter = builder.build(); system.scheduler().scheduleOnce(Duration.create(500, TimeUnit.MILLISECONDS), interpreter, new StartInterpreter(request.call()), system.dispatcher()); // interpreter.tell(new StartInterpreter(request.call()), self); if (logger.isInfoEnabled()) { logger.info("New Intepreter for first call leg: " + interpreter.path() + " started"); } // Check what to do with the second/outbound call leg of the call if (relatedCall != null && listOfRelatedCalls == null) { if (moveConnectedCallLeg) { final ActorRef relatedInterpreter = builder.build(); if (logger.isInfoEnabled()) { logger.info("About to redirect related Call :" + relatedCall.path() + " with 200ms delay to related interpreter: " + relatedInterpreter.path()); } system.scheduler().scheduleOnce(Duration.create(1000, TimeUnit.MILLISECONDS), relatedInterpreter, new StartInterpreter(relatedCall), system.dispatcher()); if (logger.isInfoEnabled()) { logger.info("New Intepreter for Second call leg: " + relatedInterpreter.path() + " started"); } } else { if (logger.isInfoEnabled()) { logger.info("moveConnectedCallLeg is: " + moveConnectedCallLeg + " so will hangup relatedCall: " + relatedCall.path()); } relatedCall.tell(new Hangup(), null); // getContext().stop(relatedCall); } } if (listOfRelatedCalls != null) { for (ActorRef branch : listOfRelatedCalls) { branch.tell(new Hangup(), null); } if (logger.isInfoEnabled()) { String msg = String.format("LiveCallModification request while dial forking, terminated %d calls", listOfRelatedCalls.size()); logger.info(msg); } } } private void outbound(final Object message, final ActorRef sender) throws ServletParseException { final CreateCall request = (CreateCall) message; ExtensionController ec = ExtensionController.getInstance(); ec.executePreOutboundAction(request, this.extensions); switch (request.type()) { case CLIENT: { if (request.isAllowed()) { outboundToClient(request, sender); } else { //Extensions didn't allowed this call final String errMsg = "Not Allowed to make this outbound call"; logger.warning(errMsg); sender.tell(new CallManagerResponse<ActorRef>(new RestcommExtensionException(errMsg), this.createCallRequest), self()); } ec.executePostOutboundAction(request, this.extensions); break; } case PSTN: { if (request.isAllowed()) { outboundToPstn(request, sender); } else { //Extensions didn't allowed this call final String errMsg = "Not Allowed to make this outbound call"; logger.warning(errMsg); sender.tell(new CallManagerResponse<ActorRef>(new RestcommExtensionException(errMsg), this.createCallRequest), self()); } ec.executePostOutboundAction(request, this.extensions); break; } case SIP: { if (actAsImsUa) { outboundToIms(request, sender); } else if (request.isAllowed()) { outboundToSip(request, sender); } else { //Extensions didn't allowed this call final String errMsg = "Not Allowed to make this outbound call"; logger.warning(errMsg); sender.tell(new CallManagerResponse<ActorRef>(new RestcommExtensionException(errMsg), this.createCallRequest), self()); } ec.executePostOutboundAction(request, this.extensions); break; } } } private void outboundToClient(final CreateCall request, final ActorRef sender) throws ServletParseException { SipURI outboundIntf = null; SipURI from = null; SipURI to = null; boolean webRTC = false; boolean isLBPresent = false; final RegistrationsDao registrationsDao = storage.getRegistrationsDao(); final String client = request.to().replaceFirst("client:", ""); //1, If this is a WebRTC client check if the instance is the current instance //2. Check if the client has more than one registrations List<Registration> registrationToDial = new CopyOnWriteArrayList<Registration>(); List<Registration> registrations = registrationsDao.getRegistrations(client); if (registrations != null && registrations.size() > 0) { if (logger.isInfoEnabled()) { logger.info("Preparing call for client: " + client + ". There are " + registrations.size() + " registrations at the database for this client"); } for (Registration registration : registrations) { if (registration.isWebRTC()) { if (registration.isLBPresent()) { if (logger.isInfoEnabled()) logger.info("WebRTC registration behind LB. Will add WebRTC registration: " + registration.getLocation() + " to the list to be dialed for client: " + client); registrationToDial.add(registration); } else { //If this is a WebRTC client registration, check that the InstanceId of the registration is for the current Restcomm instance if ((registration.getInstanceId() != null && !registration.getInstanceId() .equals(RestcommConfiguration.getInstance().getMain().getInstanceId()))) { logger.warning("Cannot create call for user agent: " + registration.getLocation() + " since this is a webrtc client registered in another Restcomm instance."); } else { if (logger.isInfoEnabled()) logger.info("Will add WebRTC registration: " + registration.getLocation() + " to the list to be dialed for client: " + client); registrationToDial.add(registration); } } } else { if (logger.isInfoEnabled()) logger.info("Will add registration: " + registration.getLocation() + " to the list to be dialed for client: " + client); registrationToDial.add(registration); } } } else { String errMsg = "The SIP Client " + request.to() + " is not registered or does not exist"; logger.warning(errMsg); sendNotification(errMsg, 11008, "error", true); sender.tell(new CallManagerResponse<ActorRef>(new NullPointerException(errMsg), this.createCallRequest), self()); return; } if (registrationToDial.size() > 0) { if (logger.isInfoEnabled()) { if (registrationToDial.size() > 1) { logger.info( "Preparing call for client: " + client + ", after WebRTC check, Restcomm have to dial :" + registrationToDial.size() + " registrations"); } } List<ActorRef> calls = new CopyOnWriteArrayList<>(); for (Registration registration : registrationToDial) { if (logger.isInfoEnabled()) logger.info("Will proceed to create call for client: " + registration.getLocation() + " registration instanceId: " + registration.getInstanceId() + " own InstanceId: " + RestcommConfiguration.getInstance().getMain().getInstanceId()); String transport; if (registration.getLocation().contains("transport")) { transport = registration.getLocation().split(";")[1].replace("transport=", ""); outboundIntf = outboundInterface(transport); } else { transport = "udp"; outboundIntf = outboundInterface(transport); } if (outboundIntf == null) { String errMsg = "The outbound interface for transport: " + transport + " is NULL, something is wrong with container, cannot proceed to call client " + request.to(); logger.error(errMsg); sendNotification(errMsg, 11008, "error", true); sender.tell(new CallManagerResponse<ActorRef>(new NullPointerException(errMsg), this.createCallRequest), self()); return; } if (request.from() != null && request.from().contains("@")) { // https://github.com/Mobicents/RestComm/issues/150 if it contains @ it means this is a sip uri and we allow // to use it directly from = (SipURI) sipFactory.createURI(request.from()); } else if (request.from() != null) { if (outboundIntf != null) { from = sipFactory.createSipURI(request.from(), mediaExternalIp + ":" + outboundIntf.getPort()); } else { logger.error( "Outbound interface is null, cannot create From header to be used to Dial client: " + client); } } else { from = outboundIntf; } final String location = registration.getLocation(); to = (SipURI) sipFactory.createURI(location); webRTC = registration.isWebRTC(); if (from == null || to == null) { //In case From or To are null we have to cancel outbound call and hnagup initial call if needed final String errMsg = "From and/or To are null, we cannot proceed to the outbound call to: " + request.to(); logger.warning(errMsg); sender.tell(new CallManagerResponse<ActorRef>(new NullPointerException(errMsg), this.createCallRequest), self()); } else { calls.add(createOutbound(request, from, to, webRTC)); } } if (calls.size() > 0) { sender.tell(new CallManagerResponse<List<ActorRef>>(calls), self()); } } else { String errMsg = "The SIP Client " + request.to() + " is not registered or does not exist"; logger.warning(errMsg); sendNotification(errMsg, 11008, "error", true); sender.tell(new CallManagerResponse<ActorRef>(new NullPointerException(errMsg), this.createCallRequest), self()); } } private void outboundToPstn(final CreateCall request, final ActorRef sender) throws ServletParseException { final String uri = (request.getOutboundProxy() != null && (!request.getOutboundProxy().isEmpty())) ? request.getOutboundProxy() : activeProxy; SipURI outboundIntf = null; SipURI from = null; SipURI to = null; final Configuration runtime = configuration.subset("runtime-settings"); final boolean useLocalAddressAtFromHeader = runtime.getBoolean("use-local-address", false); final String proxyUsername = (request.username() != null) ? request.username() : activeProxyUsername; if (uri != null) { try { to = sipFactory.createSipURI(request.to(), uri); String transport = (to.getTransportParam() != null) ? to.getTransportParam() : "udp"; outboundIntf = outboundInterface(transport); final boolean outboudproxyUserAtFromHeader = runtime.subset("outbound-proxy") .getBoolean("outboudproxy-user-at-from-header"); if (request.from() != null && request.from().contains("@")) { // https://github.com/Mobicents/RestComm/issues/150 if it contains @ it means this is a sip uri and we allow // to use it directly from = (SipURI) sipFactory.createURI(request.from()); } else if (useLocalAddressAtFromHeader) { from = sipFactory.createSipURI(request.from(), mediaExternalIp + ":" + outboundIntf.getPort()); } else { if (outboudproxyUserAtFromHeader) { // https://telestax.atlassian.net/browse/RESTCOMM-633. Use the outbound proxy username as the userpart // of the sip uri for the From header from = (SipURI) sipFactory.createSipURI(proxyUsername, uri); } else { from = sipFactory.createSipURI(request.from(), uri); } } if (((SipURI) from).getUser() == null || ((SipURI) from).getUser() == "") { if (uri != null) { from = sipFactory.createSipURI(request.from(), uri); } else { from = (SipURI) sipFactory.createURI(request.from()); } } } catch (Exception exception) { sender.tell(new CallManagerResponse<ActorRef>(exception, this.createCallRequest), self()); } if (from == null || to == null) { //In case From or To are null we have to cancel outbound call and hnagup initial call if needed final String errMsg = "From and/or To are null, we cannot proceed to the outbound call to: " + request.to(); logger.warning(errMsg); sender.tell( new CallManagerResponse<ActorRef>(new NullPointerException(errMsg), this.createCallRequest), self()); } else { sender.tell(new CallManagerResponse<ActorRef>(createOutbound(request, from, to, false)), self()); } } else { String errMsg = "Cannot create call to: " + request.to() + ". The Active Outbound Proxy is null. Please check configuration"; logger.warning(errMsg); sendNotification(errMsg, 11008, "error", true); sender.tell(new CallManagerResponse<ActorRef>(new NullPointerException(errMsg), this.createCallRequest), self()); } } private void outboundToSip(final CreateCall request, final ActorRef sender) throws ServletParseException { final String uri = (request.getOutboundProxy() != null && (!request.getOutboundProxy().isEmpty())) ? request.getOutboundProxy() : ""; SipURI outboundIntf = null; SipURI from = null; SipURI to = (SipURI) sipFactory.createURI(request.to()); if (!uri.isEmpty()) { to.setHost(uri); } String transport = (to.getTransportParam() != null) ? to.getTransportParam() : "udp"; outboundIntf = outboundInterface(transport); if (request.from() == null) { from = outboundInterface(transport); } else { if (request.from() != null && request.from().contains("@")) { // https://github.com/Mobicents/RestComm/issues/150 if it contains @ it means this is a sip uri and we // allow to use it directly from = (SipURI) sipFactory.createURI(request.from()); } else { from = sipFactory.createSipURI(request.from(), outboundIntf.getHost() + ":" + outboundIntf.getPort()); } } if (from == null || to == null) { //In case From or To are null we have to cancel outbound call and hnagup initial call if needed final String errMsg = "From and/or To are null, we cannot proceed to the outbound call to: " + request.to(); logger.warning(errMsg); sender.tell(new CallManagerResponse<ActorRef>(new NullPointerException(errMsg), this.createCallRequest), self()); } else { sender.tell(new CallManagerResponse<ActorRef>(createOutbound(request, from, to, false)), self()); } } private ActorRef createOutbound(final CreateCall request, final SipURI from, final SipURI to, final boolean webRTC) { final Configuration runtime = configuration.subset("runtime-settings"); final String proxyUsername = (request.username() != null) ? request.username() : activeProxyUsername; final String proxyPassword = (request.password() != null) ? request.password() : activeProxyPassword; final ActorRef call = call(request); final ActorRef self = self(); final boolean userAtDisplayedName = runtime.subset("outbound-proxy").getBoolean("user-at-displayed-name"); InitializeOutbound init; if (request.from() != null && !request.from().contains("@") && userAtDisplayedName) { init = new InitializeOutbound(request.from(), from, to, proxyUsername, proxyPassword, request.timeout(), request.isFromApi(), runtime.getString("api-version"), request.accountId(), request.type(), storage, webRTC); } else { init = new InitializeOutbound(null, from, to, proxyUsername, proxyPassword, request.timeout(), request.isFromApi(), runtime.getString("api-version"), request.accountId(), request.type(), storage, webRTC); } if (request.parentCallSid() != null) { init.setParentCallSid(request.parentCallSid()); } call.tell(init, self); return call; } public void cancel(final Object message) throws IOException { final ActorRef self = self(); final SipServletRequest request = (SipServletRequest) message; final SipApplicationSession application = request.getApplicationSession(); // if this response is coming from a client that is in a p2p session with another registered client // we will just proxy the response SipServletRequest originalRequest = B2BUAHelper.getLinkedRequest(request); SipSession linkedB2BUASession = B2BUAHelper.getLinkedSession(request); if (originalRequest != null) { if (logger.isInfoEnabled()) { logger.info(String.format("B2BUA: Got CANCEL request: \n %s", request)); } // SipServletRequest cancel = originalRequest.createCancel(); request.getSession().setAttribute(B2BUAHelper.B2BUA_LAST_REQUEST, request); String sessionState = linkedB2BUASession.getState().name(); SipServletResponse lastFinalResponse = (SipServletResponse) originalRequest.getSession() .getAttribute(B2BUAHelper.B2BUA_LAST_FINAL_RESPONSE); if ((sessionState == SipSession.State.INITIAL.name() || sessionState == SipSession.State.EARLY.name()) && !(lastFinalResponse != null && (lastFinalResponse.getStatus() == 401 || lastFinalResponse.getStatus() == 407))) { SipServletRequest clonedCancel = originalRequest.createCancel(); linkedB2BUASession.setAttribute(B2BUAHelper.B2BUA_LAST_REQUEST, clonedCancel); clonedCancel.send(); } else { SipServletRequest clonedBye = linkedB2BUASession.createRequest("BYE"); linkedB2BUASession.setAttribute(B2BUAHelper.B2BUA_LAST_REQUEST, clonedBye); clonedBye.send(); } // SipServletRequest cancel = originalRequest.createCancel(); // cancel.send(); // originalRequest.createCancel().send(); } else { final ActorRef call = (ActorRef) application.getAttribute(Call.class.getName()); if (call != null) call.tell(request, self); } } public void bye(final Object message) throws IOException { final ActorRef self = self(); final SipServletRequest request = (SipServletRequest) message; final SipApplicationSession application = request.getApplicationSession(); // if this response is coming from a client that is in a p2p session with another registered client // we will just proxy the response SipSession linkedB2BUASession = B2BUAHelper.getLinkedSession(request); if (linkedB2BUASession != null) { if (logger.isInfoEnabled()) { logger.info(String.format("B2BUA: Got BYE request: \n %s", request)); } //Prepare the BYE request to the linked session request.getSession().setAttribute(B2BUAHelper.B2BUA_LAST_REQUEST, request); SipServletRequest clonedBye = linkedB2BUASession.createRequest("BYE"); linkedB2BUASession.setAttribute(B2BUAHelper.B2BUA_LAST_REQUEST, clonedBye); if (patchForNatB2BUASessions) { // Issue #307: https://telestax.atlassian.net/browse/RESTCOMM-307 SipURI toInetUri = (SipURI) request.getSession().getAttribute(B2BUAHelper.TO_INET_URI); SipURI fromInetUri = (SipURI) request.getSession().getAttribute(B2BUAHelper.FROM_INET_URI); InetAddress byeRURI = null; try { byeRURI = InetAddress.getByName(((SipURI) clonedBye.getRequestURI()).getHost()); } catch (UnknownHostException e) { } boolean isBehindLB = false; final String initialIpBeforeLB = request.getHeader("X-Sip-Balancer-InitialRemoteAddr"); String initialPortBeforeLB = request.getHeader("X-Sip-Balancer-InitialRemotePort"); if (initialIpBeforeLB != null) { if (initialPortBeforeLB == null) initialPortBeforeLB = "5060"; if (logger.isDebugEnabled()) { logger.debug( "We are behind load balancer, checking if the request URI needs to be patched"); } isBehindLB = true; } if (logger.isDebugEnabled()) { logger.debug("toInetUri: " + toInetUri + " fromInetUri: " + fromInetUri + " byeRURI: " + byeRURI + " initialIpBeforeLB: " + initialIpBeforeLB + " initialPortBeforeLB: " + initialPortBeforeLB); } if (toInetUri != null && byeRURI == null) { if (logger.isInfoEnabled()) { logger.info("Using the real To inet ip address of the sip client " + toInetUri.toString() + " as a request uri of the CloneBye request"); } clonedBye.setRequestURI(toInetUri); } else if (toInetUri != null && (byeRURI.isSiteLocalAddress() || byeRURI.isAnyLocalAddress() || byeRURI.isLoopbackAddress())) { if (logger.isInfoEnabled()) { logger.info("Using the real To inet ip address of the sip client " + toInetUri.toString() + " as a request uri of the CloneBye request"); } clonedBye.setRequestURI(toInetUri); } else if (fromInetUri != null && (byeRURI.isSiteLocalAddress() || byeRURI.isAnyLocalAddress() || byeRURI.isLoopbackAddress())) { if (isBehindLB) { // https://github.com/RestComm/Restcomm-Connect/issues/1357 boolean patchRURI = isLBPatchRURI(clonedBye, initialIpBeforeLB, initialPortBeforeLB); if (patchRURI) { if (logger.isDebugEnabled()) { logger.debug( "We are behind load balancer, but Using the real ip address of the sip client " + fromInetUri.toString() + " as a request uri of the CloneBye request"); } clonedBye.setRequestURI(fromInetUri); } } else { if (logger.isInfoEnabled()) { logger.info("Using the real From inet ip address of the sip client " + fromInetUri.toString() + " as a request uri of the CloneBye request"); } clonedBye.setRequestURI(fromInetUri); } } else if (toInetUri == null && (byeRURI.isSiteLocalAddress() || byeRURI.isAnyLocalAddress() || byeRURI.isLoopbackAddress())) { if (logger.isInfoEnabled()) { logger.info( "Public IP toInetUri from SipSession is null, will check LB headers from last Response"); } if (isBehindLB) { String realIP = initialIpBeforeLB + ":" + initialPortBeforeLB; SipURI uri = sipFactory.createSipURI(null, realIP); boolean patchRURI = isLBPatchRURI(clonedBye, initialIpBeforeLB, initialPortBeforeLB); if (patchRURI) { if (logger.isDebugEnabled()) { logger.debug("We are behind load balancer, will use: " + initialIpBeforeLB + ":" + initialPortBeforeLB + " for the cloned BYE message"); } clonedBye.setRequestURI(uri); } if (logger.isInfoEnabled()) { logger.info("We are behind load balancer, will use Initial Remote Address " + initialIpBeforeLB + ":" + initialPortBeforeLB + " for the cloned BYE request"); } } else { if (logger.isInfoEnabled()) { logger.info("LB Headers are also null"); } } } } B2BUAHelper.updateCDR(request, CallStateChanged.State.COMPLETED); //Prepare 200 OK for received BYE SipServletResponse okay = request.createResponse(Response.OK); okay.send(); //Send the Cloned BYE if (logger.isInfoEnabled()) { logger.info(String.format("B2BUA: Will send out Cloned BYE request: \n %s", clonedBye)); } clonedBye.send(); } else { final ActorRef call = (ActorRef) application.getAttribute(Call.class.getName()); if (call != null) call.tell(request, self); } } public void response(final Object message) throws IOException { final ActorRef self = self(); final SipServletResponse response = (SipServletResponse) message; // If Allow-Falback is true, check for error reponses and switch proxy if needed if (allowFallback) checkErrorResponse(response); final SipApplicationSession application = response.getApplicationSession(); // if this response is coming from a client that is in a p2p session with another registered client // we will just proxy the response if (B2BUAHelper.isB2BUASession(response)) { if (response.getStatus() == SipServletResponse.SC_PROXY_AUTHENTICATION_REQUIRED || response.getStatus() == SipServletResponse.SC_UNAUTHORIZED) { AuthInfo authInfo = sipFactory.createAuthInfo(); String authHeader = response.getHeader("Proxy-Authenticate"); if (authHeader == null) { authHeader = response.getHeader("WWW-Authenticate"); } String tempRealm = authHeader.substring(authHeader.indexOf("realm=\"") + "realm=\"".length()); String realm = tempRealm.substring(0, tempRealm.indexOf("\"")); authInfo.addAuthInfo(response.getStatus(), realm, activeProxyUsername, activeProxyPassword); SipServletRequest challengeRequest = response.getSession() .createRequest(response.getRequest().getMethod()); response.getSession().setAttribute(B2BUAHelper.B2BUA_LAST_FINAL_RESPONSE, response); challengeRequest.addAuthHeader(response, authInfo); SipServletRequest invite = response.getRequest(); challengeRequest.setContent(invite.getContent(), invite.getContentType()); invite = challengeRequest; challengeRequest.send(); } else { B2BUAHelper.forwardResponse(response, patchForNatB2BUASessions); } } else { if (application.isValid()) { // otherwise the response is coming back to a Voice app hosted by Restcomm final ActorRef call = (ActorRef) application.getAttribute(Call.class.getName()); call.tell(response, self); } } } public ActorRef lookup(final Object message) { final GetCall getCall = (GetCall) message; final String callPath = getCall.getIdentifier(); final ActorContext context = getContext(); // TODO: The context.actorFor has been depreciated for actorSelection at the latest Akka release. ActorRef call = null; if (callPath != null) { try { call = context.actorFor(callPath); } catch (Exception e) { if (logger.isInfoEnabled()) { logger.info("Problem during call lookup, callPath: " + callPath); } return null; } } else { if (logger.isInfoEnabled()) { logger.info("CallPath is null, call lookup cannot be executed"); } } return call; } public void timeout(final Object message) { final ActorRef self = self(); final SipApplicationSessionEvent event = (SipApplicationSessionEvent) message; final SipApplicationSession application = event.getApplicationSession(); final ActorRef call = (ActorRef) application.getAttribute(Call.class.getName()); final ReceiveTimeout timeout = ReceiveTimeout.getInstance(); call.tell(timeout, self); } public void checkErrorResponse(SipServletResponse response) { // Response should not be a proxy branch response and request should be initial and INVITE if (!response.isBranchResponse() && (response.getRequest().getMethod().equalsIgnoreCase("INVITE") && response.getRequest().isInitial())) { final int status = response.getStatus(); // Response status should be > 400 BUT NOT 401, 404, 407 if (status != SipServletResponse.SC_UNAUTHORIZED && status != SipServletResponse.SC_PROXY_AUTHENTICATION_REQUIRED && status != SipServletResponse.SC_NOT_FOUND && status > 400) { int failures = numberOfFailedCalls.incrementAndGet(); if (logger.isInfoEnabled()) { logger.info("A total number of " + failures + " failures have now been counted."); } if (failures >= maxNumberOfFailedCalls) { if (logger.isInfoEnabled()) { logger.info("Max number of failed calls has been reached trying to switch over proxy."); logger.info("Current proxy: " + getActiveProxy().get("ActiveProxy")); } switchProxy(); if (logger.isInfoEnabled()) { logger.info("Switched to proxy: " + getActiveProxy().get("ActiveProxy")); } numberOfFailedCalls.set(0); } } } } public Map<String, String> getActiveProxy() { Map<String, String> activeProxyMap = new ConcurrentHashMap<String, String>(); activeProxyMap.put("ActiveProxy", this.activeProxy); return activeProxyMap; } public Map<String, String> switchProxy() { if (activeProxy.equalsIgnoreCase(primaryProxyUri)) { activeProxy = fallBackProxyUri; activeProxyUsername = fallBackProxyUsername; activeProxyPassword = fallBackProxyPassword; useFallbackProxy.set(true); } else if (allowFallbackToPrimary) { activeProxy = primaryProxyUri; activeProxyUsername = primaryProxyUsername; activeProxyPassword = primaryProxyPassword; useFallbackProxy.set(false); } final Notification notification = notification(WARNING_NOTIFICATION, 14110, "Max number of failed calls has been reached! Outbound proxy switched"); final NotificationsDao notifications = storage.getNotificationsDao(); notifications.addNotification(notification); return getActiveProxy(); } public Map<String, String> getProxies(final Object message) { Map<String, String> proxies = new ConcurrentHashMap<String, String>(); proxies.put("ActiveProxy", activeProxy); proxies.put("UsingFallBackProxy", useFallbackProxy.toString()); proxies.put("AllowFallbackToPrimary", String.valueOf(allowFallbackToPrimary)); proxies.put("PrimaryProxy", primaryProxyUri); proxies.put("FallbackProxy", fallBackProxyUri); return proxies; } private Notification notification(final int log, final int error, final String message) { String version = configuration.subset("runtime-settings").getString("api-version"); Sid accountId = null; // Sid callSid = new Sid("CA00000000000000000000000000000000"); if (createCallRequest != null) { accountId = createCallRequest.accountId(); } else if (switchProxyRequest != null) { accountId = switchProxyRequest.getSid(); } else { accountId = new Sid("ACae6e420f425248d6a26948c17a9e2acf"); } final Notification.Builder builder = Notification.builder(); final Sid sid = Sid.generate(Sid.Type.NOTIFICATION); builder.setSid(sid); builder.setAccountSid(accountId); // builder.setCallSid(callSid); builder.setApiVersion(version); builder.setLog(log); builder.setErrorCode(error); final String base = configuration.subset("runtime-settings").getString("error-dictionary-uri"); StringBuilder buffer = new StringBuilder(); buffer.append(base); if (!base.endsWith("/")) { buffer.append("/"); } buffer.append(error).append(".html"); final URI info = URI.create(buffer.toString()); builder.setMoreInfo(info); builder.setMessageText(message); final DateTime now = DateTime.now(); builder.setMessageDate(now); try { builder.setRequestUrl(new URI("")); } catch (URISyntaxException e) { e.printStackTrace(); } builder.setRequestMethod(""); builder.setRequestVariables(""); buffer = new StringBuilder(); buffer.append("/").append(version).append("/Accounts/"); buffer.append(accountId.toString()).append("/Notifications/"); buffer.append(sid.toString()); final URI uri = URI.create(buffer.toString()); builder.setUri(uri); return builder.build(); } private SipURI outboundInterface(String transport) { SipURI result = null; @SuppressWarnings("unchecked") final List<SipURI> uris = (List<SipURI>) context.getAttribute(OUTBOUND_INTERFACES); for (final SipURI uri : uris) { final String interfaceTransport = uri.getTransportParam(); if (transport.equalsIgnoreCase(interfaceTransport)) { result = uri; } } return result; } private boolean isFromIms(final SipServletRequest request) throws ServletParseException { SipURI uri = (SipURI) request.getRequestURI(); return uri.getUser() == null; } private Registration findRegistration(javax.servlet.sip.URI regUri) { if (regUri == null) { return null; } String formattedNumber = null; if (regUri.isSipURI()) { formattedNumber = ((SipURI) regUri).getUser().replaceFirst("\\+", ""); } else { formattedNumber = ((TelURL) regUri).getPhoneNumber().replaceFirst("\\+", ""); } if (logger.isInfoEnabled()) { logger.info("looking for registrations for number: " + formattedNumber); } final RegistrationsDao registrationsDao = storage.getRegistrationsDao(); List<Registration> registrations = registrationsDao.getRegistrations(formattedNumber); if (registrations == null || registrations.size() == 0) { return null; } else { return registrations.get(0); } } private void imsProxyThroughMediaServer(final SipServletRequest request, final Client client, final javax.servlet.sip.URI destUri, final String user, final String password, boolean isFromIms) throws IOException { javax.servlet.sip.URI srcUri = request.getFrom().getURI(); if (logger.isInfoEnabled()) { logger.info("imsProxyThroughMediaServer, isFromIms: " + isFromIms + ", destUri: " + destUri + ", srcUri: " + srcUri); } final Configuration runtime = configuration.subset("runtime-settings"); String destNumber = destUri.toString(); String rcml = "<Response><Dial>" + destNumber + "</Dial></Response>"; javax.servlet.sip.URI regUri = null; if (isFromIms) { regUri = destUri; } else { regUri = srcUri; } // Lookup of registration is based on srcUri (if call is toward IMS), // or is based on destUri (if call is from IMS) Registration reg = findRegistration(regUri); if (reg == null) { if (logger.isInfoEnabled()) { logger.info("registrations not found"); } final SipServletResponse response = request.createResponse(SC_NOT_FOUND); response.send(); // We didn't find anyway to handle the call. String errMsg = "Call cannot be processed because the registration: " + regUri.toString() + "cannot be found"; sendNotification(errMsg, 11005, "error", true); return; } else { if (isFromIms) { rcml = "<Response><Dial><Client>" + reg.getUserName() + "</Client></Dial></Response>"; } if (logger.isInfoEnabled()) { logger.info("rcml: " + rcml); } final VoiceInterpreterBuilder builder = new VoiceInterpreterBuilder(system); builder.setConfiguration(configuration); builder.setStorage(storage); builder.setCallManager(self()); builder.setConferenceManager(conferences); builder.setBridgeManager(bridges); builder.setSmsService(sms); builder.setAccount(Sid.generate(Sid.Type.ACCOUNT, imsAccount)); builder.setVersion(runtime.getString("api-version")); builder.setRcml(rcml); builder.setMonitoring(monitoring); builder.setAsImsUa(actAsImsUa); if (actAsImsUa) { builder.setImsUaLogin(user); builder.setImsUaPassword(password); } final ActorRef interpreter = builder.build(); final ActorRef call = call(null); final SipApplicationSession application = request.getApplicationSession(); application.setAttribute(Call.class.getName(), call); call.tell(request, self()); interpreter.tell(new StartInterpreter(call), self()); } } private void outboundToIms(final CreateCall request, final ActorRef sender) throws ServletParseException { if (logger.isInfoEnabled()) { logger.info("outboundToIms: " + request); } SipURI from; SipURI to; to = (SipURI) sipFactory.createURI(request.to()); if (request.from() == null) { from = sipFactory.createSipURI(null, imsDomain); } else { if (request.from() != null && request.from().contains("@")) { // https://github.com/Mobicents/RestComm/issues/150 if it contains @ it means this is a sip uri and we // allow to use it directly from = (SipURI) sipFactory.createURI(request.from()); } else { from = sipFactory.createSipURI(request.from(), imsDomain); } } if (from == null || to == null) { //In case From or To are null we have to cancel outbound call and hnagup initial call if needed final String errMsg = "From and/or To are null, we cannot proceed to the outbound call to: " + request.to(); logger.error(errMsg); sender.tell(new CallManagerResponse<ActorRef>(new NullPointerException(errMsg), this.createCallRequest), self()); } else { final ActorRef call = call(request); final ActorRef self = self(); final Configuration runtime = configuration.subset("runtime-settings"); if (logger.isInfoEnabled()) { logger.info("outboundToIms: from: " + from + ", to: " + to); } final String proxyUsername = (request.username() != null) ? request.username() : activeProxyUsername; final String proxyPassword = (request.password() != null) ? request.password() : activeProxyPassword; boolean isToWebRTC = false; Registration toReg = findRegistration(to); if (toReg != null) { isToWebRTC = toReg.isWebRTC(); } if (logger.isInfoEnabled()) { logger.info("outboundToIms: isToWebRTC: " + isToWebRTC); } InitializeOutbound init = new InitializeOutbound(request.from(), from, to, proxyUsername, proxyPassword, request.timeout(), request.isFromApi(), runtime.getString("api-version"), request.accountId(), request.type(), storage, isToWebRTC, true, imsProxyAddress, imsProxyPort); if (request.parentCallSid() != null) { init.setParentCallSid(request.parentCallSid()); } call.tell(init, self); sender.tell(new CallManagerResponse<ActorRef>(call), self()); } } }