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.mobicents.servlet.restcomm.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 com.telestax.servlet.MonitoringService; import gov.nist.javax.sip.header.UserAgent; import org.apache.commons.configuration.Configuration; import org.joda.time.DateTime; import org.mobicents.servlet.restcomm.configuration.RestcommConfiguration; import org.mobicents.servlet.restcomm.dao.AccountsDao; import org.mobicents.servlet.restcomm.dao.ApplicationsDao; import org.mobicents.servlet.restcomm.dao.ClientsDao; import org.mobicents.servlet.restcomm.dao.DaoManager; import org.mobicents.servlet.restcomm.dao.IncomingPhoneNumbersDao; import org.mobicents.servlet.restcomm.dao.NotificationsDao; import org.mobicents.servlet.restcomm.dao.RegistrationsDao; import org.mobicents.servlet.restcomm.entities.Account; import org.mobicents.servlet.restcomm.entities.Application; import org.mobicents.servlet.restcomm.entities.Client; import org.mobicents.servlet.restcomm.entities.IncomingPhoneNumber; import org.mobicents.servlet.restcomm.entities.Notification; import org.mobicents.servlet.restcomm.entities.Registration; import org.mobicents.servlet.restcomm.entities.Sid; import org.mobicents.servlet.restcomm.interpreter.StartInterpreter; import org.mobicents.servlet.restcomm.interpreter.StopInterpreter; import org.mobicents.servlet.restcomm.interpreter.VoiceInterpreterBuilder; import org.mobicents.servlet.restcomm.mscontrol.MediaServerControllerFactory; import org.mobicents.servlet.restcomm.patterns.StopObserving; import org.mobicents.servlet.restcomm.telephony.util.B2BUAHelper; import org.mobicents.servlet.restcomm.telephony.util.CallControlHelper; import org.mobicents.servlet.restcomm.util.UriUtils; import scala.concurrent.Await; import scala.concurrent.Future; import scala.concurrent.duration.Duration; import javax.servlet.ServletContext; 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.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.List; 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_BAD_REQUEST; import static javax.servlet.sip.SipServletResponse.SC_NOT_FOUND; import static javax.servlet.sip.SipServletResponse.SC_OK; /** * @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"; 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; // 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") { logger.warning(errMessage); // send message to console if (createNotification) { notification = notification(ERROR_NOTIFICATION, errCode, errMessage); notifications.addNotification(notification); } } else if (errType == "error") { logger.error(errMessage); // send message to console if (createNotification) { notification = notification(ERROR_NOTIFICATION, errCode, errMessage); notifications.addNotification(notification); } } else if (errType == "info") { if (logger.isInfoEnabled()) { logger.info(errMessage); // send message to console } } } public CallManager(final Configuration configuration, final ServletContext context, final ActorSystem system, final MediaServerControllerFactory msControllerFactory, final ActorRef conferences, final ActorRef bridges, final ActorRef sms, final SipFactory factory, final DaoManager storage) { super(); this.system = 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()); } private ActorRef call() { return system.actorOf(new Props(new UntypedActorFactory() { private static final long serialVersionUID = 1L; @Override public UntypedActor create() throws Exception { return new Call(sipFactory, msControllerFactory.provideCallController(), configuration); } })); } private void check(final Object message) throws IOException { final SipServletRequest request = (SipServletRequest) message; String content = new String(request.getRawContent()); if (request.getContentLength() == 0 || !("application/sdp".equals(request.getContentType()) || content.contains("application/sdp"))) { final SipServletResponse response = request.createResponse(SC_BAD_REQUEST); response.send(); } } 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()); } context.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()) { final SipServletResponse okay = request.createResponse(SC_OK); okay.send(); 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("ToHost: " + toHost); logger.info("ruri: " + ruri); logger.info("myHostIp: " + myHostIp); logger.info("mediaExternalIp: " + mediaExternalIp); logger.info("proxyIp: " + proxyIp); } if (client != null) { // make sure the caller is a registered client and not some external SIP agent that we have little control over Client toClient = clients.getClient(toUser); 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); } 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 return; } else { String errMsg = "Cannot Connect to Client: " + toClient.getFriendlyName() + " : Make sure the Client exist or is registered with Restcomm"; sendNotification(errMsg, 11001, "warning", true); } } 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)) { // 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); if (isWebRTC(request)) { //This is a WebRTC client that dials out proxyThroughMediaServer(request, client, toUser); return; } // https://telestax.atlassian.net/browse/RESTCOMM-335 final String proxyURI = activeProxy; final String proxyUsername = activeProxyUsername; final String proxyPassword = activeProxyPassword; SipURI from = null; SipURI to = null; boolean callToSipUri = false; // proxy DID or number if the outbound proxy fields are not empty in the restcomm.xml if (proxyURI != null && !proxyURI.isEmpty()) { 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); if ((myHostIp.equalsIgnoreCase(toHost) || mediaExternalIp.equalsIgnoreCase(toHost)) || (myHostIp.equalsIgnoreCase(toHostIpAddress) || mediaExternalIp.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; } } else { String msg = "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 { // Client is null, check if this call is for a registered DID (application) if (redirectToHostedVoiceApp(self, request, accounts, applications, toUser)) { // 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); } 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(); return "ws".equalsIgnoreCase(transport) || "wss".equalsIgnoreCase(transport); } 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(); 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("toInetUri"); SipURI fromInetUri = (SipURI) request.getSession().getAttribute("fromInetUri"); 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); } } /** * 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) { boolean isFoundHostedApp = false; // Format the destination to an E.164 phone number. final PhoneNumberUtil phoneNumberUtil = PhoneNumberUtil.getInstance(); String formatedPhone = null; try { formatedPhone = phoneNumberUtil.format(phoneNumberUtil.parse(phone, "US"), PhoneNumberFormat.E164); } catch (Exception e) { } 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); builder.setAccount(number.getAccountSid()); builder.setVersion(number.getApiVersion()); final Account account = accounts.getAccount(number.getAccountSid()); 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(); 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 have a Restcomm hosted application attached"; } sendNotification(errMsg, 11007, "error", false); logger.error(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(); 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 ("INVITE".equals(method)) { 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 (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) { } // Issue #307: https://telestax.atlassian.net/browse/RESTCOMM-307 SipURI toInetUri = (SipURI) request.getSession().getAttribute("toInetUri"); if (toInetUri != null && ackRURI == null) { 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("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"); } 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.isInfoEnabled()) { logger.info("We are behind load balancer, will use Initial Remote Address " + initialIpBeforeLB + ":" + initialPortBeforeLB + " for the ACK request"); } String realIP = initialIpBeforeLB + ":" + initialPortBeforeLB; SipURI uri = sipFactory.createSipURI(null, realIP); 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 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.setStatusCallback(request.callback()); builder.setStatusCallbackMethod(request.callbackMethod()); 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 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; if (answer instanceof ActorRef) { relatedCall = (ActorRef) answer; } if (logger.isInfoEnabled()) { logger.info("About to start Live Call Modification"); logger.info("Initial Call path: " + call.path()); if (relatedCall != null) { logger.info("Related Call path: " + relatedCall.path()); } // Cleanup all observers from both call legs logger.info("Will tell Call actors to stop observing existing Interpreters"); } call.tell(new StopObserving(), self()); if (relatedCall != null) { relatedCall.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) { 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); } } } private void outbound(final Object message, final ActorRef sender) throws ServletParseException { final CreateCall request = (CreateCall) message; switch (request.type()) { case CLIENT: { outboundToClient(request, sender); break; } case PSTN: { outboundToPstn(request, sender); break; } case SIP: { outboundToSip(request, sender); 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.error(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.error(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.error(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 = 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.error(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.error(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 { SipURI outboundIntf = null; SipURI from = null; SipURI to = null; to = (SipURI) sipFactory.createURI(request.to()); 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.error(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(); 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()); 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("toInetUri"); SipURI fromInetUri = (SipURI) request.getSession().getAttribute("fromInetUri"); InetAddress byeRURI = null; try { byeRURI = InetAddress.getByName(((SipURI) clonedBye.getRequestURI()).getHost()); } catch (UnknownHostException e) { } if (toInetUri != null && byeRURI == 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"); } clonedBye.setRequestURI(toInetUri); } else if (toInetUri != null && (byeRURI.isSiteLocalAddress() || byeRURI.isAnyLocalAddress() || byeRURI.isLoopbackAddress())) { if (logger.isInfoEnabled()) { logger.info("Using the real 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 (logger.isInfoEnabled()) { logger.info("Using the real 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"); } 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.isInfoEnabled()) { logger.info("We are behind load balancer, will use Initial Remote Address " + initialIpBeforeLB + ":" + initialPortBeforeLB + " for the cloned BYE request"); } String realIP = initialIpBeforeLB + ":" + initialPortBeforeLB; SipURI uri = sipFactory.createSipURI(null, realIP); clonedBye.setRequestURI(uri); } 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.callPath(); final ActorContext context = getContext(); // TODO: The context.actorFor has been depreciated for actorSelection at the latest Akka release. return context.actorFor(callPath); } 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; } }