Java tutorial
/* * TeleStax, Open Source Cloud Communications * Copyright 2011-2014, Telestax Inc and individual contributors * by the @authors tag. * * This program is free software: you can redistribute it and/or modify * under the terms of the GNU Affero General Public License as * published by the Free Software Foundation; either version 3 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/> * */ package org.mobicents.servlet.restcomm.telephony; import 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.concurrent.TimeUnit; 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.sip.header.RecordRouteHeader; import javax.sip.message.Response; import org.apache.commons.configuration.Configuration; import org.joda.time.DateTime; import org.mobicents.javax.servlet.sip.SipSessionExt; import org.mobicents.servlet.restcomm.annotations.concurrency.Immutable; import org.mobicents.servlet.restcomm.configuration.RestcommConfiguration; import org.mobicents.servlet.restcomm.dao.CallDetailRecordsDao; import org.mobicents.servlet.restcomm.dao.DaoManager; import org.mobicents.servlet.restcomm.entities.CallDetailRecord; import org.mobicents.servlet.restcomm.entities.Sid; import org.mobicents.servlet.restcomm.fsm.Action; import org.mobicents.servlet.restcomm.fsm.FiniteStateMachine; import org.mobicents.servlet.restcomm.fsm.State; import org.mobicents.servlet.restcomm.fsm.Transition; import org.mobicents.servlet.restcomm.fsm.TransitionFailedException; import org.mobicents.servlet.restcomm.fsm.TransitionNotFoundException; import org.mobicents.servlet.restcomm.fsm.TransitionRollbackException; import org.mobicents.servlet.restcomm.mscontrol.messages.CloseMediaSession; import org.mobicents.servlet.restcomm.mscontrol.messages.Collect; import org.mobicents.servlet.restcomm.mscontrol.messages.CreateMediaSession; import org.mobicents.servlet.restcomm.mscontrol.messages.JoinBridge; import org.mobicents.servlet.restcomm.mscontrol.messages.JoinComplete; import org.mobicents.servlet.restcomm.mscontrol.messages.JoinConference; import org.mobicents.servlet.restcomm.mscontrol.messages.Leave; import org.mobicents.servlet.restcomm.mscontrol.messages.Left; import org.mobicents.servlet.restcomm.mscontrol.messages.MediaGroupResponse; import org.mobicents.servlet.restcomm.mscontrol.messages.MediaServerControllerStateChanged; import org.mobicents.servlet.restcomm.mscontrol.messages.MediaSessionInfo; import org.mobicents.servlet.restcomm.mscontrol.messages.Mute; import org.mobicents.servlet.restcomm.mscontrol.messages.Play; import org.mobicents.servlet.restcomm.mscontrol.messages.Record; import org.mobicents.servlet.restcomm.mscontrol.messages.StartRecording; import org.mobicents.servlet.restcomm.mscontrol.messages.Stop; import org.mobicents.servlet.restcomm.mscontrol.messages.StopMediaGroup; import org.mobicents.servlet.restcomm.mscontrol.messages.StopRecording; import org.mobicents.servlet.restcomm.mscontrol.messages.Unmute; import org.mobicents.servlet.restcomm.mscontrol.messages.UpdateMediaSession; import org.mobicents.servlet.restcomm.patterns.Observe; import org.mobicents.servlet.restcomm.patterns.Observing; import org.mobicents.servlet.restcomm.patterns.StopObserving; import org.mobicents.servlet.restcomm.util.SdpUtils; import akka.actor.ActorRef; import akka.actor.ReceiveTimeout; import akka.actor.UntypedActor; import akka.actor.UntypedActorContext; import akka.event.Logging; import akka.event.LoggingAdapter; import scala.concurrent.duration.Duration; /** * @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); // 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"; // Finite State Machine private final FiniteStateMachine fsm; private final State uninitialized; private final State initializing; 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 boolean fail; // SIP runtime stuff private final SipFactory factory; private String apiVersion; private Sid accountId; private String name; private SipURI from; private SipURI to; // custom headers for SIP Out https://bitbucket.org/telestax/telscale-restcomm/issue/132/implement-twilio-sip-out private Map<String, String> headers; private String username; private String password; private CreateCall.Type type; private long timeout; private SipServletRequest invite; private SipServletResponse lastResponse; // 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 muted; private boolean webrtc; // Conferencing private ActorRef conference; private boolean conferencing; // 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 Sid parentCallSid; // Runtime Setting private Configuration runtimeSettings; private Configuration configuration; private boolean disableSdpPatchingOnUpdatingMediaSession; public Call(final SipFactory factory, final ActorRef mediaSessionController, final Configuration configuration) { super(); final ActorRef source = self(); // States for the FSM this.uninitialized = new State("uninitialized", null, null); this.initializing = new State("initializing", new Initializing(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); // 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.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.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.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.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)); // 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; this.disableSdpPatchingOnUpdatingMediaSession = this.configuration.subset("runtime-settings") .getBoolean("disable-sdp-patching-on-updating-mediasession", false); } 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() { final String from = this.from.getUser(); final String to = this.to.getUser(); final CallInfo info = new CallInfo(id, external, type, direction, created, forwardedFrom, name, from, to, invite, lastResponse, webrtc, muted, callUpdatedTime); return new CallResponse<CallInfo>(info); } 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, self, 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.mobicents.servlet.restcomm.telephony.NotFound.class.equals(klass)) { onNotFound((org.mobicents.servlet.restcomm.telephony.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); } } 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", id.toString() + "-" + instanceId); } // 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 { 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(); String toHeaderString = to.toString(); if (toHeaderString.indexOf('?') != -1) { // custom headers parsing for SIP Out // https://bitbucket.org/telestax/telscale-restcomm/issue/132/implement-twilio-sip-out headers = new HashMap<String, String>(); // 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); headers.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); builder.setTo(to.getUser()); 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 StringBuilder buffer = new StringBuilder(); buffer.append(to.getHost()); if (to.getPort() > -1) { buffer.append(":").append(to.getPort()); } String transport = to.getTransportParam(); if (transport != null) { buffer.append(";transport=").append(to.getTransportParam()); } final SipURI uri = factory.createSipURI(null, buffer.toString()); final SipApplicationSession application = factory.createApplicationSession(); application.setAttribute(Call.class.getName(), self); 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 = factory.createRequest(application, "INVITE", fromAddress, toAddress); } else { invite = factory.createRequest(application, "INVITE", from, to); } invite.pushRoute(uri); if (headers != null) { // adding custom headers for SIP Out // https://bitbucket.org/telestax/telscale-restcomm/issue/132/implement-twilio-sip-out Set<Map.Entry<String, String>> entrySet = headers.entrySet(); for (Map.Entry<String, String> entry : entrySet) { invite.addHeader("X-" + entry.getKey(), entry.getValue()); } } addCustomHeaders(invite); // invite.addHeader("X-RestComm-ApiVersion", apiVersion); // invite.addHeader("X-RestComm-AccountSid", accountId.toString()); // invite.addHeader("X-RestComm-CallSid", id.toString()); 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(CreateCall.Type.CLIENT) || type.equals(CreateCall.Type.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)); } } 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 = (SipURI) 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); } } // 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()) { final UntypedActorContext context = getContext(); context.setReceiveTimeout(Duration.Undefined()); final SipServletRequest cancel = invite.createCancel(); addCustomHeaders(cancel); cancel.send(); } } 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); 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.mobicents.servlet.restcomm.telephony.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); 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); 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 = invite.createResponse(503, "Problem to setup services"); addCustomHeaders(resp); if (message instanceof CallFail) { String reason = ((CallFail) message).getReason(); if (reason != null) resp.addHeader("Reason", reason); } resp.send(); } else { if (message instanceof CallFail) sendBye(new Hangup(((CallFail) message).getReason())); } // Notify the observers. external = CallStateChanged.State.FAILED; final CallStateChanged 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); } 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 CreateMediaSession generateRequest(SipServletMessage sipMessage) throws IOException, SdpException, ServletParseException { 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); } } final byte[] sdp = sipMessage.getRawContent(); final String offer = SdpUtils.patch(sipMessage.getContentType(), sdp, externalIp); return new CreateMediaSession("sendrecv", offer, false, webrtc); } } 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.isInfoEnabled()) { logger.info("We are behind load balancer, will use: " + initialIpBeforeLB + ":" + initialPortBeforeLB + " for ACK message, "); } String realIP = initialIpBeforeLB + ":" + initialPortBeforeLB; SipURI uri = factory.createSipURI(null, realIP); 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"); } 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 InProgress extends AbstractAction { public InProgress(final ActorRef source) { super(source); } @Override public void execute(final Object message) throws Exception { // Notify the observers. 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.name()); outgoingCallRecord = outgoingCallRecord.setAnsweredBy(to.getUser()); recordsDao.updateCallDetailRecord(outgoingCallRecord); } } } 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 { // if (!receivedBye) { // // Conference was stopped and this call was asked to leave // // Send BYE to remote client // sendBye(); // } msController.tell(message, super.source); } } 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; } final CallStateChanged 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); final int seconds = (int) ((now.getMillis() - outgoingCallRecord.getStartTime().getMillis()) / 1000); outgoingCallRecord = outgoingCallRecord.setDuration(seconds); recordsDao.updateCallDetailRecord(outgoingCallRecord); if (logger.isDebugEnabled()) { logger.debug("Start: " + outgoingCallRecord.getStartTime()); logger.debug("End: " + outgoingCallRecord.getEndTime()); logger.debug("Duration: " + seconds); logger.debug("Just updated CDR for completed call"); } } } } /* * 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)) { // Forward to media server controller this.msController.tell(message, sender); } } private void onStopMediaGroup(StopMediaGroup message, ActorRef self, ActorRef sender) { if (is(inProgress)) { // Forward to media server controller this.msController.tell(message, sender); } } 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; } } 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 self, 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 { 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 (logger.isInfoEnabled()) { logger.info("Got CANCEL for Call with the following details, from: " + from + " to: " + to + " direction: " + direction + " state: " + fsm.state()); } if (is(initializing) || is(dialing) || is(ringing) || is(failingNoAnswer)) { fsm.transition(message, canceling); } } private void onReceiveTimeout(ReceiveTimeout message, ActorRef self, ActorRef sender) throws Exception { getContext().setReceiveTimeout(Duration.Undefined()); if (is(ringing)) { fsm.transition(message, failingNoAnswer); } 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); } } else if ("CANCEL".equalsIgnoreCase(method)) { if (is(initializing)) { fsm.transition(message, canceling); } else if (is(ringing) && 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 (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 fsm.transition(message, stopping); } } else if ("INFO".equalsIgnoreCase(method)) { processInfo(message); } else if ("ACK".equalsIgnoreCase(method)) { if (isInbound() && is(initializing)) { 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); // } 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.password == null) { 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"); 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(null, 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"); } ack.setRequestURI(realInetUri); } } ack.send(); if (logger.isInfoEnabled()) { logger.info("Just sent out ACK : " + ack.toString()); } } this.fail = true; sendCallInfoToObservers(); fsm.transition(message, stopping); } } } } private void onHangup(Hangup message, ActorRef self, ActorRef sender) throws Exception { if (logger.isDebugEnabled()) { logger.debug("Got Hangup for Call, from: " + from + " to: " + to + " state: " + fsm.state()); } if (is(updatingMediaSession) || is(ringing) || is(queued) || is(dialing) || is(inProgress)) { if (!receivedBye) { // Send BYE to client if RestComm took initiative to hangup the call sendBye(message); } // Stop recording if necessary if (recording) { recording = false; if (logger.isInfoEnabled()) { logger.info("Call - Will stop recording now"); } msController.tell(new Stop(true), self); } // Move to next state to clean media resources and close session fsm.transition(message, stopping); } } private void sendBye(Hangup hangup) throws IOException, TransitionNotFoundException, TransitionFailedException, TransitionRollbackException { final SipSession session = invite.getSession(); String sessionState = session.getState().name(); if (sessionState == SipSession.State.INITIAL.name() || (sessionState == SipSession.State.EARLY.name() && isInbound())) { final SipServletResponse resp = invite.createResponse(Response.SERVER_INTERNAL_ERROR); 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(); 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("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()); } bye.send(); } } private void onNotFound(org.mobicents.servlet.restcomm.telephony.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 { 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(); boolean waitForAck = false; if (!(SipSession.State.CONFIRMED.equals(sessionState) || SipSession.State.TERMINATED.equals(sessionState))) { mediaSessionInfo = message.getMediaSession(); 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"); // okay.addHeader("X-RestComm-CallSid",id.toString()); addCustomHeaders(okay); okay.send(); waitForAck = true; } 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); // Activate call if (!waitForAck) { fsm.transition(message, inProgress); } else if (logger.isInfoEnabled()) { logger.info("current state: " + fsm.state() + " , will wait for ACK to move to inProgress"); } } 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)) { this.bridge = sender; this.fsm.transition(message, joining); } } private void onJoinConference(JoinConference message, ActorRef self, ActorRef sender) throws Exception { if (is(inProgress)) { this.conferencing = true; this.conference = sender; this.fsm.transition(message, joining); } } private void onJoinComplete(JoinComplete message, ActorRef self, ActorRef sender) throws Exception { if (is(joining)) { // Forward message to the bridge if (conferencing) { 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); } } 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); this.conference = null; } // After leaving let the Interpreter know the Call is ready. fsm.transition(message, inProgress); } } 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; } } 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; } } @Override public void postStop() { try { 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); } } super.postStop(); } }