Java tutorial
/* * TeleStax, Open Source Cloud Communications * Copyright 2011-2014, Telestax Inc and individual contributors * by the @authors tag. * * This program is free software: you can redistribute it and/or modify * under the terms of the GNU Affero General Public License as * published by the Free Software Foundation; either version 3 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/> * */ package org.restcomm.connect.telephony; import akka.actor.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 org.apache.commons.configuration.Configuration; import org.apache.http.NameValuePair; import org.apache.http.message.BasicNameValuePair; import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import org.mobicents.javax.servlet.sip.SipFactoryExt; import org.mobicents.javax.servlet.sip.SipSessionExt; import org.restcomm.connect.commons.annotations.concurrency.Immutable; import org.restcomm.connect.commons.configuration.RestcommConfiguration; import org.restcomm.connect.commons.dao.Sid; import org.restcomm.connect.commons.fsm.Action; import org.restcomm.connect.commons.fsm.FiniteStateMachine; import org.restcomm.connect.commons.fsm.State; import org.restcomm.connect.commons.fsm.Transition; import org.restcomm.connect.commons.fsm.TransitionFailedException; import org.restcomm.connect.commons.fsm.TransitionNotFoundException; import org.restcomm.connect.commons.fsm.TransitionRollbackException; import org.restcomm.connect.commons.patterns.Observe; import org.restcomm.connect.commons.patterns.Observing; import org.restcomm.connect.commons.patterns.StopObserving; import org.restcomm.connect.commons.telephony.CreateCallType; import org.restcomm.connect.commons.util.SdpUtils; import org.restcomm.connect.dao.CallDetailRecordsDao; import org.restcomm.connect.dao.DaoManager; import org.restcomm.connect.dao.entities.CallDetailRecord; import org.restcomm.connect.http.client.Downloader; import org.restcomm.connect.http.client.HttpRequestDescriptor; import org.restcomm.connect.mscontrol.api.messages.CloseMediaSession; import org.restcomm.connect.mscontrol.api.messages.Collect; import org.restcomm.connect.mscontrol.api.messages.CreateMediaSession; import org.restcomm.connect.mscontrol.api.messages.JoinBridge; import org.restcomm.connect.mscontrol.api.messages.JoinComplete; import org.restcomm.connect.mscontrol.api.messages.JoinConference; import org.restcomm.connect.mscontrol.api.messages.Leave; import org.restcomm.connect.mscontrol.api.messages.Left; import org.restcomm.connect.mscontrol.api.messages.MediaGroupResponse; import org.restcomm.connect.mscontrol.api.messages.MediaServerControllerStateChanged; import org.restcomm.connect.mscontrol.api.messages.MediaSessionInfo; import org.restcomm.connect.mscontrol.api.messages.Mute; import org.restcomm.connect.mscontrol.api.messages.Play; import org.restcomm.connect.mscontrol.api.messages.Record; import org.restcomm.connect.mscontrol.api.messages.StartRecording; import org.restcomm.connect.mscontrol.api.messages.Stop; import org.restcomm.connect.mscontrol.api.messages.StopMediaGroup; import org.restcomm.connect.mscontrol.api.messages.StopRecording; import org.restcomm.connect.mscontrol.api.messages.Unmute; import org.restcomm.connect.mscontrol.api.messages.UpdateMediaSession; import org.restcomm.connect.telephony.api.Answer; import org.restcomm.connect.telephony.api.BridgeStateChanged; import org.restcomm.connect.telephony.api.CallFail; import org.restcomm.connect.telephony.api.CallHoldStateChange; import org.restcomm.connect.telephony.api.CallInfo; import org.restcomm.connect.telephony.api.CallResponse; import org.restcomm.connect.telephony.api.CallStateChanged; import org.restcomm.connect.telephony.api.Cancel; import org.restcomm.connect.telephony.api.ChangeCallDirection; import org.restcomm.connect.telephony.api.ConferenceInfo; import org.restcomm.connect.telephony.api.ConferenceResponse; import org.restcomm.connect.telephony.api.Dial; import org.restcomm.connect.telephony.api.GetCallInfo; import org.restcomm.connect.telephony.api.GetCallObservers; import org.restcomm.connect.telephony.api.Hangup; import org.restcomm.connect.telephony.api.InitializeOutbound; import org.restcomm.connect.telephony.api.Reject; import org.restcomm.connect.telephony.api.RemoveParticipant; import scala.concurrent.duration.Duration; import javax.sdp.SdpException; import javax.servlet.sip.Address; import javax.servlet.sip.AuthInfo; import javax.servlet.sip.ServletParseException; import javax.servlet.sip.SipApplicationSession; import javax.servlet.sip.SipFactory; import javax.servlet.sip.SipServletMessage; import javax.servlet.sip.SipServletRequest; import javax.servlet.sip.SipServletResponse; import javax.servlet.sip.SipSession; import javax.servlet.sip.SipURI; import javax.servlet.sip.TelURL; import javax.sip.header.RecordRouteHeader; import javax.sip.header.RouteHeader; import javax.sip.message.Response; import java.io.IOException; import java.math.BigDecimal; import java.net.InetAddress; import java.net.URI; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collections; import java.util.Currency; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Set; import java.util.StringTokenizer; import java.util.UUID; import java.util.concurrent.TimeUnit; /** * @author quintana.thomas@gmail.com (Thomas Quintana) * @author jean.deruelle@telestax.com (Jean Deruelle) * @author amit.bhayani@telestax.com (Amit Bhayani) * @author gvagenas@telestax.com (George Vagenas) * @author henrique.rosa@telestax.com (Henrique Rosa) * */ @Immutable public final class Call extends UntypedActor { // Logging private final LoggingAdapter logger = Logging.getLogger(getContext().system(), this); // Response Code for Media Server Failure private static final int MEDIA_SERVER_FAILURE_RESPONSE_CODE = 569; // Define possible directions. private static final String INBOUND = "inbound"; private static final String OUTBOUND_API = "outbound-api"; private static final String OUTBOUND_DIAL = "outbound-dial"; // Call Hold actions private static final String CALL_ON_HOLD_ACTION = "action=onHold"; private static final String CALL_OFF_HOLD_ACTION = "action=offHold"; // Finite State Machine private final FiniteStateMachine fsm; private final State uninitialized; private final State initializing; private final State waitingForAnswer; private final State queued; private final State failingBusy; private final State ringing; private final State busy; private final State notFound; private final State canceling; private final State canceled; private final State failingNoAnswer; private final State noAnswer; private final State dialing; private final State updatingMediaSession; private final State inProgress; private final State joining; private final State leaving; private final State stopping; private final State completed; private final State failed; private final State inDialogRequest; private boolean fail; // SIP runtime stuff private final SipFactory factory; private String apiVersion; private Sid accountId; private String name; private SipURI from; private javax.servlet.sip.URI to; // custom headers for SIP Out https://bitbucket.org/telestax/telscale-restcomm/issue/132/implement-twilio-sip-out //headers defined in rcml private Map<String, String> rcmlHeaders; //headers populated by extension to modify existing headers and add new headers private Map<String, ArrayList<String>> extensionHeaders; private String username; private String password; private CreateCallType type; private long timeout; private SipServletRequest invite; private SipServletRequest inDialogInvite; private SipServletResponse lastResponse; private boolean isFromApi; // Call runtime stuff. private final Sid id; private final String instanceId; private CallStateChanged.State external; private String direction; private String forwardedFrom; private DateTime created; private DateTime callUpdatedTime; private final List<ActorRef> observers; private boolean receivedBye; private boolean sentBye; private boolean muted; private boolean webrtc; private boolean initialInviteOkSent; // Conferencing private ActorRef conference; private boolean conferencing; private Sid conferenceSid; // Call Bridging private ActorRef bridge; // Media Session Control runtime stuff private final ActorRef msController; private MediaSessionInfo mediaSessionInfo; // Media Group runtime stuff private CallDetailRecord outgoingCallRecord; private CallDetailRecordsDao recordsDao; private DaoManager daoManager; private boolean liveCallModification; private boolean recording; private URI recordingUri; private Sid recordingSid; private Sid parentCallSid; // Runtime Setting private Configuration runtimeSettings; private Configuration configuration; private boolean disableSdpPatchingOnUpdatingMediaSession; private Sid inboundCallSid; private boolean inboundConfirmCall; private int collectTimeout; private String collectFinishKey; private boolean collectSipInfoDtmf = false; private boolean enable200OkDelay; private boolean outboundToIms; private String imsProxyAddress; private int imsProxyPort; private boolean actAsImsUa; private boolean isOnHold; private int callDuration; private DateTime recordingStart; private long recordingDuration; private HttpRequestDescriptor requestCallback; ActorRef downloader = null; ActorSystem system = null; private URI statusCallback; private String statusCallbackMethod; private List<String> statusCallbackEvent; public static enum CallbackState { INITIATED("initiated"), RINGING("ringing"), ANSWERED("answered"), COMPLETED("completed"); private final String text; private CallbackState(final String text) { this.text = text; } @Override public String toString() { return text; } }; public Call(final SipFactory factory, final ActorRef mediaSessionController, final Configuration configuration, final URI statusCallback, final String statusCallbackMethod, final List<String> statusCallbackEvent) { this(factory, mediaSessionController, configuration, statusCallback, statusCallbackMethod, statusCallbackEvent, null); } public Call(final SipFactory factory, final ActorRef mediaSessionController, final Configuration configuration, final URI statusCallback, final String statusCallbackMethod, final List<String> statusCallbackEvent, Map<String, ArrayList<String>> headers) { super(); final ActorRef source = self(); this.system = context().system(); this.statusCallback = statusCallback; this.statusCallbackMethod = statusCallbackMethod; this.statusCallbackEvent = statusCallbackEvent; if (statusCallback != null) { downloader = downloader(); } this.extensionHeaders = new HashMap<String, ArrayList<String>>(); if (headers != null) { this.extensionHeaders = headers; } // States for the FSM this.uninitialized = new State("uninitialized", null, null); this.initializing = new State("initializing", new Initializing(source), null); this.waitingForAnswer = new State("waiting for answer", new WaitingForAnswer(source), null); this.queued = new State("queued", new Queued(source), null); this.ringing = new State("ringing", new Ringing(source), null); this.failingBusy = new State("failing busy", new FailingBusy(source), null); this.busy = new State("busy", new Busy(source), null); this.notFound = new State("not found", new NotFound(source), null); //This time the --new Canceling(source)-- is an ActionOnState. Overloaded constructor is used here this.canceling = new State("canceling", new Canceling(source)); this.canceled = new State("canceled", new Canceled(source), null); this.failingNoAnswer = new State("failing no answer", new FailingNoAnswer(source), null); this.noAnswer = new State("no answer", new NoAnswer(source), null); this.dialing = new State("dialing", new Dialing(source), null); this.updatingMediaSession = new State("updating media session", new UpdatingMediaSession(source), null); this.inProgress = new State("in progress", new InProgress(source), null); this.joining = new State("joining", new Joining(source), null); this.leaving = new State("leaving", new Leaving(source), null); this.stopping = new State("stopping", new Stopping(source), null); this.completed = new State("completed", new Completed(source), null); this.failed = new State("failed", new Failed(source), null); this.inDialogRequest = new State("InDialogRequest", new InDialogRequest(source), null); // Transitions for the FSM final Set<Transition> transitions = new HashSet<Transition>(); transitions.add(new Transition(this.uninitialized, this.ringing)); transitions.add(new Transition(this.uninitialized, this.queued)); transitions.add(new Transition(this.uninitialized, this.canceled)); transitions.add(new Transition(this.uninitialized, this.completed)); transitions.add(new Transition(this.queued, this.canceled)); transitions.add(new Transition(this.queued, this.initializing)); transitions.add(new Transition(this.ringing, this.busy)); transitions.add(new Transition(this.ringing, this.notFound)); transitions.add(new Transition(this.ringing, this.canceling)); transitions.add(new Transition(this.ringing, this.canceled)); transitions.add(new Transition(this.ringing, this.failingNoAnswer)); transitions.add(new Transition(this.ringing, this.failingBusy)); transitions.add(new Transition(this.ringing, this.noAnswer)); transitions.add(new Transition(this.ringing, this.initializing)); transitions.add(new Transition(this.ringing, this.updatingMediaSession)); transitions.add(new Transition(this.ringing, this.completed)); transitions.add(new Transition(this.ringing, this.stopping)); transitions.add(new Transition(this.ringing, this.failed)); transitions.add(new Transition(this.initializing, this.canceling)); transitions.add(new Transition(this.initializing, this.dialing)); transitions.add(new Transition(this.initializing, this.failed)); transitions.add(new Transition(this.initializing, this.inProgress)); transitions.add(new Transition(this.initializing, this.waitingForAnswer)); transitions.add(new Transition(this.initializing, this.stopping)); transitions.add(new Transition(this.waitingForAnswer, this.inProgress)); transitions.add(new Transition(this.waitingForAnswer, this.joining)); transitions.add(new Transition(this.waitingForAnswer, this.canceling)); transitions.add(new Transition(this.waitingForAnswer, this.completed)); transitions.add(new Transition(this.waitingForAnswer, this.stopping)); transitions.add(new Transition(this.dialing, this.canceling)); transitions.add(new Transition(this.dialing, this.stopping)); transitions.add(new Transition(this.dialing, this.failingBusy)); transitions.add(new Transition(this.dialing, this.ringing)); transitions.add(new Transition(this.dialing, this.failed)); transitions.add(new Transition(this.dialing, this.failingNoAnswer)); transitions.add(new Transition(this.dialing, this.noAnswer)); transitions.add(new Transition(this.dialing, this.updatingMediaSession)); transitions.add(new Transition(this.inProgress, this.stopping)); transitions.add(new Transition(this.inProgress, this.joining)); transitions.add(new Transition(this.inProgress, this.leaving)); transitions.add(new Transition(this.inProgress, this.failed)); transitions.add(new Transition(this.inProgress, this.inDialogRequest)); transitions.add(new Transition(this.joining, this.inProgress)); transitions.add(new Transition(this.joining, this.stopping)); transitions.add(new Transition(this.joining, this.failed)); transitions.add(new Transition(this.leaving, this.inProgress)); transitions.add(new Transition(this.leaving, this.stopping)); transitions.add(new Transition(this.leaving, this.failed)); transitions.add(new Transition(this.leaving, this.completed)); transitions.add(new Transition(this.canceling, this.canceled)); transitions.add(new Transition(this.canceling, this.completed)); transitions.add(new Transition(this.failingBusy, this.busy)); transitions.add(new Transition(this.failingNoAnswer, this.noAnswer)); transitions.add(new Transition(this.failingNoAnswer, this.canceling)); transitions.add(new Transition(this.updatingMediaSession, this.inProgress)); transitions.add(new Transition(this.updatingMediaSession, this.failed)); transitions.add(new Transition(this.stopping, this.completed)); transitions.add(new Transition(this.stopping, this.failed)); transitions.add(new Transition(this.failed, this.completed)); transitions.add(new Transition(this.completed, this.stopping)); transitions.add(new Transition(this.completed, this.failed)); // FSM this.fsm = new FiniteStateMachine(this.uninitialized, transitions); // SIP runtime stuff. this.factory = factory; // Conferencing this.conferencing = false; // Media Session Control runtime stuff. this.msController = mediaSessionController; this.fail = false; // Initialize the runtime stuff. this.id = Sid.generate(Sid.Type.CALL); this.instanceId = RestcommConfiguration.getInstance().getMain().getInstanceId(); this.created = DateTime.now(); this.observers = Collections.synchronizedList(new ArrayList<ActorRef>()); this.receivedBye = false; // Media Group runtime stuff this.liveCallModification = false; this.recording = false; this.configuration = configuration; final Configuration runtime = this.configuration.subset("runtime-settings"); this.disableSdpPatchingOnUpdatingMediaSession = runtime .getBoolean("disable-sdp-patching-on-updating-mediasession", false); this.enable200OkDelay = runtime.getBoolean("enable-200-ok-delay", false); if (!runtime.subset("ims-authentication").isEmpty()) { final Configuration imsAuthentication = runtime.subset("ims-authentication"); this.actAsImsUa = imsAuthentication.getBoolean("act-as-ims-ua"); } } ActorRef downloader() { final Props props = new Props(new UntypedActorFactory() { private static final long serialVersionUID = 1L; @Override public UntypedActor create() throws Exception { return new Downloader(); } }); return system.actorOf(props); } private boolean is(State state) { return this.fsm.state().equals(state); } private boolean isInbound() { return INBOUND.equals(this.direction); } private boolean isOutbound() { return !isInbound(); } private CallResponse<CallInfo> info() { try { final String from = this.from.getUser(); String to = null; if (this.to.isSipURI()) { to = ((SipURI) this.to).getUser(); } else { to = ((TelURL) this.to).getPhoneNumber(); } final CallInfo info = new CallInfo(id, external, type, direction, created, forwardedFrom, name, from, to, invite, lastResponse, webrtc, muted, isFromApi, callUpdatedTime); return new CallResponse<CallInfo>(info); } catch (Exception e) { if (logger.isInfoEnabled()) { logger.info("Problem during preparing call info, exception {}", e); } } return null; } private List<NameValuePair> dialStatusCallbackParameters(final CallbackState state) { final List<NameValuePair> parameters = new ArrayList<NameValuePair>(); parameters.add(new BasicNameValuePair("InstanceId", RestcommConfiguration.getInstance().getMain().getInstanceId())); parameters.add(new BasicNameValuePair("AccountSid", accountId.toString())); parameters.add(new BasicNameValuePair("CallSid", id.toString())); parameters.add(new BasicNameValuePair("From", this.from.getUser())); String to = null; if (this.to.isSipURI()) { to = ((SipURI) this.to).getUser(); } else { to = ((TelURL) this.to).getPhoneNumber(); } parameters.add(new BasicNameValuePair("To", to)); parameters.add(new BasicNameValuePair("Direction", direction)); parameters.add(new BasicNameValuePair("CallerName", from.getUser())); parameters.add(new BasicNameValuePair("ForwardedFrom", forwardedFrom)); if (parentCallSid != null) parameters.add(new BasicNameValuePair("ParentCallSid", parentCallSid.toString())); parameters.add(new BasicNameValuePair("CallStatus", state.toString())); if (state.equals(CallbackState.COMPLETED)) { parameters.add(new BasicNameValuePair("CallDuration", String.valueOf(callDuration))); //We never record an outgoing call leg, we only record parent call leg and both legs of the call //are mixed down into a single channel //The recording duration will be used only for REST-API created calls if (recording && direction.equalsIgnoreCase("outbound-api")) { if (recordingUri != null) parameters.add(new BasicNameValuePair("RecordingUrl", recordingUri.toString())); if (recordingSid != null) parameters.add(new BasicNameValuePair("RecordingSid", recordingSid.toString())); if (recordingDuration > -1) parameters.add(new BasicNameValuePair("RecordingDuration", String.valueOf(recordingDuration))); } } //RFC 2822 (example: Mon, 15 Aug 2005 15:52:01 +0000) DateTimeFormatter fmt = DateTimeFormat.forPattern("EEE, dd MMM YYYY HH:mm:ss ZZZZ"); final String timestamp = DateTime.now().toString(fmt); parameters.add(new BasicNameValuePair("Timestamp", timestamp)); parameters.add(new BasicNameValuePair("CallbackSource", "call-progress-events")); String sequence = "0"; switch (state) { case INITIATED: sequence = "0"; break; case RINGING: sequence = "1"; break; case ANSWERED: sequence = "2"; break; case COMPLETED: sequence = "3"; break; default: sequence = "0"; break; } parameters.add(new BasicNameValuePair("SequenceNumber", sequence)); if (logger.isDebugEnabled()) { String msg = String.format( "Created parameters for Call StatusCallback for state %s and sequence %s uri %s", state, sequence, statusCallback.toString()); logger.debug(msg); } return parameters; } private void executeStatusCallback(final CallbackState state) { if (statusCallback != null) { if (statusCallbackEvent.contains(state.toString())) { if (logger.isDebugEnabled()) { String msg = String.format( "About to execute Call StatusCallback for state %s to StatusCallback %s. Call from %s to %s direction %s", state.text, statusCallback.toString(), from.toString(), to.toString(), direction); logger.debug(msg); } if (statusCallbackMethod == null) { statusCallbackMethod = "POST"; } final List<NameValuePair> parameters = dialStatusCallbackParameters(state); if (parameters != null) { requestCallback = new HttpRequestDescriptor(statusCallback, statusCallbackMethod, parameters); downloader.tell(requestCallback, null); } } else { if (logger.isDebugEnabled()) { String msg = String.format( "Call StatusCallback did not run because state %s no in the statusCallbackEvent list", state.text); logger.debug(msg); } } } else if (logger.isInfoEnabled()) { logger.info("status callback is null"); } } private void forwarding(final Object message) { // XXX does nothing } private SipURI getInitialIpAddressPort(SipServletMessage message) throws ServletParseException, UnknownHostException { // Issue #268 - https://bitbucket.org/telestax/telscale-restcomm/issue/268 // First get the Initial Remote Address (real address that the request came from) // Then check the following: // 1. If contact header address is private network address // 2. If there are no "Record-Route" headers (there is no proxy in the call) // 3. If contact header address != real ip address // Finally, if all of the above are true, create a SIP URI using the realIP address and the SIP port // and store it to the sip session to be used as request uri later SipURI uri = null; try { String realIP = message.getInitialRemoteAddr(); Integer realPort = message.getInitialRemotePort(); if (realPort == null || realPort == -1) { realPort = 5060; } if (realPort == 0) { realPort = message.getRemotePort(); } final ListIterator<String> recordRouteHeaders = message.getHeaders("Record-Route"); final Address contactAddr = factory.createAddress(message.getHeader("Contact")); InetAddress contactInetAddress = InetAddress.getByName(((SipURI) contactAddr.getURI()).getHost()); InetAddress inetAddress = InetAddress.getByName(realIP); int remotePort = message.getRemotePort(); int contactPort = ((SipURI) contactAddr.getURI()).getPort(); String remoteAddress = message.getRemoteAddr(); // Issue #332: https://telestax.atlassian.net/browse/RESTCOMM-332 final String initialIpBeforeLB = message.getHeader("X-Sip-Balancer-InitialRemoteAddr"); String initialPortBeforeLB = message.getHeader("X-Sip-Balancer-InitialRemotePort"); String contactAddress = ((SipURI) contactAddr.getURI()).getHost(); if (initialIpBeforeLB != null) { if (initialPortBeforeLB == null) initialPortBeforeLB = "5060"; if (logger.isInfoEnabled()) { logger.info("We are behind load balancer, storing Initial Remote Address " + initialIpBeforeLB + ":" + initialPortBeforeLB + " to the session for later use"); } realIP = initialIpBeforeLB + ":" + initialPortBeforeLB; uri = factory.createSipURI(null, realIP); } else if (contactInetAddress.isSiteLocalAddress() && !recordRouteHeaders.hasNext() && !contactInetAddress.toString().equalsIgnoreCase(inetAddress.toString())) { if (logger.isInfoEnabled()) { logger.info("Contact header address " + contactAddr.toString() + " is a private network ip address, storing Initial Remote Address " + realIP + ":" + realPort + " to the session for later use"); } realIP = realIP + ":" + realPort; uri = factory.createSipURI(null, realIP); } } catch (Exception e) { logger.warning("Exception while trying to get the Initial IP Address and Port: " + e); } return uri; } @Override public void onReceive(Object message) throws Exception { final Class<?> klass = message.getClass(); final ActorRef self = self(); final ActorRef sender = sender(); final State state = fsm.state(); if (logger.isInfoEnabled()) { logger.info("********** Call's " + self().path() + " Current State: \"" + state.toString() + " direction: " + direction); logger.info("********** Call " + self().path() + " Processing Message: \"" + klass.getName() + " sender : " + sender.path().toString()); } if (Observe.class.equals(klass)) { onObserve((Observe) message, self, sender); } else if (StopObserving.class.equals(klass)) { onStopObserving((StopObserving) message, self, sender); } else if (GetCallObservers.class.equals(klass)) { onGetCallObservers((GetCallObservers) message, self, sender); } else if (GetCallInfo.class.equals(klass)) { onGetCallInfo((GetCallInfo) message, sender); } else if (InitializeOutbound.class.equals(klass)) { onInitializeOutbound((InitializeOutbound) message, self, sender); } else if (ChangeCallDirection.class.equals(klass)) { onChangeCallDirection((ChangeCallDirection) message, self, sender); } else if (Answer.class.equals(klass)) { onAnswer((Answer) message, self, sender); } else if (Dial.class.equals(klass)) { onDial((Dial) message, self, sender); } else if (Reject.class.equals(klass)) { onReject((Reject) message, self, sender); } else if (CallFail.class.equals(klass)) { fsm.transition(message, failed); } else if (JoinComplete.class.equals(klass)) { onJoinComplete((JoinComplete) message, self, sender); } else if (StartRecording.class.equals(klass)) { onStartRecordingCall((StartRecording) message, self, sender); } else if (StopRecording.class.equals(klass)) { onStopRecordingCall((StopRecording) message, self, sender); } else if (Cancel.class.equals(klass)) { onCancel((Cancel) message, self, sender); } else if (message instanceof ReceiveTimeout) { onReceiveTimeout((ReceiveTimeout) message, self, sender); } else if (message instanceof SipServletRequest) { onSipServletRequest((SipServletRequest) message, self, sender); } else if (message instanceof SipServletResponse) { onSipServletResponse((SipServletResponse) message, self, sender); } else if (Hangup.class.equals(klass)) { onHangup((Hangup) message, self, sender); } else if (org.restcomm.connect.telephony.api.NotFound.class.equals(klass)) { onNotFound((org.restcomm.connect.telephony.api.NotFound) message, self, sender); } else if (MediaServerControllerStateChanged.class.equals(klass)) { onMediaServerControllerStateChanged((MediaServerControllerStateChanged) message, self, sender); } else if (JoinConference.class.equals(klass)) { onJoinConference((JoinConference) message, self, sender); } else if (JoinBridge.class.equals(klass)) { onJoinBridge((JoinBridge) message, self, sender); } else if (Leave.class.equals(klass)) { onLeave((Leave) message, self, sender); } else if (Left.class.equals(klass)) { onLeft((Left) message, self, sender); } else if (Record.class.equals(klass)) { onRecord((Record) message, self, sender); } else if (Play.class.equals(klass)) { onPlay((Play) message, self, sender); } else if (Collect.class.equals(klass)) { onCollect((Collect) message, self, sender); } else if (StopMediaGroup.class.equals(klass)) { onStopMediaGroup((StopMediaGroup) message, self, sender); } else if (Mute.class.equals(klass)) { onMute((Mute) message, self, sender); } else if (Unmute.class.equals(klass)) { onUnmute((Unmute) message, self, sender); } else if (ConferenceResponse.class.equals(klass)) { onConferenceResponse((ConferenceResponse) message); } else if (BridgeStateChanged.class.equals(klass)) { onBridgeStateChanged((BridgeStateChanged) message, self, sender); } else if (CallHoldStateChange.class.equals(klass)) { onCallHoldStateChange((CallHoldStateChange) message, sender); } } private void onConferenceResponse(ConferenceResponse conferenceResponse) { //ConferenceResponse received ConferenceInfo ci = (ConferenceInfo) conferenceResponse.get(); if (logger.isInfoEnabled()) { String infoMsg = String.format("Conference response, name %s, state %s, participants %d", ci.name(), ci.state(), ci.globalParticipants()); logger.info(infoMsg); } } private void onCallHoldStateChange(CallHoldStateChange message, ActorRef sender) throws IOException { if (logger.isInfoEnabled()) { logger.info("CallHoldStateChange received, state: " + message.state() + " isOnHold " + isOnHold); } if (is(inProgress)) { if (!isOnHold && CallHoldStateChange.State.ONHOLD.equals(message.state())) { final SipServletRequest messageRequest = invite.getSession().createRequest("MESSAGE"); messageRequest.setContent(CALL_ON_HOLD_ACTION, "text/plain"); messageRequest.send(); isOnHold = true; } else if (isOnHold && CallHoldStateChange.State.OFFHOLD.equals(message.state())) { final SipServletRequest messageRequest = invite.getSession().createRequest("MESSAGE"); messageRequest.setContent(CALL_OFF_HOLD_ACTION, "text/plain"); messageRequest.send(); isOnHold = false; } } } private void addCustomHeaders(SipServletMessage message) { if (apiVersion != null) message.addHeader("X-RestComm-ApiVersion", apiVersion); if (accountId != null) message.addHeader("X-RestComm-AccountSid", accountId.toString()); message.addHeader("X-RestComm-CallSid", instanceId + "-" + id.toString()); } // Allow updating of the callInfo at the VoiceInterpreter so that we can do Dial SIP Screening // (https://bitbucket.org/telestax/telscale-restcomm/issue/132/implement-twilio-sip-out) accurately from latest response // received private void sendCallInfoToObservers() { for (final ActorRef observer : this.observers) { observer.tell(info(), self()); } } private void processInfo(final SipServletRequest request) throws IOException { //Seems we will receive DTMF over SIP INFO, we should start timeout timer //to simulate the collect timeout when using the RMS collectSipInfoDtmf = true; context().setReceiveTimeout(Duration.create(collectTimeout, TimeUnit.SECONDS)); final SipServletResponse okay = request.createResponse(SipServletResponse.SC_OK); addCustomHeaders(okay); okay.send(); String digits = null; if (request.getContentType().equalsIgnoreCase("application/dtmf-relay")) { final String content = new String(request.getRawContent()); digits = content.split("\n")[0].replaceFirst("Signal=", "").trim(); } else { digits = new String(request.getRawContent()); } if (digits != null) { MediaGroupResponse<String> infoResponse = new MediaGroupResponse<String>(digits); for (final ActorRef observer : observers) { observer.tell(infoResponse, self()); } this.msController.tell(new Stop(), self()); } } /* * ACTIONS */ private abstract class AbstractAction implements Action { protected final ActorRef source; public AbstractAction(final ActorRef source) { super(); this.source = source; } } private final class Queued extends AbstractAction { public Queued(final ActorRef source) { super(source); } @Override public void execute(Object message) throws Exception { final InitializeOutbound request = (InitializeOutbound) message; name = request.name(); from = request.from(); to = request.to(); apiVersion = request.apiVersion(); accountId = request.accountId(); username = request.username(); password = request.password(); type = request.type(); parentCallSid = request.getParentCallSid(); recordsDao = request.getDaoManager().getCallDetailRecordsDao(); isFromApi = request.isFromApi(); outboundToIms = request.isOutboundToIms(); imsProxyAddress = request.getImsProxyAddress(); imsProxyPort = request.getImsProxyPort(); String toHeaderString = to.toString(); rcmlHeaders = new HashMap<String, String>(); if (toHeaderString.indexOf('?') != -1) { // custom headers parsing for SIP Out // https://bitbucket.org/telestax/telscale-restcomm/issue/132/implement-twilio-sip-out // we keep only the to URI without the headers to = (SipURI) factory.createURI(toHeaderString.substring(0, toHeaderString.lastIndexOf('?'))); String headersString = toHeaderString.substring(toHeaderString.lastIndexOf('?') + 1); StringTokenizer tokenizer = new StringTokenizer(headersString, "&"); while (tokenizer.hasMoreTokens()) { String headerNameValue = tokenizer.nextToken(); String headerName = headerNameValue.substring(0, headerNameValue.lastIndexOf('=')); String headerValue = headerNameValue.substring(headerNameValue.lastIndexOf('=') + 1); rcmlHeaders.put(headerName, headerValue); } } timeout = request.timeout(); direction = request.isFromApi() ? OUTBOUND_API : OUTBOUND_DIAL; webrtc = request.isWebrtc(); // Notify the observers. external = CallStateChanged.State.QUEUED; final CallStateChanged event = new CallStateChanged(external); for (final ActorRef observer : observers) { observer.tell(event, source); } if (recordsDao != null) { CallDetailRecord cdr = recordsDao.getCallDetailRecord(id); if (cdr == null) { final CallDetailRecord.Builder builder = CallDetailRecord.builder(); builder.setSid(id); builder.setInstanceId(RestcommConfiguration.getInstance().getMain().getInstanceId()); builder.setDateCreated(created); builder.setAccountSid(accountId); String toUser = null; if (to.isSipURI()) { toUser = ((SipURI) to).getUser(); } else { toUser = ((TelURL) to).getPhoneNumber(); } builder.setTo(toUser); builder.setCallerName(name); builder.setStartTime(new DateTime()); String fromString = (from.getUser() != null ? from.getUser() : "CALLS REST API"); builder.setFrom(fromString); // builder.setForwardedFrom(callInfo.forwardedFrom()); // builder.setPhoneNumberSid(phoneId); builder.setStatus(external.name()); builder.setDirection("outbound-api"); builder.setApiVersion(apiVersion); builder.setPrice(new BigDecimal("0.00")); // TODO implement currency property to be read from Configuration builder.setPriceUnit(Currency.getInstance("USD")); final StringBuilder buffer = new StringBuilder(); buffer.append("/").append(apiVersion).append("/Accounts/"); buffer.append(accountId.toString()).append("/Calls/"); buffer.append(id.toString()); final URI uri = URI.create(buffer.toString()); builder.setUri(uri); builder.setCallPath(self().path().toString()); builder.setParentCallSid(parentCallSid); outgoingCallRecord = builder.build(); recordsDao.addCallDetailRecord(outgoingCallRecord); } else { cdr.setStatus(external.name()); } } } } private final class Dialing extends AbstractAction { public Dialing(final ActorRef source) { super(source); } @Override public void execute(Object message) throws Exception { final MediaServerControllerStateChanged response = (MediaServerControllerStateChanged) message; final ActorRef self = self(); mediaSessionInfo = response.getMediaSession(); // Create a SIP invite to initiate a new session. final SipURI uri; if (!outboundToIms) { final StringBuilder buffer = new StringBuilder(); buffer.append(((SipURI) to).getHost()); if (((SipURI) to).getPort() > -1) { buffer.append(":").append(((SipURI) to).getPort()); } String transport = ((SipURI) to).getTransportParam(); if (transport != null) { buffer.append(";transport=").append(((SipURI) to).getTransportParam()); } uri = factory.createSipURI(null, buffer.toString()); } else { uri = factory.createSipURI(null, imsProxyAddress); uri.setPort(imsProxyPort); uri.setLrParam(true); } final SipApplicationSession application = factory.createApplicationSession(); application.setAttribute(Call.class.getName(), self); String callId = null; String userAgent = null; if (outboundToIms && !configuration.subset("runtime-settings").subset("ims-authentication").isEmpty()) { final Configuration imsAuthentication = configuration.subset("runtime-settings") .subset("ims-authentication"); final String callIdPrefix = imsAuthentication.getString("call-id-prefix"); userAgent = imsAuthentication.getString("user-agent"); callId = callIdPrefix + UUID.randomUUID().toString(); } if (name != null && !name.isEmpty()) { // Create the from address using the inital user displayed name // Example: From: "Alice" <sip:userpart@host:port> final Address fromAddress = factory.createAddress(from, name); final Address toAddress = factory.createAddress(to); invite = ((SipFactoryExt) factory).createRequestWithCallID(application, "INVITE", fromAddress, toAddress, callId); } else { invite = ((SipFactoryExt) factory).createRequestWithCallID(application, "INVITE", from, to, callId); } invite.pushRoute(uri); if (userAgent != null) { invite.setHeader("User-Agent", userAgent); } addCustomHeadersToMap(rcmlHeaders); // adding custom headers for SIP Out // https://bitbucket.org/telestax/telscale-restcomm/issue/132/implement-twilio-sip-out addHeadersToMessage(invite, rcmlHeaders, "X-"); //the extension headers will override any headers addHeadersToMessage(invite, extensionHeaders); final SipSession session = invite.getSession(); session.setHandler("CallManager"); // Issue: https://telestax.atlassian.net/browse/RESTCOMM-608 // If this is a call to Restcomm client or SIP URI bypass LB if (logger.isInfoEnabled()) logger.info("bypassLoadBalancer is set to: " + RestcommConfiguration.getInstance().getMain().getBypassLbForClients()); if (RestcommConfiguration.getInstance().getMain().getBypassLbForClients()) { if (type.equals(CreateCallType.CLIENT) || type.equals(CreateCallType.SIP)) { ((SipSessionExt) session).setBypassLoadBalancer(true); ((SipSessionExt) session).setBypassProxy(true); } } String offer = null; if (mediaSessionInfo.usesNat()) { final String externalIp = mediaSessionInfo.getExternalAddress().getHostAddress(); final byte[] sdp = mediaSessionInfo.getLocalSdp().getBytes(); offer = SdpUtils.patch("application/sdp", sdp, externalIp); } else { offer = mediaSessionInfo.getLocalSdp(); } offer = SdpUtils.endWithNewLine(offer); invite.setContent(offer, "application/sdp"); // Send the invite. invite.send(); // Set the timeout period. final UntypedActorContext context = getContext(); context.setReceiveTimeout(Duration.create(timeout, TimeUnit.SECONDS)); executeStatusCallback(CallbackState.INITIATED); } /** * addCustomHeadersToMap */ private void addCustomHeadersToMap(Map<String, String> headers) { if (apiVersion != null) headers.put("RestComm-ApiVersion", apiVersion); if (accountId != null) headers.put("RestComm-AccountSid", accountId.toString()); headers.put("RestComm-CallSid", instanceId + "-" + id.toString()); } //TODO: put this in a central place private void addHeadersToMessage(SipServletRequest message, Map<String, String> headers, String keyPrepend) { try { for (Map.Entry<String, String> entry : headers.entrySet()) { String headerName = keyPrepend + entry.getKey(); message.addHeader(headerName, entry.getValue()); } } catch (IllegalArgumentException iae) { if (logger.isErrorEnabled()) { logger.error("Exception while setting message header: " + iae.getMessage()); } } } /** * Replace headers * @param SipServletRequest message * @param Map<String, ArrayList<String> > headers */ private void addHeadersToMessage(SipServletRequest message, Map<String, ArrayList<String>> headers) { if (headers != null) { for (Map.Entry<String, ArrayList<String>> entry : headers.entrySet()) { //check if header exists String headerName = entry.getKey(); StringBuilder sb = new StringBuilder(); if (entry.getValue() instanceof ArrayList) { for (String pair : entry.getValue()) { sb.append(";").append(pair); } } if (logger.isDebugEnabled()) { logger.debug("headerName=" + headerName + " headerVal=" + message.getHeader(headerName) + " concatValue=" + sb.toString()); } if (!headerName.equalsIgnoreCase("Request-URI")) { try { String headerVal = message.getHeader(headerName); if (headerVal != null && !headerVal.isEmpty()) { message.setHeader(headerName, headerVal + sb.toString()); } else { message.addHeader(headerName, sb.toString()); } } catch (IllegalArgumentException iae) { if (logger.isErrorEnabled()) { logger.error("Exception while setting message header: " + iae.getMessage()); } } } else { //handle Request-URI javax.servlet.sip.URI reqURI = message.getRequestURI(); if (logger.isDebugEnabled()) { logger.debug("ReqURI=" + reqURI.toString() + " msgReqURI=" + message.getRequestURI()); } for (String keyValPair : entry.getValue()) { String parName = ""; String parVal = ""; int equalsPos = keyValPair.indexOf("="); parName = keyValPair.substring(0, equalsPos); parVal = keyValPair.substring(equalsPos + 1); reqURI.setParameter(parName, parVal); if (logger.isDebugEnabled()) { logger.debug("ReqURI pars =" + parName + "=" + parVal + " equalsPos=" + equalsPos + " keyValPair=" + keyValPair); } } message.setRequestURI(reqURI); if (logger.isDebugEnabled()) { logger.debug("ReqURI=" + reqURI.toString() + " msgReqURI=" + message.getRequestURI()); } } if (logger.isDebugEnabled()) { logger.debug("headerName=" + headerName + " headerVal=" + message.getHeader(headerName)); } } } } } private final class Ringing extends AbstractAction { public Ringing(final ActorRef source) { super(source); } @Override public void execute(final Object message) throws Exception { if (message instanceof SipServletRequest) { invite = (SipServletRequest) message; from = (SipURI) invite.getFrom().getURI(); to = invite.getTo().getURI(); timeout = -1; direction = INBOUND; try { // Send a ringing response final SipServletResponse ringing = invite.createResponse(SipServletResponse.SC_RINGING); addCustomHeaders(ringing); // ringing.addHeader("X-RestComm-CallSid", id.toString()); ringing.send(); } catch (IllegalStateException exception) { if (logger.isDebugEnabled()) { logger.debug("Exception while creating 180 response to inbound invite request"); } fsm.transition(message, canceled); } SipURI initialInetUri = getInitialIpAddressPort(invite); if (initialInetUri != null) { invite.getSession().setAttribute("realInetUri", initialInetUri); } } else if (message instanceof SipServletResponse) { // Timeout still valid in case we receive a 180, we don't know if the // call will be eventually answered. // Issue 585: https://telestax.atlassian.net/browse/RESTCOMM-585 // final UntypedActorContext context = getContext(); // context.setReceiveTimeout(Duration.Undefined()); SipURI initialInetUri = getInitialIpAddressPort((SipServletResponse) message); if (initialInetUri != null) { ((SipServletResponse) message).getSession().setAttribute("realInetUri", initialInetUri); } executeStatusCallback(CallbackState.RINGING); } // Notify the observers. external = CallStateChanged.State.RINGING; final CallStateChanged event = new CallStateChanged(external); for (final ActorRef observer : observers) { observer.tell(event, source); } if (outgoingCallRecord != null && isOutbound()) { outgoingCallRecord = outgoingCallRecord.setStatus(external.name()); recordsDao.updateCallDetailRecord(outgoingCallRecord); } } } private final class Canceling extends AbstractAction { public Canceling(final ActorRef source) { super(source); } @Override public void execute(final Object message) throws Exception { try { if (isOutbound() && (invite.getSession().getState() != SipSession.State.INITIAL || invite.getSession().getState() != SipSession.State.TERMINATED)) { final UntypedActorContext context = getContext(); context.setReceiveTimeout(Duration.Undefined()); final SipServletRequest cancel = invite.createCancel(); addCustomHeaders(cancel); cancel.send(); if (logger.isInfoEnabled()) { logger.info("Sent CANCEL for Call: " + self().path() + ", state: " + fsm.state() + ", direction: " + direction); } } } catch (Exception e) { StringBuffer strBuffer = new StringBuffer(); strBuffer.append( "Exception while trying to create Cancel for Call with the following details, from: " + from + " to: " + to + " direction: " + direction + " call state: " + fsm.state()); if (invite != null) { strBuffer.append(" , invite RURI: " + invite.getRequestURI()); } else { strBuffer.append(" , invite is NULL! "); } strBuffer.append(" Exception: " + e.getMessage()); logger.warning(strBuffer.toString()); } msController.tell(new CloseMediaSession(), source); } } private final class Canceled extends AbstractAction { public Canceled(ActorRef source) { super(source); } @Override public void execute(final Object message) throws Exception { //A no-answer call will be cancelled and will arrive here. In that case don't change the external case //since no-answer is a final state and we need to keep it so observer knows how the call ended // if (!external.equals(CallStateChanged.State.NO_ANSWER)) { external = CallStateChanged.State.CANCELED; // final CallStateChanged event = new CallStateChanged(external); // for (final ActorRef observer : observers) { // observer.tell(event, source); // } // } // Record call data if (outgoingCallRecord != null && isOutbound()) { if (logger.isInfoEnabled()) { logger.info("Going to update CDR to CANCEL, call sid: " + id + " from: " + from + " to: " + to + " direction: " + direction); } outgoingCallRecord = outgoingCallRecord.setStatus(external.name()); recordsDao.updateCallDetailRecord(outgoingCallRecord); } fsm.transition(message, completed); } } private abstract class Failing extends AbstractAction { public Failing(final ActorRef source) { super(source); } @Override public void execute(final Object message) throws Exception { if (message instanceof ReceiveTimeout) { final UntypedActorContext context = getContext(); context.setReceiveTimeout(Duration.Undefined()); } callUpdatedTime = DateTime.now(); msController.tell(new CloseMediaSession(), source); } } private final class FailingBusy extends Failing { public FailingBusy(final ActorRef source) { super(source); } } private final class FailingNoAnswer extends Failing { public FailingNoAnswer(final ActorRef source) { super(source); } @Override public void execute(Object message) throws Exception { if (logger.isInfoEnabled()) { logger.info("Call moves to failing state because no answer"); } fsm.transition(message, noAnswer); } } private final class Busy extends AbstractAction { public Busy(final ActorRef source) { super(source); } @Override public void execute(final Object message) throws Exception { final Class<?> klass = message.getClass(); // Send SIP BUSY to remote peer if (Reject.class.equals(klass) && is(ringing) && isInbound()) { Reject reject = (Reject) message; SipServletResponse rejectResponse; if (reject.getReason().equalsIgnoreCase("busy")) { rejectResponse = invite.createResponse(SipServletResponse.SC_BUSY_HERE); } else { rejectResponse = invite.createResponse(SipServletResponse.SC_DECLINE); } addCustomHeaders(rejectResponse); rejectResponse.send(); } // Explicitly invalidate the application session. // if (invite.getSession().isValid()) // invite.getSession().invalidate(); // if (invite.getApplicationSession().isValid()) // invite.getApplicationSession().invalidate(); // Notify the observers. external = CallStateChanged.State.BUSY; final CallStateChanged event = new CallStateChanged(external, lastResponse.getStatus()); for (final ActorRef observer : observers) { observer.tell(event, source); } // Record call data if (outgoingCallRecord != null && isOutbound()) { outgoingCallRecord = outgoingCallRecord.setStatus(external.name()); recordsDao.updateCallDetailRecord(outgoingCallRecord); outgoingCallRecord = outgoingCallRecord.setDuration(0); recordsDao.updateCallDetailRecord(outgoingCallRecord); final int seconds = (int) ((DateTime.now().getMillis() - outgoingCallRecord.getStartTime().getMillis()) / 1000); outgoingCallRecord = outgoingCallRecord.setRingDuration(seconds); recordsDao.updateCallDetailRecord(outgoingCallRecord); } } } private final class NotFound extends AbstractAction { public NotFound(final ActorRef source) { super(source); } @Override public void execute(final Object message) throws Exception { final Class<?> klass = message.getClass(); // Send SIP NOT_FOUND to remote peer if (org.restcomm.connect.telephony.api.NotFound.class.equals(klass) && isInbound()) { final SipServletResponse notFound = invite.createResponse(SipServletResponse.SC_NOT_FOUND); addCustomHeaders(notFound); notFound.send(); } // Notify the observers. external = CallStateChanged.State.NOT_FOUND; final CallStateChanged event = new CallStateChanged(external, SipServletResponse.SC_NOT_FOUND); for (final ActorRef observer : observers) { observer.tell(event, source); } // Record call data if (outgoingCallRecord != null && isOutbound()) { outgoingCallRecord = outgoingCallRecord.setStatus(external.name()); recordsDao.updateCallDetailRecord(outgoingCallRecord); } } } private final class NoAnswer extends AbstractAction { public NoAnswer(final ActorRef source) { super(source); } @Override public void execute(final Object message) throws Exception { // // Explicitly invalidate the application session. // if (invite.getSession().isValid()) // invite.getSession().invalidate(); // if (invite.getApplicationSession().isValid()) // invite.getApplicationSession().invalidate(); // Notify the observers. external = CallStateChanged.State.NO_ANSWER; final CallStateChanged event = new CallStateChanged(external, SipServletResponse.SC_REQUEST_TIMEOUT); for (final ActorRef observer : observers) { observer.tell(event, source); } // Record call data if (outgoingCallRecord != null && isOutbound()) { outgoingCallRecord = outgoingCallRecord.setStatus(external.name()); recordsDao.updateCallDetailRecord(outgoingCallRecord); } } } private final class Failed extends AbstractAction { public Failed(final ActorRef source) { super(source); } @Override public void execute(final Object message) throws Exception { if (isInbound()) { SipServletResponse resp = null; if (message instanceof CallFail) { resp = invite.createResponse(500, "Problem to setup the call"); String reason = ((CallFail) message).getReason(); if (reason != null) resp.addHeader("Reason", reason); } else { // https://github.com/RestComm/Restcomm-Connect/issues/1663 // We use 569 only if there is a problem to reach RMS as LB can be configured to take out // nodes that send back 569. This is meant to protect the cluster from nodes where the RMS // is in bad state and not responding anymore resp = invite.createResponse(MEDIA_SERVER_FAILURE_RESPONSE_CODE, "Problem to setup services"); } addCustomHeaders(resp); resp.send(); } else { if (message instanceof CallFail) sendBye(new Hangup(((CallFail) message).getReason())); } // Notify the observers. external = CallStateChanged.State.FAILED; CallStateChanged event = null; if (lastResponse != null) { event = new CallStateChanged(external, lastResponse.getStatus()); } else { event = new CallStateChanged(external); } for (final ActorRef observer : observers) { observer.tell(event, source); } // Record call data if (outgoingCallRecord != null && isOutbound()) { outgoingCallRecord = outgoingCallRecord.setStatus(external.name()); recordsDao.updateCallDetailRecord(outgoingCallRecord); } } } private final class Initializing extends AbstractAction { public Initializing(final ActorRef source) { super(source); } @Override public void execute(final Object message) throws Exception { // Start observing state changes in the MSController final Observe observe = new Observe(super.source); msController.tell(observe, super.source); // Initialize the MS Controller CreateMediaSession command = null; if (isOutbound()) { command = new CreateMediaSession("sendrecv", "", true, webrtc, id); } else { if (!liveCallModification) { command = generateRequest(invite); } else { if (lastResponse != null && lastResponse.getStatus() == 200) { command = generateRequest(lastResponse); } // TODO no else may lead to NullPointerException } } msController.tell(command, source); } } private final class InDialogRequest extends AbstractAction { public InDialogRequest(final ActorRef source) { super(source); } @Override public void execute(final Object message) throws Exception { SipServletRequest request = (SipServletRequest) message; if (logger.isDebugEnabled()) { logger.debug("IN-Dialog INVITE received: " + request.getRequestURI().toString()); } CreateMediaSession command = generateRequest(request); msController.tell(command, self()); } } private CreateMediaSession generateRequest(SipServletMessage sipMessage) throws IOException, SdpException, ServletParseException { final byte[] sdp = sipMessage.getRawContent(); String offer = SdpUtils.getSdp(sipMessage.getContentType(), sipMessage.getRawContent()); if (!disableSdpPatchingOnUpdatingMediaSession) { String externalIp = null; final SipURI externalSipUri = (SipURI) sipMessage.getSession().getAttribute("realInetUri"); if (externalSipUri != null) { if (logger.isInfoEnabled()) { logger.info("ExternalSipUri stored in the sip session : " + externalSipUri.toString() + " will use host: " + externalSipUri.getHost().toString()); } externalIp = externalSipUri.getHost().toString(); } else { externalIp = sipMessage.getInitialRemoteAddr(); if (logger.isInfoEnabled()) { logger.info( "ExternalSipUri stored in the session was null, will use the message InitialRemoteAddr: " + externalIp); } } offer = SdpUtils.patch(sipMessage.getContentType(), sdp, externalIp); } return new CreateMediaSession("sendrecv", offer, false, webrtc, inboundCallSid); } private final class UpdatingMediaSession extends AbstractAction { public UpdatingMediaSession(final ActorRef source) { super(source); } @Override public void execute(final Object message) throws Exception { if (is(dialing) || is(ringing)) { final UntypedActorContext context = getContext(); context.setReceiveTimeout(Duration.Undefined()); } final SipServletResponse response = (SipServletResponse) message; // Issue 99: https://bitbucket.org/telestax/telscale-restcomm/issue/99 if (response.getStatus() == SipServletResponse.SC_OK && isOutbound()) { String initialIpBeforeLB = null; String initialPortBeforeLB = null; try { initialIpBeforeLB = response.getHeader("X-Sip-Balancer-InitialRemoteAddr"); initialPortBeforeLB = response.getHeader("X-Sip-Balancer-InitialRemotePort"); } catch (Exception e) { if (logger.isDebugEnabled()) { logger.debug("Exception during check of LB custom headers for IP address and port"); } } final SipServletRequest ack = response.createAck(); addCustomHeaders(ack); SipSession session = response.getSession(); 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"); } String realIP = initialIpBeforeLB + ":" + initialPortBeforeLB; SipURI uri = factory.createSipURI(((SipURI) ack.getRequestURI()).getUser(), realIP); boolean patchRURI = true; 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 = ack.getAddressHeaders(RouteHeader.NAME); while (routes.hasNext() && patchRURI) { 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 ACK 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 ACK request, so not patching the Request-URI"); } patchRURI = false; } } } catch (ServletParseException e) { logger.error("Impossible to parse the route set from the ACK " + ack, e); } if (patchRURI) { if (logger.isDebugEnabled()) { logger.debug("We are behind load balancer, will use: " + initialIpBeforeLB + ":" + initialPortBeforeLB + " for ACK message, "); } ack.setRequestURI(uri); } } else if (!ack.getHeaders("Route").hasNext()) { final SipServletRequest originalInvite = response.getRequest(); final SipURI realInetUri = (SipURI) originalInvite.getRequestURI(); if ((SipURI) session.getAttribute("realInetUri") == null) { // session.setAttribute("realInetUri", factory.createSipURI(null, realInetUri.getHost()+":"+realInetUri.getPort())); session.setAttribute("realInetUri", realInetUri); } final InetAddress ackRURI = InetAddress.getByName(((SipURI) ack.getRequestURI()).getHost()); final int ackRURIPort = ((SipURI) ack.getRequestURI()).getPort(); if (realInetUri != null && (ackRURI.isSiteLocalAddress() || ackRURI.isAnyLocalAddress() || ackRURI.isLoopbackAddress()) && (ackRURIPort != realInetUri.getPort())) { if (logger.isInfoEnabled()) { logger.info("Using the real ip address and port of the sip client " + realInetUri.toString() + " as a request uri of the ACK"); } realInetUri.setUser(((SipURI) ack.getRequestURI()).getUser()); ack.setRequestURI(realInetUri); } } ack.send(); if (logger.isInfoEnabled()) { logger.info("Just sent out ACK : " + ack.toString()); } } //Set Call created time, only for "Talk time". callUpdatedTime = DateTime.now(); //Update CDR for Outbound Call. if (recordsDao != null) { if (outgoingCallRecord != null && isOutbound()) { final int seconds = (int) ((DateTime.now().getMillis() - outgoingCallRecord.getStartTime().getMillis()) / 1000); outgoingCallRecord = outgoingCallRecord.setRingDuration(seconds); recordsDao.updateCallDetailRecord(outgoingCallRecord); outgoingCallRecord = outgoingCallRecord.setStartTime(DateTime.now()); recordsDao.updateCallDetailRecord(outgoingCallRecord); outgoingCallRecord = outgoingCallRecord.setStatus(external.name()); recordsDao.updateCallDetailRecord(outgoingCallRecord); } } String answer = null; if (!disableSdpPatchingOnUpdatingMediaSession) { if (logger.isInfoEnabled()) { logger.info( "Will patch SDP answer from 200 OK received with the external IP Address from Response on updating media session"); } final String externalIp = response.getInitialRemoteAddr(); final byte[] sdp = response.getRawContent(); answer = SdpUtils.patch(response.getContentType(), sdp, externalIp); } else { if (logger.isInfoEnabled()) { logger.info("SDP Patching on updating media session is disabled"); } answer = SdpUtils.getSdp(response.getContentType(), response.getRawContent()); } final UpdateMediaSession update = new UpdateMediaSession(answer); msController.tell(update, source); } } private final class WaitingForAnswer extends AbstractAction { public WaitingForAnswer(final ActorRef source) { super(source); } @Override public void execute(final Object message) throws Exception { // Notify the observers. if (external != null && !external.equals(CallStateChanged.State.WAIT_FOR_ANSWER)) { external = CallStateChanged.State.WAIT_FOR_ANSWER; final CallStateChanged event = new CallStateChanged(external); for (final ActorRef observer : observers) { observer.tell(event, source); } } } } private final class InProgress extends AbstractAction { public InProgress(final ActorRef source) { super(source); } @Override public void execute(final Object message) throws Exception { // Notify the observers. if (external != null && !external.equals(CallStateChanged.State.IN_PROGRESS)) { external = CallStateChanged.State.IN_PROGRESS; final CallStateChanged event = new CallStateChanged(external); for (final ActorRef observer : observers) { observer.tell(event, source); } // Record call data if (outgoingCallRecord != null && isOutbound() && !outgoingCallRecord.getStatus().equalsIgnoreCase("in_progress")) { outgoingCallRecord = outgoingCallRecord.setStatus(external.toString()); String toUser = null; if (to.isSipURI()) { toUser = ((SipURI) to).getUser(); } else { toUser = ((TelURL) to).getPhoneNumber(); } outgoingCallRecord = outgoingCallRecord.setAnsweredBy(toUser); if (conferencing) { outgoingCallRecord = outgoingCallRecord.setConferenceSid(conferenceSid); outgoingCallRecord = outgoingCallRecord.setMuted(muted); } recordsDao.updateCallDetailRecord(outgoingCallRecord); } if (isOutbound()) { executeStatusCallback(CallbackState.ANSWERED); } } } } private final class Joining extends AbstractAction { public Joining(ActorRef source) { super(source); } @Override public void execute(Object message) throws Exception { msController.tell(message, super.source); } } private final class Leaving extends AbstractAction { public Leaving(ActorRef source) { super(source); } @Override public void execute(Object message) throws Exception { Leave leaveMsg = (Leave) message; if (!leaveMsg.isLiveCallModification()) { if (!receivedBye) { // Conference was stopped and this call was asked to leave // Send BYE to remote client sendBye(new Hangup("Conference time limit reached")); } } else { liveCallModification = true; } msController.tell(message, self()); } } private final class Stopping extends AbstractAction { public Stopping(final ActorRef source) { super(source); } @Override public void execute(Object message) throws Exception { // Stops media operations and closes media session msController.tell(new CloseMediaSession(), source); } } private final class Completed extends AbstractAction { public Completed(final ActorRef source) { super(source); } @Override public void execute(final Object message) throws Exception { if (logger.isInfoEnabled()) { logger.info("Completing Call sid: " + id + " from: " + from + " to: " + to + " direction: " + direction + " current external state: " + external); } //In the case of canceled that reach the completed method, don't change the external state if (!external.equals(CallStateChanged.State.CANCELED)) { // Notify the observers. external = CallStateChanged.State.COMPLETED; } CallStateChanged event = new CallStateChanged(external); if (external.equals(CallStateChanged.State.CANCELED)) { event = new CallStateChanged(external); } for (final ActorRef observer : observers) { observer.tell(event, source); } if (logger.isInfoEnabled()) { logger.info("Call sid: " + id + " from: " + from + " to: " + to + " direction: " + direction + " new external state: " + external); } // Record call data if (outgoingCallRecord != null && isOutbound()) { outgoingCallRecord = outgoingCallRecord.setStatus(external.toString()); final DateTime now = DateTime.now(); outgoingCallRecord = outgoingCallRecord.setEndTime(now); callDuration = (int) ((now.getMillis() - outgoingCallRecord.getStartTime().getMillis()) / 1000); outgoingCallRecord = outgoingCallRecord.setDuration(callDuration); recordsDao.updateCallDetailRecord(outgoingCallRecord); if (logger.isDebugEnabled()) { logger.debug("Start: " + outgoingCallRecord.getStartTime()); logger.debug("End: " + outgoingCallRecord.getEndTime()); logger.debug("Duration: " + callDuration); logger.debug("Just updated CDR for completed call"); } } if (isOutbound()) { executeStatusCallback(CallbackState.COMPLETED); } } } /* * EVENTS */ private void onRecord(Record message, ActorRef self, ActorRef sender) { if (is(inProgress)) { // Forward to media server controller this.recording = true; this.msController.tell(message, sender); } } private void onPlay(Play message, ActorRef self, ActorRef sender) { if (is(inProgress)) { // Forward to media server controller this.msController.tell(message, sender); } } private void onCollect(Collect message, ActorRef self, ActorRef sender) { if (is(inProgress)) { collectTimeout = message.timeout(); collectFinishKey = message.endInputKey(); // Forward to media server controller this.msController.tell(message, sender); } } private void onStopMediaGroup(StopMediaGroup message, ActorRef self, ActorRef sender) { if (is(inProgress) || is(waitingForAnswer)) { // Forward to media server controller this.msController.tell(message, sender); if (conferencing && message.isLiveCallModification()) { liveCallModification = true; self().tell(new Leave(true), self()); } } } private void onMute(Mute message, ActorRef self, ActorRef sender) { if (is(inProgress)) { // Forward to media server controller this.msController.tell(message, sender); muted = true; } } private void onUnmute(Unmute message, ActorRef self, ActorRef sender) { if (is(inProgress) && muted) { // Forward to media server controller this.msController.tell(message, sender); muted = false; if (logger.isInfoEnabled()) { final String infoMsg = String.format("Call %s, direction %s, unmuted", self().path(), direction); logger.info(infoMsg); } } } private void onObserve(Observe message, ActorRef self, ActorRef sender) throws Exception { final ActorRef observer = message.observer(); if (observer != null) { synchronized (this.observers) { this.observers.add(observer); observer.tell(new Observing(self), self); } } } private void onStopObserving(StopObserving stopObservingMessage, ActorRef self, ActorRef sender) throws Exception { final ActorRef observer = stopObservingMessage.observer(); if (observer != null) { observer.tell(stopObservingMessage, self); this.observers.remove(observer); } else { Iterator<ActorRef> observerIter = observers.iterator(); while (observerIter.hasNext()) { ActorRef observerNext = observerIter.next(); observerNext.tell(stopObservingMessage, self); if (logger.isInfoEnabled()) { logger.info("Sent stop observing for call, from: " + from + " to: " + to + " direction: " + direction + " to observer: " + observerNext.path() + " observer is terminated: " + observerNext.isTerminated()); } // this.observers.remove(observerNext); } this.observers.clear(); } } private void onGetCallObservers(GetCallObservers message, ActorRef self, ActorRef sender) throws Exception { sender.tell(new CallResponse<List<ActorRef>>(this.observers), self); } private void onGetCallInfo(GetCallInfo message, ActorRef sender) throws Exception { sender.tell(info(), self()); } private void onInitializeOutbound(InitializeOutbound message, ActorRef self, ActorRef sender) throws Exception { if (is(uninitialized)) { fsm.transition(message, queued); } } private void onChangeCallDirection(ChangeCallDirection message, ActorRef self, ActorRef sender) { // Needed for LiveCallModification API where the outgoing call also needs to move to the new destination. this.direction = INBOUND; this.liveCallModification = true; this.conferencing = false; this.conference = null; this.bridge = null; } private void onAnswer(Answer message, ActorRef self, ActorRef sender) throws Exception { inboundCallSid = message.callSid(); inboundConfirmCall = message.confirmCall(); if (is(ringing) && !invite.getSession().getState().equals(SipSession.State.TERMINATED)) { fsm.transition(message, initializing); } else { fsm.transition(message, canceled); } } private void onDial(Dial message, ActorRef self, ActorRef sender) throws Exception { if (is(queued)) { fsm.transition(message, initializing); } } private void onReject(Reject message, ActorRef self, ActorRef sender) throws Exception { if (is(ringing)) { fsm.transition(message, busy); } } private void onCancel(Cancel message, ActorRef self, ActorRef sender) throws Exception { if (is(initializing) || is(dialing) || is(ringing) || is(failingNoAnswer)) { if (logger.isInfoEnabled()) { logger.info("Got CANCEL for Call with the following details, from: " + from + " to: " + to + " direction: " + direction + " state: " + fsm.state() + ", will Cancel the call"); } fsm.transition(message, canceling); } else if (is(inProgress)) { if (logger.isInfoEnabled()) { logger.info("Got CANCEL for Call with the following details, from: " + from + " to: " + to + " direction: " + direction + " state: " + fsm.state() + ", will Hangup the call"); } onHangup(new Hangup(), self(), sender()); } else { if (logger.isInfoEnabled()) { logger.info("Got CANCEL for Call with the following details, from: " + from + " to: " + to + " direction: " + direction + " state: " + fsm.state()); } } } private void onReceiveTimeout(ReceiveTimeout message, ActorRef self, ActorRef sender) throws Exception { getContext().setReceiveTimeout(Duration.Undefined()); if (is(ringing) || is(dialing)) { fsm.transition(message, failingNoAnswer); } else if (is(inProgress) && collectSipInfoDtmf) { if (logger.isInfoEnabled()) { logger.info( "Collecting DTMF with SIP INFO, inter digit timeout fired. Will send finishKey to observers"); } MediaGroupResponse<String> infoResponse = new MediaGroupResponse<String>(collectFinishKey); for (final ActorRef observer : observers) { observer.tell(infoResponse, self()); } } else if (logger.isInfoEnabled()) { logger.info("Timeout received for Call : " + self().path() + " isTerminated(): " + self().isTerminated() + ". Sender: " + sender.path().toString() + " State: " + this.fsm.state() + " Direction: " + direction + " From: " + from + " To: " + to); } } private void onSipServletRequest(SipServletRequest message, ActorRef self, ActorRef sender) throws Exception { final String method = message.getMethod(); if ("INVITE".equalsIgnoreCase(method)) { if (is(uninitialized)) { fsm.transition(message, ringing); } if (is(inProgress)) { if (logger.isDebugEnabled()) { logger.debug("IN-Dialog INVITE received: " + message.getRequestURI().toString()); } inDialogInvite = message; String answer = null; if (!disableSdpPatchingOnUpdatingMediaSession) { if (logger.isInfoEnabled()) { logger.info( "Will patch SDP answer from 200 OK received with the external IP Address from Response on updating media session"); } final String externalIp = message.getInitialRemoteAddr(); final byte[] sdp = message.getRawContent(); answer = SdpUtils.patch(message.getContentType(), sdp, externalIp); } else { if (logger.isInfoEnabled()) { logger.info("SDP Patching on updating media session is disabled"); } answer = SdpUtils.getSdp(message.getContentType(), message.getRawContent()); } final UpdateMediaSession update = new UpdateMediaSession(answer); msController.tell(update, self()); if (isCallOnHoldSdp(answer)) { final CallHoldStateChange.State onHold = CallHoldStateChange.State.ONHOLD; for (final ActorRef observer : this.observers) { observer.tell(new CallHoldStateChange(onHold), self()); } } else if (isCallOffHoldSdp(answer)) { final CallHoldStateChange.State offHold = CallHoldStateChange.State.OFFHOLD; for (final ActorRef observer : this.observers) { observer.tell(new CallHoldStateChange(offHold), self()); } } } } else if ("CANCEL".equalsIgnoreCase(method)) { if (is(initializing)) { fsm.transition(message, canceling); } else if ((is(ringing) || is(waitingForAnswer)) && isInbound()) { fsm.transition(message, canceling); } // XXX can receive SIP cancel any other time? } else if ("BYE".equalsIgnoreCase(method)) { // Reply to BYE with OK this.receivedBye = true; final SipServletRequest bye = (SipServletRequest) message; final SipServletResponse okay = bye.createResponse(SipServletResponse.SC_OK); okay.send(); // Stop recording if necessary if (recording) { if (!direction.contains("outbound")) { // Initial Call sent BYE recording = false; if (logger.isInfoEnabled()) { logger.info("Call Direction: " + direction); logger.info("Initial Call - Will stop recording now"); } msController.tell(new Stop(false), self); // VoiceInterpreter will take care to prepare the Recording object } else if (direction.equalsIgnoreCase("outbound-api")) { //REST API Outgoing call, calculate recording recordingDuration = (DateTime.now().getMillis() - recordingStart.getMillis()) / 1000; } else if (conference != null) { // Outbound call sent BYE. !Important conference is the initial call here. conference.tell(new StopRecording(accountId, runtimeSettings, daoManager), null); } } if (conferencing) { // Tell conference to remove the call from participants list // before moving to a stopping state conference.tell(new RemoveParticipant(self), self); } else { // Clean media resources as necessary if (!is(completed)) fsm.transition(message, stopping); } } else if ("INFO".equalsIgnoreCase(method)) { processInfo(message); } else if ("ACK".equalsIgnoreCase(method)) { if (isInbound() && (is(initializing) || is(waitingForAnswer))) { if (logger.isInfoEnabled()) { logger.info("ACK received moving state to inProgress"); } fsm.transition(message, inProgress); } } } private void onSipServletResponse(SipServletResponse message, ActorRef self, ActorRef sender) throws Exception { this.lastResponse = message; final int code = message.getStatus(); switch (code) { case SipServletResponse.SC_CALL_BEING_FORWARDED: { forwarding(message); break; } case SipServletResponse.SC_RINGING: case SipServletResponse.SC_SESSION_PROGRESS: { if (!is(ringing)) { if (logger.isInfoEnabled()) { logger.info("Got 180 Ringing for Call: " + self().path() + " To: " + to + " sender: " + sender.path() + " observers size: " + observers.size()); } fsm.transition(message, ringing); } break; } case SipServletResponse.SC_BUSY_HERE: case SipServletResponse.SC_BUSY_EVERYWHERE: case SipServletResponse.SC_DECLINE: { sendCallInfoToObservers(); //Important. If state is DIALING, then do nothing about the BUSY. If not DIALING state move to failingBusy // // Notify the observers. // external = CallStateChanged.State.BUSY; // final CallStateChanged event = new CallStateChanged(external); // for (final ActorRef observer : observers) { // observer.tell(event, self); // } // XXX shouldnt it move to failingBusy IF dialing ???? // if (is(dialing)) { // break; // } else { // fsm.transition(message, failingBusy); // } if (!is(failingNoAnswer)) fsm.transition(message, failingBusy); break; } case SipServletResponse.SC_UNAUTHORIZED: case SipServletResponse.SC_PROXY_AUTHENTICATION_REQUIRED: { // Handles Auth for https://bitbucket.org/telestax/telscale-restcomm/issue/132/implement-twilio-sip-out if ((this.username != null || this.username.isEmpty()) && (this.password != null && this.password.isEmpty())) { sendCallInfoToObservers(); fsm.transition(message, failed); } else { AuthInfo authInfo = this.factory.createAuthInfo(); String authHeader = message.getHeader("Proxy-Authenticate"); if (authHeader == null) { authHeader = message.getHeader("WWW-Authenticate"); } String tempRealm = authHeader.substring(authHeader.indexOf("realm=\"") + "realm=\"".length()); String realm = tempRealm.substring(0, tempRealm.indexOf("\"")); authInfo.addAuthInfo(message.getStatus(), realm, this.username, this.password); SipServletRequest challengeRequest = message.getSession() .createRequest(message.getRequest().getMethod()); challengeRequest.addAuthHeader(message, authInfo); challengeRequest.setContent(this.invite.getContent(), this.invite.getContentType()); this.invite = challengeRequest; // https://github.com/Mobicents/RestComm/issues/147 Make sure we send the SDP again this.invite.setContent(message.getRequest().getContent(), "application/sdp"); if (outboundToIms) { final SipURI uri = factory.createSipURI(null, imsProxyAddress); uri.setPort(imsProxyPort); uri.setLrParam(true); challengeRequest.pushRoute(uri); } challengeRequest.send(); } break; } // https://github.com/Mobicents/RestComm/issues/148 // Session in Progress Response should trigger MMS to start the Media Session // case SipServletResponse.SC_SESSION_PROGRESS: case SipServletResponse.SC_OK: { if (is(dialing) || (is(ringing) && !"inbound".equals(direction))) { fsm.transition(message, updatingMediaSession); } break; } default: { if (code >= 400 && code != 487) { if (code == 487 && isOutbound()) { String initialIpBeforeLB = null; String initialPortBeforeLB = null; try { initialIpBeforeLB = message.getHeader("X-Sip-Balancer-InitialRemoteAddr"); initialPortBeforeLB = message.getHeader("X-Sip-Balancer-InitialRemotePort"); } catch (Exception e) { if (logger.isDebugEnabled()) { logger.debug("Exception during check of LB custom headers for IP address and port"); } } final SipServletRequest ack = message.createAck(); addCustomHeaders(ack); SipSession session = message.getSession(); if (initialIpBeforeLB != null) { if (initialPortBeforeLB == null) initialPortBeforeLB = "5060"; if (logger.isInfoEnabled()) { logger.info("We are behind load balancer, will use: " + initialIpBeforeLB + ":" + initialPortBeforeLB + " for ACK message, "); } String realIP = initialIpBeforeLB + ":" + initialPortBeforeLB; SipURI uri = factory.createSipURI(((SipURI) ack.getRequestURI()).getUser(), realIP); ack.setRequestURI(uri); } else if (!ack.getHeaders("Route").hasNext()) { final SipServletRequest originalInvite = message.getRequest(); final SipURI realInetUri = (SipURI) originalInvite.getRequestURI(); if ((SipURI) session.getAttribute("realInetUri") == null) { session.setAttribute("realInetUri", realInetUri); } final InetAddress ackRURI = InetAddress.getByName(((SipURI) ack.getRequestURI()).getHost()); final int ackRURIPort = ((SipURI) ack.getRequestURI()).getPort(); if (realInetUri != null && (ackRURI.isSiteLocalAddress() || ackRURI.isAnyLocalAddress() || ackRURI.isLoopbackAddress()) && (ackRURIPort != realInetUri.getPort())) { if (logger.isInfoEnabled()) { logger.info("Using the real ip address and port of the sip client " + realInetUri.toString() + " as a request uri of the ACK"); } realInetUri.setUser(((SipURI) ack.getRequestURI()).getUser()); ack.setRequestURI(realInetUri); } } ack.send(); if (logger.isInfoEnabled()) { logger.info("Just sent out ACK : " + ack.toString()); } } this.fail = true; fsm.transition(message, stopping); } } } } private void onHangup(Hangup message, ActorRef self, ActorRef sender) throws Exception { if (logger.isDebugEnabled()) { logger.debug("Got Hangup: " + message + " for Call, from: " + from + " to: " + to + " state: " + fsm.state() + " conferencing: " + conferencing + " conference: " + conference); } // Stop recording if necessary if (recording) { recording = false; if (logger.isInfoEnabled()) { logger.info("Call - Will stop recording now"); } msController.tell(new Stop(true), self); } if (is(updatingMediaSession) || is(ringing) || is(queued) || is(dialing) || is(inProgress) || is(completed) || is(waitingForAnswer)) { if (conferencing) { // Tell conference to remove the call from participants list // before moving to a stopping state conference.tell(new RemoveParticipant(self()), self()); } else { if (!receivedBye && !sentBye) { // Send BYE to client if RestComm took initiative to hangup the call sendBye(message); } // Move to next state to clean media resources and close session fsm.transition(message, stopping); } } else if (is(failingNoAnswer)) { fsm.transition(message, canceling); } } private void sendBye(Hangup hangup) throws IOException, TransitionNotFoundException, TransitionFailedException, TransitionRollbackException { final SipSession session = invite.getSession(); final String sessionState = session.getState().name(); if (sessionState == SipSession.State.TERMINATED.name()) { if (logger.isInfoEnabled()) { logger.info("SipSession already TERMINATED, will not send BYE"); } return; } else { if (logger.isInfoEnabled()) { logger.info("About to send BYE, session state: " + sessionState); } } if (sessionState == SipSession.State.INITIAL.name() || (sessionState == SipSession.State.EARLY.name() && isInbound())) { int sipResponse = (enable200OkDelay && hangup.getSipResponse() != null) ? hangup.getSipResponse() : Response.SERVER_INTERNAL_ERROR; final SipServletResponse resp = invite.createResponse(sipResponse); if (hangup.getMessage() != null && !hangup.getMessage().equals("")) { resp.addHeader("Reason", hangup.getMessage()); } addCustomHeaders(resp); resp.send(); fsm.transition(hangup, completed); return; } if (sessionState == SipSession.State.EARLY.name()) { final SipServletRequest cancel = invite.createCancel(); if (hangup.getMessage() != null && !hangup.getMessage().equals("")) { cancel.addHeader("Reason", hangup.getMessage()); } addCustomHeaders(cancel); cancel.send(); external = CallStateChanged.State.CANCELED; fsm.transition(hangup, completed); return; } else { final SipServletRequest bye = session.createRequest("BYE"); addCustomHeaders(bye); if (hangup.getMessage() != null && !hangup.getMessage().equals("")) { bye.addHeader("Reason", hangup.getMessage()); } SipURI realInetUri = (SipURI) session.getAttribute("realInetUri"); InetAddress byeRURI = InetAddress.getByName(((SipURI) bye.getRequestURI()).getHost()); // INVITE sip:+12055305520@107.21.247.251 SIP/2.0 // Record-Route: <sip:10.154.28.245:5065;transport=udp;lr;node_host=10.13.169.214;node_port=5080;version=0> // Record-Route: <sip:10.154.28.245:5060;transport=udp;lr;node_host=10.13.169.214;node_port=5080;version=0> // Record-Route: <sip:67.231.8.195;lr=on;ftag=gK0043eb81> // Record-Route: <sip:67.231.4.204;r2=on;lr=on;ftag=gK0043eb81> // Record-Route: <sip:192.168.6.219;r2=on;lr=on;ftag=gK0043eb81> // Accept: application/sdp // Allow: INVITE,ACK,CANCEL,BYE // Via: SIP/2.0/UDP 10.154.28.245:5065;branch=z9hG4bK1cdb.193075b2.058724zsd_0 // Via: SIP/2.0/UDP 10.154.28.245:5060;branch=z9hG4bK1cdb.193075b2.058724_0 // Via: SIP/2.0/UDP 67.231.8.195;branch=z9hG4bK1cdb.193075b2.0 // Via: SIP/2.0/UDP 67.231.4.204;branch=z9hG4bK1cdb.f9127375.0 // Via: SIP/2.0/UDP 192.168.16.114:5060;branch=z9hG4bK00B6ff7ff87ed50497f // From: <sip:+1302109762259@192.168.16.114>;tag=gK0043eb81 // To: <sip:12055305520@192.168.6.219> // Call-ID: 587241765_133360558@192.168.16.114 // CSeq: 393447729 INVITE // Max-Forwards: 67 // Contact: <sip:+1302109762259@192.168.16.114:5060> // Diversion: <sip:+112055305520@192.168.16.114:5060>;privacy=off;screen=no; reason=unknown; counter=1 // Supported: replaces // Content-Disposition: session;handling=required // Content-Type: application/sdp // Remote-Party-ID: <sip:+1302109762259@192.168.16.114:5060>;privacy=off;screen=no // X-Sip-Balancer-InitialRemoteAddr: 67.231.8.195 // X-Sip-Balancer-InitialRemotePort: 5060 // Route: <sip:10.13.169.214:5080;transport=udp;lr> // Content-Length: 340 ListIterator<String> recordRouteList = invite.getHeaders(RecordRouteHeader.NAME); if (invite.getHeader("X-Sip-Balancer-InitialRemoteAddr") != null) { if (logger.isInfoEnabled()) { logger.info( "We are behind LoadBalancer and will remove the first two RecordRoutes since they are the LB node"); } recordRouteList.next(); recordRouteList.remove(); recordRouteList.next(); recordRouteList.remove(); } if (recordRouteList.hasNext()) { if (logger.isInfoEnabled()) { logger.info("Record Route is set, wont change the Request URI"); } } else { if (logger.isInfoEnabled()) { logger.info("Checking RURI, realInetUri: " + realInetUri + " byeRURI: " + byeRURI); } if (logger.isDebugEnabled()) { logger.debug("byeRURI.isSiteLocalAddress(): " + byeRURI.isSiteLocalAddress()); logger.debug("byeRURI.isAnyLocalAddress(): " + byeRURI.isAnyLocalAddress()); logger.debug("byeRURI.isLoopbackAddress(): " + byeRURI.isLoopbackAddress()); } if (realInetUri != null && (byeRURI.isSiteLocalAddress() || byeRURI.isAnyLocalAddress() || byeRURI.isLoopbackAddress())) { if (logger.isInfoEnabled()) { logger.info("real ip address of the sip client " + realInetUri.toString() + " is not null, checking if the request URI needs to be patched"); } boolean patchRURI = true; 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 = bye.getAddressHeaders(RouteHeader.NAME); while (routes.hasNext() && patchRURI) { 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 of realNetURI " + realInetUri.getHost() + ":" + realInetUri.getPort() + " for the BYE request"); } if (routeHost.equalsIgnoreCase(realInetUri.getHost()) && routePort == realInetUri.getPort()) { if (logger.isDebugEnabled()) { logger.debug("route " + route + " is matching ip and port of realNetURI " + realInetUri.getHost() + ":" + realInetUri.getPort() + " for the BYE request, so not patching the Request-URI"); } patchRURI = false; } } } catch (ServletParseException e) { logger.error("Impossible to parse the route set from the BYE " + bye, e); } if (patchRURI) { if (logger.isInfoEnabled()) { logger.info("Using the real ip address of the sip client " + realInetUri.toString() + " as a request uri of the BYE request"); } bye.setRequestURI(realInetUri); } } } if (logger.isInfoEnabled()) { logger.info("Will sent out BYE to: " + bye.getRequestURI()); } try { bye.send(); sentBye = true; } catch (Exception e) { if (logger.isDebugEnabled()) { logger.debug("Exception during Send Bye: " + e.toString()); } } } } private void onNotFound(org.restcomm.connect.telephony.api.NotFound message, ActorRef self, ActorRef sender) throws Exception { if (is(ringing)) { fsm.transition(message, notFound); } } private void onMediaServerControllerStateChanged(MediaServerControllerStateChanged message, ActorRef self, ActorRef sender) throws Exception { if (logger.isInfoEnabled()) { logger.info("onMediaServerControllerStateChanged " + message.getState() + " inboundConfirmCall " + inboundConfirmCall); } switch (message.getState()) { case PENDING: if (is(initializing)) { fsm.transition(message, dialing); } break; case ACTIVE: if (is(initializing) || is(updatingMediaSession)) { SipSession.State sessionState = invite.getSession().getState(); if (!(SipSession.State.CONFIRMED.equals(sessionState) || SipSession.State.TERMINATED.equals(sessionState))) { // Issue #1649: mediaSessionInfo = message.getMediaSession(); if (inboundConfirmCall) { sendInviteOk(); } else { fsm.transition(message, waitingForAnswer); } } else if (SipSession.State.CONFIRMED.equals(sessionState) && is(inProgress)) { // We have an ongoing call and Restcomm executes new RCML app on that // If the sipSession state is Confirmed, then update SDP with the new SDP from MMS SipServletRequest reInvite = invite.getSession().createRequest("INVITE"); addCustomHeaders(reInvite); mediaSessionInfo = message.getMediaSession(); final byte[] sdp = mediaSessionInfo.getLocalSdp().getBytes(); String answer = null; if (mediaSessionInfo.usesNat()) { final String externalIp = mediaSessionInfo.getExternalAddress().getHostAddress(); answer = SdpUtils.patch("application/sdp", sdp, externalIp); } else { answer = mediaSessionInfo.getLocalSdp().toString(); } // Issue #215: // https://bitbucket.org/telestax/telscale-restcomm/issue/215/restcomm-adds-extra-newline-to-sdp answer = SdpUtils.endWithNewLine(answer); reInvite.setContent(answer, "application/sdp"); reInvite.send(); } // Make sure the SIP session doesn't end pre-maturely. invite.getApplicationSession().setExpires(0); if (isInbound()) { if (logger.isInfoEnabled()) { logger.info("current state: " + fsm.state() + " , will wait for OK to move to inProgress"); } } else { fsm.transition(message, inProgress); } } else if (is(inProgress) && inDialogRequest != null) { mediaSessionInfo = message.getMediaSession(); final byte[] sdp = mediaSessionInfo.getLocalSdp().getBytes(); String answer = null; if (mediaSessionInfo.usesNat()) { final String externalIp = mediaSessionInfo.getExternalAddress().getHostAddress(); answer = SdpUtils.patch("application/sdp", sdp, externalIp); } else { answer = mediaSessionInfo.getLocalSdp().toString(); } // Issue #215: // https://bitbucket.org/telestax/telscale-restcomm/issue/215/restcomm-adds-extra-newline-to-sdp answer = SdpUtils.endWithNewLine(answer); SipServletResponse resp = inDialogInvite.createResponse(Response.OK); resp.setContent(answer, "application/sdp"); resp.send(); } break; case INACTIVE: if (is(stopping)) { if (fail) { fsm.transition(message, failed); } else { fsm.transition(message, completed); } } else if (is(canceling)) { fsm.transition(message, canceled); } else if (is(failingBusy)) { fsm.transition(message, busy); } else if (is(failingNoAnswer)) { fsm.transition(message, noAnswer); } break; case FAILED: if (is(initializing) || is(updatingMediaSession) || is(joining) || is(leaving)) { fsm.transition(message, failed); } break; default: break; } } private void onJoinBridge(JoinBridge message, ActorRef self, ActorRef sender) throws Exception { if (is(inProgress) || is(waitingForAnswer)) { this.bridge = sender; this.fsm.transition(message, joining); } } private void onJoinConference(JoinConference message, ActorRef self, ActorRef sender) throws Exception { if (logger.isInfoEnabled()) { logger.info("********************* onJoinConference *********************"); } if (is(inProgress)) { this.conferencing = true; this.conference = sender; this.conferenceSid = message.getSid(); this.fsm.transition(message, joining); } } private void onJoinComplete(JoinComplete message, ActorRef self, ActorRef sender) throws Exception { //The CallController will send to the Call the JoinComplete message when the join completes if (is(joining)) { // Forward message to the bridge if (conferencing) { if (outgoingCallRecord != null && isOutbound()) { if (logger.isInfoEnabled()) { logger.info("Updating CDR for outgoing call: " + id.toString() + ", call status: " + external.name() + ", to include Conference details, conference: " + conferenceSid); } outgoingCallRecord = outgoingCallRecord.setConferenceSid(conferenceSid); outgoingCallRecord = outgoingCallRecord.setMuted(muted); recordsDao.updateCallDetailRecord(outgoingCallRecord); } this.conference.tell(message, self); } else { this.bridge.tell(message, self); } // Move to state In Progress fsm.transition(message, inProgress); } } private void onLeave(Leave message, ActorRef self, ActorRef sender) throws Exception { if (is(inProgress)) { fsm.transition(message, leaving); } else { if (logger.isDebugEnabled()) { logger.debug( "Received Leave for Call: " + self.path() + ", but state is :" + fsm.state().toString()); } } } private void onLeft(Left message, ActorRef self, ActorRef sender) throws Exception { if (is(leaving)) { if (conferencing) { // Let conference know the call exited the room this.conferencing = false; this.conference.tell(new Left(self()), self); this.conference = null; if (logger.isDebugEnabled()) { logger.debug("Call left conference room and notification sent to conference actor"); } } if (!liveCallModification) { // After leaving let the Interpreter know the Call is ready. fsm.transition(message, completed); } else { if (muted) { // Forward to media server controller this.msController.tell(new Unmute(), sender); muted = false; } if (!receivedBye) { fsm.transition(message, inProgress); } else { fsm.transition(message, completed); } } } } private void onStartRecordingCall(StartRecording message, ActorRef self, ActorRef sender) throws Exception { if (is(inProgress)) { if (runtimeSettings == null) { this.runtimeSettings = message.getRuntimeSetting(); } if (daoManager == null) { daoManager = message.getDaoManager(); } if (accountId == null) { accountId = message.getAccountId(); } // Forward message for Media Session Controller to handle message.setCallId(this.id); this.msController.tell(message, sender); this.recording = true; this.recordingUri = message.getRecordingUri(); this.recordingSid = message.getRecordingSid(); this.recordingStart = DateTime.now(); } } private void onStopRecordingCall(StopRecording message, ActorRef self, ActorRef sender) throws Exception { if (is(inProgress) && this.recording) { // Forward message for Media Session Controller to handle this.msController.tell(message, sender); this.recording = false; recordingDuration = (DateTime.now().getMillis() - recordingStart.getMillis()) / 1000; } } private void onBridgeStateChanged(BridgeStateChanged message, ActorRef self, ActorRef sender) throws Exception { if (is(inProgress) && isInbound() && enable200OkDelay) { switch (message.getState()) { case BRIDGED: sendInviteOk(); break; case FAILED: fsm.transition(message, stopping); default: break; } } else { if (logger.isDebugEnabled()) { logger.debug("Received BridgeStateChanged for Call: " + self.path() + ", but state is :" + fsm.state().toString()); } } } private void sendInviteOk() throws Exception { if (logger.isInfoEnabled()) { logger.info("sending initial invite ok, initialInviteOkSent:" + initialInviteOkSent); } if (!initialInviteOkSent) { final SipServletResponse okay = invite.createResponse(SipServletResponse.SC_OK); final byte[] sdp = mediaSessionInfo.getLocalSdp().getBytes(); String answer = null; if (mediaSessionInfo.usesNat()) { final String externalIp = mediaSessionInfo.getExternalAddress().getHostAddress(); answer = SdpUtils.patch("application/sdp", sdp, externalIp); } else { answer = mediaSessionInfo.getLocalSdp().toString(); } // Issue #215: // https://bitbucket.org/telestax/telscale-restcomm/issue/215/restcomm-adds-extra-newline-to-sdp answer = SdpUtils.endWithNewLine(answer); okay.setContent(answer, "application/sdp"); addCustomHeaders(okay); okay.send(); initialInviteOkSent = true; } } private boolean isCallOnHoldSdp(String answer) { if (answer.contains("a=inactive")) { return true; } if (answer.contains("a=sendonly")) { return true; } return false; } private boolean isCallOffHoldSdp(String answer) { if (answer.contains("a=sendrecv")) { return true; } if (!answer.contains("a=inactive")) { return true; } return false; } @Override public void postStop() { try { if (logger.isInfoEnabled()) { logger.info("Call actor at postStop, path: " + self().path() + ", direction: " + direction + ", state: " + fsm.state() + ", isTerminated: " + self().isTerminated() + ", sender: " + sender()); } onStopObserving(new StopObserving(), self(), null); getContext().stop(msController); } catch (Exception exception) { if (logger.isInfoEnabled()) { logger.info("Exception during Call postStop while trying to remove observers: " + exception); } } if (actAsImsUa && outgoingCallRecord != null) { recordsDao.removeCallDetailRecord(outgoingCallRecord.getSid()); } super.postStop(); } }