org.restcomm.connect.telephony.CallManager Maven / Gradle / Ivy
/*
* 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
*
*/
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 org.restcomm.connect.extension.api.CallRequest;
import org.restcomm.connect.extension.api.ExtensionResponse;
import org.restcomm.connect.extension.api.ExtensionType;
import org.restcomm.connect.monitoringservice.MonitoringService;
import org.restcomm.connect.extension.api.RestcommExtensionGeneric;
import gov.nist.javax.sip.header.UserAgent;
import org.apache.commons.configuration.Configuration;
import org.joda.time.DateTime;
import org.restcomm.connect.extension.controller.ExtensionController;
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.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.commons.configuration.RestcommConfiguration;
import org.restcomm.connect.dao.AccountsDao;
import org.restcomm.connect.dao.ApplicationsDao;
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.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.commons.dao.Sid;
import org.restcomm.connect.interpreter.StartInterpreter;
import org.restcomm.connect.interpreter.StopInterpreter;
import org.restcomm.connect.interpreter.VoiceInterpreterBuilder;
import org.restcomm.connect.mscontrol.api.MediaServerControllerFactory;
import org.restcomm.connect.commons.patterns.StopObserving;
import org.restcomm.connect.telephony.api.util.B2BUAHelper;
import org.restcomm.connect.telephony.api.util.CallControlHelper;
import org.restcomm.connect.commons.util.SdpUtils;
import org.restcomm.connect.commons.util.UriUtils;
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.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.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_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;
/**
* @author [email protected] (Thomas Quintana)
* @author [email protected]
* @author [email protected]
* @author [email protected]
*/
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 = "[email protected]";
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;
//List of extensions for CallManager
List extensions;
// 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());
extensions = ExtensionController.getInstance().getExtensions(ExtensionType.CallManager);
if (logger.isInfoEnabled()) {
logger.info("CallManager extensions: "+(extensions != null ? extensions.size() : "0"));
}
}
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()+", call isTerminated(): "+sender().isTerminated()+", sender: "+sender());
}
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()) {
// String destination = ((SipURI)request.getTo().getURI()).getUser();
CallRequest callRequest = new CallRequest(fromUser,toUser, CallRequest.Type.PSTN, client.getAccountSid());
if (executePreOutboundAction(callRequest)) {
proxyOut(request, client, toUser, toHost, toHostIpAddress, toPort, outboundIntf, proxyURI, proxyUsername, proxyPassword, from, to, callToSipUri);
return;
} else {
//log here
final SipServletResponse response = request.createResponse(SC_FORBIDDEN);
response.send();
if (logger.isInfoEnabled()) {
logger.info("Call request rejected: "+callRequest.toString());
}
}
} 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 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 = ""+destNumber+" ";
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(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);
}
}
/**
* 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(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.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
© 2015 - 2025 Weber Informatics LLC | Privacy Policy