Microsoft Lync and Skype for Business have a rich set of .NET APIs which make it easy to extend the platform and integrate it with other applications. This blog helps explain how to use those APIs.

Rerouting requests to a UCMA application with MSPL

Posted: September 4th, 2011 | Author: | Filed under: Lync Development, MSPL | Tags: , , | 25 Comments »

One of the most useful (and most confusing) things you can do with Microsoft SIP Processing Language (MSPL) is change the routing behaviour of Lync Server. There are a variety of reasons why you might want to do this, but in this post I want to discuss a specific case: rerouting calls to a UCMA application.

Let’s say, for purposes of illustration, that you have a UCMA application that establishes a user endpoint for each of several users in your environment and answers calls on their behalf. By default, though, Lync Server will “fork” an incoming call to all locations where the receiving user is logged in. In other words, it will send a copy of the INVITE request, with a special identifying tag added, to each network location where the user has an endpoint registered. As soon as one of the endpoints answers the call, the INVITEs to the others are canceled.

This puts a bit of a damper on the idea of having the UCMA application take that user’s calls. Yes, it can answer the incoming call for the user immediately as soon as it begins to ring, but the UCMA application itself can’t prevent the call from ringing to the user if he or she is logged into the Lync client somewhere.

MSPL offers a workaround for this problem, allowing you to override the default routing and tell Lync Server to route the INVITE request only to the server (or servers) where the UCMA application is located.

I covered the basics of MSPL scripts and how to write them in a previous post, so I won’t go into too much detail about that here. Instead, I want to discuss the key tool you have in MSPL scripts for changing message routing, the ProxyRequest method.

ProxyRequest can be called with no parameters, like so:

if (sipRequest) {
    ProxyRequest();
}

This simply tells Lync Server to go ahead and send the request on to wherever it would have sent it based on the default routing rules. Normally you would use this in an application that does not proxy requests by default: i.e., it has the following element at the beginning:

<lc:proxyByDefault action="false" />

The effect of setting proxyByDefault to false is that any request that you do not explicitly proxy by calling ProxyRequest will be declined by the server. You might use this to perform some type of message filtering:

msgContent = sipRequest.Content;
ignoreCase = true;

// Only accept messages that don't contain the word "secret."
if (!Contains(msgContent, "secret", ignoreCase)) {
    ProxyRequest();
}

It’s also possible, though, to pass a parameter to ProxyRequest. This causes Lync Server to change the request URI for the message to what you specify, effectively redirecting the request to a specific location.

Let’s take a look at how you can use this in two different situations: with an ApplicationEndpoint, and with a UserEndpoint.

Proxying all requests to an ApplicationEndpoint

For certain scenarios, you might want to send all of the calls you are intercepting to a single ApplicationEndpoint, in order to play a message, ask for information, and/or pass the call along to the intended recipient. To do this, you can call ProxyRequest with the GRUU of the application as the parameter. This will look something like the following.

ProxyRequest("sip:appserver.domain.local@domain.local;gruu;opaque=srvr:interceptor:saLA7P02gZi2Ho78Ix_w2AA");

It’s important to note that this should be in a script that filters only INVITE requests, which is specified in an element like this:

<lc:requestFilter methodNames="INVITE" strictRoute="false"
    registrarGenerated="false" domainSupported="true" />

An alternative is to wrap the code in an if statement that checks whether this is a request and what method name it has:

if (sipRequest && sipRequest.Method == "INVITE") {
    ProxyRequest("sip:appserver.domain.local@domain.local;gruu;opaque=srvr:interceptor:saLA7P02gZi2Ho78Ix_w2AA");
}

The GRUU is an endpoint-specific URI, so this will send the call to the exact instance of the UCMA application whose GRUU you specify.

Note: The application must have a default routing endpoint (click for more details) in order to receive the call. Why? Because the To header on the INVITE message in this case has the SIP URI of the originally intended recipient, and not the SIP URI of an application endpoint.

This is actually one of the chief advantages of the approach I am describing here: the UCMA application, when accepting the call, can tell by looking at the To header where the call was originally meant to go. This means that if your application needs to forward the call to its original destination at some point, it has the information it needs to do this. Here is an example of an incoming call handler that looks at the To header and stores it for later use:

void OnCallReceived(object sender, CallReceivedEventArgs e)
{
     _originalRecipientUri = e.RequestData.ToHeader.Uri;

     e.Call.BeginAccept(OnAcceptCompleted, e.Call);
}

If you don’t need the To header

What if your application will not be transferring the call to the original recipient when it is done? This might be the case if, for example, your application is simply playing a failure message to callers who are not authorized to reach the number they have dialed. In this case, you can do something a bit simpler in your script:

if (sipRequest && sipRequest.Method == "INVITE") {

    // Check to see if the caller is authorized
    if (ContainsString(sipRequest.From, "sip:authorized@domain.local", true)) {
        // Allow the call to continue to its original destination.
        ProxyRequest();
    } else {
        // Forward the call to the UCMA app.
        Log("Debugr", 1, "Forwarded caller to UCMA app: ", sipRequest.From);
        Respond("302", "Moved Temporarily", "Contact=");
    }
}

Instead of proxying the INVITE message to another location, this script simply responds with a 302 response code,  to indicate to the caller that it should direct its call to a different URI. That other URI is specified in a Contact header which the script adds to the response. The caller, assuming it responds to the 302 code properly, will place the call again to the new URI, in this case your application.

This has been a long post, but I hope it has helped to explain some of the useful routing changes that can be made with MSPL. Please feel free to get in touch if you have any questions about how to do this or if you have suggestions for future posts!


25 Comments on “Rerouting requests to a UCMA application with MSPL”

  1. 1 Stian Thoresen said at 12:14 am on September 19th, 2011:

    Hi. First of all I’d like to say that you’re doing a great job. This site has been really helpful when developing applications for the Lync server. Very well written and excellent code-snippets which are easy to follow and understand.

    I do got one question though; When filtering “INVITE” requests in MSPL it seems like this also applies to Instant Messages, is this true?

    I got a MSPL-script running which is intended to filter out AudioVideo call requests, but this seems to apply to IMs as well. Do you got any information on how to do this?

    From what I’ve read the request for IMs should be “MESSAGE” and not “INVITES”.

    Any help on this topic would be much appreciated!

    -Stian

  2. 2 Michael said at 9:45 pm on October 5th, 2011:

    Hi Stian,

    Sorry, I just saw your comment now. MESSAGE is actually for sending IM messages back and forth after the IM dialog has been established. The inital request is still an INVITE. To distinguish between audio and IM conversations, you can look at the SDP (i.e. Session Description Protocol) in the message body of the INVITE. In MSPL you can get that by looking at sipRequest.Content. You’ll want to look for the m= field, which will have something like m=audio if it’s an INVITE for an audio call.

    Hope this helps.

    Michael

  3. 3 Schueh said at 8:41 am on February 2nd, 2012:

    You have to set the IsDefaultRoutingEndpoint Property of your application endpoint settings to “true”. Otherwise you don’t receive any calls on your UCMA application.

  4. 4 Michael said at 11:28 pm on February 2nd, 2012:

    Hi Schueh, that was mentioned in the post, but since it was buried in the middle I made some edits to make it more visible. Sorry for any confusion.

  5. 5 Michael J said at 1:49 am on February 24th, 2012:

    Hi. This post is great and very helpful, thank you for this.

    I’ve written such an application to send all INVITES for a specified SIP URI address to an ucma application. But when I try to accept the call in the default endpoint, the Lync Server swallows all responses, especially the 200 OK, and the accept method times out. Do I missed something in the skript?

    Any help on this topic would be much appreciated!

    Michael

  6. 6 Michael said at 9:14 pm on February 26th, 2012:

    Hi Michael,

    What types of requests and responses are you capturing in your script – what’s in your requestFilter and responseFilter elements?
    Also, what do you have as the value of proxyByDefault?

    Thanks,
    Michael

  7. 7 David said at 7:44 am on March 8th, 2012:

    I have written a managed app that does forwarding by looking at invites for specific destination and then does the following:

    Response resp = e.Request.CreateResponse();
    resp.ReasonPhrase = “Forwarded Unconditionally”;
    resp.StatusCode = 302;
    Header contactHeader = resp.AllHeaders.FindFirst(“Contact”);
    if (contactHeader != null)
    resp.AllHeaders.Remove(contactHeader);
    contactHeader = new Header(“Contact”);
    contactHeader.Value = forwardTo;
    resp.AllHeaders.Add(contactHeader);
    e.ServerTransaction.SendResponse(resp);

    This works perfectly fine except for that the next invite has no information about the original called number. I have tried adding Referred-By and History-Info and still no luck. I have even started with trying to add embedded headers in the contact header as well. Is this possible with Lync or does one have to use ProxyRequest to a default endpoint like you suggested in another article. There the original To should be intact

  8. 8 Michael said at 8:40 am on March 13th, 2012:

    Hi David – There’s no way as far as I know to include information about the original destination when forwarding a call. The problem, as you said, is that the caller creates a completely new INVITE which doesn’t include the original To header. Like you, I tried putting the info in a few different places with no luck, and that’s what led me to using ProxyRequest.

  9. 9 Ian said at 2:44 am on March 21st, 2012:

    I followed your instruction on this blog however, my UCMA application is not working correctly.
    Any comments would be appreciated.

    Request is rerouted to UCMA application by MSPL but, UCMA application is not accepting the call. Timeout exception occurs.
    In my dev environment, UCMA application is running on a dedicated server not on FE.

    Here’s my codes. Please, any help is much appreciated!

    ###########################
    MSPL script source
    ###########################

    ###########################
    UCMA application source
    ###########################

    class TestDomainLyncCallManager
    {
    ApplicationEndpoint _appEndpoint;
    CollaborationPlatform _collaborationPlatform;
    int _endpointsDiscovered = 0;

    ManualResetEvent _startupWaitHandle = new ManualResetEvent(false);
    ManualResetEvent _shutdownWaitHandle = new ManualResetEvent(false);

    static void Main(string[] args)
    {

    TestDomainLyncCallManager fm = new TestDomainLyncCallManager();
    fm.Run();

    fm.WaitForStartup();

    string entry = Console.ReadLine();
    }

    private void Run()
    {

    string applicationUserAgent = “TestDomain”;
    string applicationId = Properties.Settings.Default.ApplicationID;

    ProvisionedApplicationPlatformSettings settings =
    new ProvisionedApplicationPlatformSettings(applicationUserAgent, applicationId);

    _collaborationPlatform = new CollaborationPlatform(settings);

    _collaborationPlatform.RegisterForApplicationEndpointSettings(OnApplicationEndpointSettingsDiscovered);

    Console.WriteLine(“Starting collaboration platform…”);

    _collaborationPlatform.BeginStartup(OnPlatformStartupCompleted, null);
    }

    public void WaitForStartup()
    {
    _startupWaitHandle.WaitOne();
    }

    private void OnPlatformStartupCompleted(IAsyncResult result)
    {
    try
    {
    _collaborationPlatform.EndStartup(result);

    Console.WriteLine(“Collaboration platform started.”);
    }
    catch (RealTimeException ex)
    {
    Console.WriteLine(“Platform startup failed: {0}”, ex);
    }
    }

    private void OnApplicationEndpointSettingsDiscovered(object sender, ApplicationEndpointSettingsDiscoveredEventArgs args)
    {

    Interlocked.Increment(ref _endpointsDiscovered);

    if (_endpointsDiscovered > 1)
    {
    return;
    }

    ApplicationEndpointSettings settings = args.ApplicationEndpointSettings;
    settings.IsDefaultRoutingEndpoint = true;

    _appEndpoint = new ApplicationEndpoint(_collaborationPlatform, settings);//args.ApplicationEndpointSettings);

    _appEndpoint.BeginEstablish(OnApplicationEndpointEstablishCompleted, null);
    }

    private void OnApplicationEndpointEstablishCompleted(IAsyncResult result)
    {
    try
    {
    _appEndpoint.EndEstablish(result);

    Console.WriteLine(“Application endpoint established.”);

    _startupWaitHandle.Set();

    _appEndpoint.RegisterForIncomingCall(OnAudioVideoCallReceived);

    }
    catch (RealTimeException ex)
    {
    Console.WriteLine(“Application endpoint establishment failed: {0}”, ex);
    }
    }

    private void OnAudioVideoCallReceived(object sender,
    CallReceivedEventArgs args)
    {
    try
    {

    Console.WriteLine(“Accepting call.”);

    Console.WriteLine(string.Format(“ToHeader.Uri={0}”, args.RequestData.ToHeader.Uri));
    foreach (SignalingHeader h in args.RequestData.SignalingHeaders)
    {
    if (h.Name.Equals(“OriginalAddress”))
    {
    Console.WriteLine(string.Format(“SignalingHeader={0}, Value={1}”, h.Name, h.GetValue()));
    break;
    }
    }

    args.Call.BeginAccept(
    ar =>
    {
    try
    {

    Console.WriteLine(“Accepting call 2.”);

    args.Call.StateChanged += Call_StateChanged;
    args.Call.EndAccept(ar); // ERROR OCCURS

    Console.WriteLine(“Accepted call.”);

    }
    catch (RealTimeException ex)
    {
    Console.WriteLine(“Failed to accept call. {0}”, ex);
    }
    },
    null);

    }
    catch (InvalidOperationException ex)
    {
    Console.WriteLine(“Failed to accept call. {0}”, ex);
    }
    }

    void Call_StateChanged(object sender, CallStateChangedEventArgs e)
    {
    Console.WriteLine(“State changed {0} to {1}”, e.PreviousState, e.State);
    //throw new NotImplementedException();
    if (e.State == CallState.Terminated)
    {

    }
    }

    private void Flow_StateChanged(object sender, MediaFlowStateChangedEventArgs e)
    {
    AudioVideoFlow flow = sender as AudioVideoFlow;

    if (e.State == MediaFlowState.Terminated)
    {
    // Detach the flow from the player if
    // the flow has terminated.
    //_player.DetachFlow(flow);

    // Remove the event handler for the flow.
    flow.StateChanged -= Flow_StateChanged;
    }
    }
    }

    ###########################
    App.config
    ###########################

    urn:application:lyncwidget

    ###########################
    Error messages
    ###########################

    Accepting call.
    ToHeader.Uri=sip:sahan@testDomain.com
    Accepting call 2.
    Failed to accept call. Microsoft.Rtc.Signaling.OperationTimeoutException:This operation has timed out.
    Location: Microsoft.Rtc.Signaling.SipAsyncResult`1.ThrowIfFailed()
    Location: Microsoft.Rtc.Signaling.Helper.EndAsyncOperation[T](Object owner, IAsyncResult result)
    Location: Microsoft.Rtc.Signaling.Helper.EndAsyncOperation[T](Object owner, IAsyncResult result, String operationId)
    Location: Microsoft.Rtc.Collaboration.Call.EndAcceptCore(IAsyncResult result)
    Location: CallManager.TestDomainLyncCallManager.c__DisplayClass2.b__0(IAsyncResult ar) file e:00.Source\Lync201000.reg\P
    rofessional_Unified_Communications_Development_with_Microsoft_Lync_Server_Code\939031 ch12 Code\CallManager\Program.cs:Line 153
    Detected Location: System.Environment.get_StackTrace()
    Location: Microsoft.Rtc.Signaling.Helper.get_StackTrace()
    Location: Microsoft.Rtc.Signaling.OperationTimeoutException..ctor(String message)
    Location: Microsoft.Rtc.Signaling.SipAcceptAsyncResult.TimerItem_Expired(Object sender, EventArgs e)
    Location: Microsoft.Rtc.Signaling.TimerItem.OnExpired(Object state)
    Location: Microsoft.Rtc.Signaling.QueueWorkItemState.ExecuteWrappedMethod(WaitCallback method, Object state)
    Location: System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
    Location: System.Threading._ThreadPoolWaitCallback.PerformWaitCallbackInternal(_ThreadPoolWaitCallback tpWaitCallBack)
    Location: System.Threading._ThreadPoolWaitCallback.PerformWaitCallback(Object state)

    ###########################
    Powershell script to register
    ###########################

    New-CsTrustedApplicationPool -Identity fnlyncdev21.testDomain.com -Registrar lyncpool2.testDomain.com -Site 1 Enable-CsTopology New-CsTrustedApplication -ApplicationId “LyncWidget” -TrustedApplicationPoolFqdn “fnlyncdev21.testDomain.com” -port 10607 New-CsTrustedApplicationEndpoint -ApplicationId “urn:application:LyncWidget” -TrustedApplicationPoolFqdn “fnlyncdev21.testDomain.com” -SipAddress sip:lyncwidget@testDomain.com -DisplayName “Lync Presence Service”

  10. 10 Ian said at 6:45 am on March 21st, 2012:

    I’m sorry but, some part of codes are not posted. you can download it from here.
    http://db.tt/juK8A6OP

  11. 11 Michael said at 5:32 pm on March 27th, 2012:

    Ian – is your OnAudioVideoCallReceived event handler being executed at all? If not, I would check firewall settings and make sure the listening port is not being blocked. You might also want to get a SIP log using OCSLogger.exe (S4 and SIPStack logs) on the Front End Server to see if it gives you any clues as to what is happening.

  12. 12 Matt said at 9:24 am on May 10th, 2012:

    Great Article and exactly what I was looking for! I have a MSPL now that forwards a call made to a DID (sip:+1555555555) to an UCMA app and the the UCMA is playing a WMA file and forwarding the call to another user. Ultimately we’d like to have a “You will be recorded for QA” in front of DID’s and then transfer to the original recipient of the call.

    Question, once we start transferring from the UCMA to the original TO address, how do you prevent the MSPL script from seeing that invite and looping back to UCMA? Would it be best to filter the MSPL to not look for the transfer (or ignore calls with a from address of the UCMA app), or would you do a look up in the UCMA app for the user’s sip address and forward to that (sip:test@domain.com) and no their DID?

  13. 13 Duncan said at 4:47 am on November 5th, 2012:

    There is a small typo in the XML example:

    There is a wrong placed space on the close tag, which will cause an MSPL compile error. If your app is not set to critical, the script won’t be run.

  14. 14 Michael said at 2:30 pm on November 7th, 2012:

    Hi Duncan, thanks for catching that! I’ll fix it now.

  15. 15 Michael said at 2:37 pm on November 7th, 2012:

    Matt, sorry for the late reply. One approach is to have yoru MSPL script look at the contents of the Referred-By header, and not intercepting any calls that have your UCMA app as the referring endpoint.

    Another option is to use a BackToBackCall to connect the caller with the called user, and to have your MSPL script ignore calls originating from your UCMA app – with a BackToBackCall it will be your UCMA app’s URI in the From header.

  16. 16 Gene said at 1:42 pm on November 19th, 2012:

    I know very little about programming however I’ve been told UCMA does not allow a 3rd part PIC to register with a UCMA Application. I tested MS Messenger with Lync couldn’t determine how to get it to work. Do you know if this is a supported feature?

  17. 17 Michael said at 10:37 am on November 20th, 2012:

    What do you mean by register? PIC contacts can’t subscribe to the presence of UCMA applications if that’s what you mean. They should however be able to send messages to them assuming the UCMA application’s contact is enabled for federation.

  18. 18 Ahmad said at 5:07 am on January 2nd, 2014:

    Is it possible to Register, send Invites through Lync server as a trusted application ?

  19. 19 HimansuIt Services said at 6:31 am on March 24th, 2014:

    Hai

    I create the mspl to transfer reroute the calls to ucma application endpoint

    Below is my code for MSPL

    I regidter my MSPl By Using power shell
    New-Csserver application

    and my c# code is

    private void Run()
    {
    try
    {
    _helper = new UCMASampleHelper();

    _appEndpoint = _helper.CreateAutoProvisionedApplicationEndpoint();
    _appEndpoint.RegisterForIncomingCall(AudioVideoCall_Received);

    void AudioVideoCall_Received(object sender, CallReceivedEventArgs e)
    {
    try
    {
    string _originalRecipientUri = e.RequestData.ToHeader.Uri;

    _inboundAVCall.StateChanged += new EventHandler(_inboundAVCall_StateChanged);
    _inboundAVCall.AudioVideoFlowConfigurationRequested += new EventHandler(_inboundAVCall_AudioVideoFlowConfigurationRequested);

    Console.WriteLine(“Call Received!”);

    _inboundAVCall.BeginAccept(BeginAcceptCB, _inboundAVCall);

    }}

    When i get the call MSPL route the call to the UCMA application and transfer to the agents but when agent accepts the call it is not establishing and i am gettimg the following error find in ocs logger

    SIP/2.0 481 Call Leg/Transaction Does Not Exit

    Can Any one please help me

    Thanks.

  20. 20 Lokesh said at 9:00 am on April 21st, 2014:

    Iam developing a call center application where in we are using UCMA and MSPL to redirect the calls, here in my local environment we are receiving calls by the application properly, but in production environment we are not able to receive the calls, and we are getting 10 times hit to application for one call, and immediatly disconnecting once try to transfer the call to establish.
    Could you please assist me so that it would be very helpfull for me, thanks in advance.

  21. 21 Sairam said at 11:23 am on August 8th, 2014:

    Hi,
    I am developing an UCMA application using AutoProvision. Collaboration platform started successfully. This is my call

    “appEndpoint.BeginEstablish(EndEndpointEstablish, null);”

    Here “EndEndpointEstablish” is not getting called.

    Any help?

  22. 22 Jas said at 11:58 pm on September 13th, 2014:

    I’m trying to detect how many rings or how long ring for endpoints which is like we can configure setting for call forwarding in lync client. If the user doesn’t pick up for a few sec and then want to route to UCMA app.
    Is it possible to detect the duration of ring or respond time using MSPL script before routing to UCMA app? Thanks in advance.

  23. 23 Thomas said at 8:43 am on January 22nd, 2015:

    Hi, could you look at a question I posted at stack overflow? 🙂

    Do I have to call ProxyRequest() after i call DispatchNotification when proxyByDefault=true?

    link: http://stackoverflow.com/questions/28090178/why-do-i-get-an-mspl-exception-proxyrequest-only-valid-for-siprequest

  24. 24 Jan said at 7:50 am on May 8th, 2015:

    Hi, we are curently working on an UCMA app. A MPSL sends invites of incoming calls to our app where calls are beeing routed to specific Lync endpoints. We now would like to extend this app that incoming calls also can be routed to non Lync users through the gateway, out to the PSTN.
    But we are not able to route calls through the gateway using the following code:

    requestArgs.Request.SimpleProxy = true;
    requestArgs.ServerTransaction.EnableForking = true;
    requestArgs.Request.Retarget(targetUri);
    }
    requestArgs.ServerTransaction.CreateBranch().SendRequest(requestArgs.Request);
    }

    How can calls be routed to the PSTN?

    Regards,
    Jan

  25. 25 Shaaap said at 5:01 am on October 14th, 2015:

    Hi,
    Is there a way to record outbound calls server side? What should the MSLP script look like? I’ve got Recorder example from SDK running, but it is client application. We have skype for Business Server 2015


Leave a Reply

  • Note: Comment moderation is in use because of excessive spam. Your comment may not appear immediately.

  •