Developer Guide

Introduction

Getting your kit up and running

drachtio-srf (the 'srf' stands for Signaling Resource Framework) is the npm module that you will add to your package.json to build SIP server applications using drachtio.

drachtio-srf works in concert with a drachtio server process to control and manage SIP calls and events. So you will need a running instance of a drachtio server somewhere to connect to in order to start developing.

You can find instructions for building a drachtio server from scratch here, or if you prefer ansible you can find an ansible role here, but the easiest way to get started is probably to run a docker image.

Review the drachtio server docs for detailed information on configuring the server.

Notes: The sample code below assumes that a drachtio server process is running on the localhost and is listening for connections from applications on port 9021 (tcp).

Let's do something simple

Let's write a simple app that receives an INVITE and responds with a 486 status with a custom reason.

First, create a new application and add drachtio-srf as a dependency:

$ mkdir reject-nice && cd $_
$ npm init
...follow prompts, enter 'app.js' for entry point

$ touch app.js
$ npm install --save drachtio-srf

Next, make your app.js to look like this:

const Srf = require('drachtio-srf');
const srf = new Srf();

srf.connect({
  host: '127.0.0.1',
  port: 9021,
  secret: 'cymru'
});

srf.on('connect', (err, hostport) => {
  console.log(`connected to a drachtio server listening on: ${hostport}`);
});

srf.invite((req, res) => {
  res.send(486, 'So sorry, busy right now', {
    headers: {
      'X-Custom-Header': 'because why not?'
    }
  });
});

Now start your drachtio server or docker image -- in the example above the drachtio server is running locally and listening on the localhost address with default port and secret.

Once the drachtio server is running, start your app and verify it connects:

$ node app.js 
connected to a drachtio server listening on: tcp/[::1]:5060,udp/[::1]:5060, \
tcp/127.0.0.1:5060,udp/127.0.0.1:5060,tcp/192.168.200.135:5060,udp/192.168.200.135:5060

Now fire up a sip client of some kind (e.g. Bria, Blink, or other), point it at the address your drachtio server is listening on, and place a call.

If everything is communicating properly, the call will get rejected with the reason above and in the drachtio log you should see the SIP trace, including the generated response:

2018-05-05 13:31:02.879056 send 358 bytes to udp/[127.0.0.1]:57296 at 13:31:02.878925:
SIP/2.0 486 So sorry, busy right now
Via: SIP/2.0/UDP 127.0.0.1:57296;branch=z9hG4bK-524287-1---de4c69061049b867;rport=57296
From: <sip:dhorton@sip.drachtio.org>;tag=5fac7d01
To: <sip:22@sip.drachtio.org>;tag=KjH30DtKFKXcQ
Call-ID: 89373MWI0ODM1YTc2MTc2NThlZDE0MTU1YmRmNDY5OTk0NzM
CSeq: 1 INVITE
Content-Length: 0
X-Custom-Header: because why not?

What did we just do?

OK, so rejecting an incoming call is not particularly exciting, but the main thing we just accomplished was to verify that we have a working drachtio server, and also we illustrated how to connect an application to a drachtio server.

The type of connection made in our example above is called an inbound connection; that is, a TCP connection made from the nodejs application acting as a client to the drachtio server process acting as the server. There is also the possibility of having the drachtio server make an outbound connection to a listening application, but that is a more advanced topic we will cover later, along with the reasons on why you might want to do that.

By default, the drachtio server process listens for inbound connections on tcp port 9021, but this can be configured to a different port in its configuration file. Authentication is currently performed using a simple plaintext secret, which is also configured in the drachtio server configuration file.

In the example above, we listened for the 'connect' event on the srf object. However, it is a best practice to also listen for the error event, e.g.:

srf
  .on('connect', (err, hostport) => {
    console.log(`connected to a drachtio server listening on: ${hostport}`);
  })
  .on('error', (err) => {
    console.log(`Error connecting to drachtio server: ${err}`);
  });

The reason for this is that if (and only if) your app has an error handler on the srf instance, the framework will automatically try to reconnect any time the connection is lost, which is generally what you want in production scenarios.

Pro tip: always have an error handler on your Srf instance when using inbound connections, so your application will automatically reconnect to the server if the tcp connection is dropped.

Where did those other SIP headers come from?

Notice that although our application only provided one SIP header (a custom 'X-' header), the response actually sent by the drachtio server was a normal, fully-formed SIP response.

This is because the drachtio server process does a lot of the heavy lifting for us when it comes to managing the low-level SIP messaging. Our applications generally do not need to specify values for the common SIP headers, unless for some reason we want to override the behavior of the drachtio server.

By the way, the custom header was, of course, not really necessary and was only done for illustrative purposes in the example above. Neither, for that matter, was the SIP reason we provided: we could have simply sent a standard SIP/2.0 486 Busy Here with the following line of code:

res.send(486);

And, by the way, we are not limited to adding custom SIP headers to our messages -- we can add standard SIP headers in the same way:

res.send(486, {
  headers, {
    'Subject' : 'my first app'
  }
});

Middleware

drachtio-srf is a middleware framework. As we saw above, we handle SIP INVITEs using srf.invite(handler) where our handler function is invoked with (req, res) and the arguments provided are objects that represent the incoming SIP request and the SIP response the application will send, respectively.

All of the SIP methods are routed similarly, e.g.

srf.register((req, res) => {...handle REGISTERs});

srf.options((req, res) => {...handle OPTIONS});

srf.subscribe((req, res) => {...handle SUBSCRIBE}); //...etc

drachtio middleware can also be installed via the .use method. The middleware can be globally applied to all requests, or can be scoped by method. Below is an example where we use global middleware to log all requests, and a second middleware that parses authentication credentials from incoming REGISTER requests.

const Srf = require('drachtio-srf');
const srf = new Srf();
const registrationParser = require('drachtio-mw-registration-parser');

srf.use((req, res, next) => console.log(`incoming ${req.method from ${req.source_address}}`));
srf.use('register', registrationParser);

srf.register((req, res) => {
  // middleware has populated req.registration
  console.log(`registration info: ${req.registration});

    // {
    //    type: 'register' or 'unregister'
    //    expires: expires value in either Contact or Expires header
    //    contact: sip contact / address to send requests to
    //    aor: address-of-record being registered
    // } ;

});

Example middleware include:

SIP Messages

In the examples above, we've seen the callback signature (req, res) through which we are passed objects representing a SIP request and an associated response. These objects are event emitters and have some useful properties and methods attached. Since we will be interacting with these objects a lot when writing applications, let's review them now.

Properties, methods and events

The following properties are available on both req and res objects:

  • type: 'request' or 'response'
  • body: the SIP message body, if any
  • payload: an array of content, useful mainly if the message included multipart content. Each object in the payload array has a type and content property, containing the Content-Type header and the associated content, respectively
  • source: 'network' or 'application'; the sender of the message
  • source_address: the IP address of the sender
  • source_port: the source port of the sender
  • protocol: the transport protocol being used (e.g., 'udp', 'tcp')
  • stackTime: the time the message was sent or received by the drachtio server sip stack
  • calledNumber: the phone number (if any) parsed from the user part of the request uri
  • callingNumber: the phone number (if any) of the calling party, parsed from the P-Asserted-Identity header if it exists, otherwise from the From header.
  • raw: a string containing the full, unparsed SIP message

The following methods are available on both req and res objects as well:

  • has(name): returns true if the message includes the specified header
  • get(name): returns the value of a specified SIP header
  • set(name, value): sets the value of a specified SIP header
  • getParsedHeader(name): returns an object that represents the specified SIP header parsed into components

    Request-specific properties, methods and events

The req object additionally has the following properties:

  • method: the SIP method of the request

the following methods:

  • isNewInvite(): returns true if the request is a new INVITE (vs a re-INVITE, or a non-INVITE request)
  • cancel(callback): cancels an INVITE request that was sent by the application
  • proxy(opts, callback): proxies an incoming request. While this method is available, the preferred usage is to call srf.proxyRequest() instead.

and emits the following events:

  • cancel: this event is emitted for an incoming INVITE request, when a CANCEL for that INVITE is subsequently received from the sender.
  • response: when an application sends a SIP request, an application can listen for the 'response' event to obtain the matching SIP response that is received.

    Response-specific properties, methods and events

The res object additionally has the following properties:

  • status: the SIP response status, as an integer (alias: statusCode)
  • reason: the SIP reason (e.g. 'Busy Here')
  • finalResponseSent: true if the response message has been sent (alias: headersSent)

and the following methods:

  • send(status, reason, opts, callback): we have already seen this method used to send a response. Only the status parameter is required. The callback, if provided, will be invoked with the signature (err, msg) where the msg parameter will contain a representation of the SIP response message sent out over the wire.

Usage patterns

In the few sample code snippets we've looked at so far, we have been receiving SIP requests and then sending SIP responses in return.

However, we can also do the reverse -- send out a SIP request and receive a response back. In either case, we are dealing with the request and response objects described above, but different methods and events may apply. Below some of the common patterns are covered.

Receiving a request and sending a response

srf.options((req, res) => {
  res.send(200);
});

Sending a response with headers

srf.options((req, res) => {
  res.send(200, {
    headers: {
      'Subject': 'All\'s well here'
    }
  });
});

Sending a response with callback to get the msg actually sent

srf.options((req, res) => {
  res.send(200, {
    headers: {
      'Subject': 'All\'s well here'
    }
  }, (err, msg) => {
    const to = msg.getParsedHeader('To');
    console.log(`drachtio server added tag on To header: ${to.params.tag}`);
  });
});

Sending a request, and then receiving the response

srf.request('sip:1234@example.com', {
  method: 'OPTIONS'
}, (err, req) => {
  // req is the SIP request that went out over the wire
  req.on('response', (res) => {
    console.log(`received ${res.status} response to our OPTIONS request`);
  });
});

Sending a request with headers and body

const dtmf = 
`Signal=5
Duration=160`;

srf.request('sip:1234@example.com', {
  method: 'INFO', 
  headers: {
    'Content-Type': 'application/dtmf-relay'
  },
  body: dtmf
});

Handling the cancel of an INVITE

srf.invite((req, res) => {
  let canceled = false;

  req.on('cancel', () => canceled = true);

  doLengthyDatabaseLookup()
    .then((results) => {

      // was the call canceled while 
      // we were doing database lookup?

      if (canceled) return;

      ..go on to process the call
  })
})

SIP Dialogs

Conceptually, a SIP dialog is defined in RFC 3261 as a relationship between two SIP endpoints that persists for a period time. Generally speaking, we are referring most often to a multimedia call initiated by a SIP INVITE transaction during which audio and/or video is exchanged. (Later we will discuss an alternative type of dialog created by a SUBSCRIBE transaction).

Within drachtio, A SIP dialog is an object that is created to represent a multimedia session and to allow a developer to manage such sessions: to create them, modify them, and tear them down.

The examples we have shown till now have illustrated how to manage SIP interactions at the SIP message level. However, in most cases the drachtio SIP Dialog API provides a higher-level abstraction that makes it easier for developers to manage sessions.

User Roles in a Dialog

Some basic terminology is going to be helpful before diving into the API and some examples.

A drachtio application can:

  • initiate an INVITE transaction -- in which case we say it is acting as a 'User Agent Client', or UAC,
  • respond to an incoming INVITE transaction -- in which case we call it a 'User Agent Server', or UAS, and/or
  • both receive and send an INVITE transaction -- in which case we call it a 'back to back User Agent', or B2BUA

By the way, these are not the only types of applications we can build with drachtio. We can also build:

  • a registar -- which is an application that responds to REGISTER requests
  • a presence server -- which is an application that responds to SUBSCRIBE requests
  • an instant messaging server -- which responds to MESSAGE requests (see here for an example of a presence and messaging server)
  • a sip proxy -- which is an application that routes and forwards SIP requests

Quite frequently, though, we will find ourselves wanting to build some form of SIP User Agent application, and that is what we will cover in this section.

The dialog API consists of the methods

Each of these methods produces a Dialog object when a session is successfully established, which is returned via a callback (if provided) and resolves the Promise returned by each of the above methods.

Before we review the above three methods, let's examine the Dialog class itself, and how to work with it.

Dialog

The Dialog class is an event emitter, and has the following properties, methods, and events.

properties

  • sip: an object containing properties that identify the SIP dialog
  • sip.callId: the SIP Call-Id associated with this dialog
  • sip.remoteTag: the remote tag associated with the dialog
  • sip.localTag: the local tag associated with the dialog
  • local: an object containing properties associated with the local end of the dialog
  • local.sdp: the local session description protocol
  • local.uri: the local sip uri
  • local.contact: the local contact
  • remote: an object containing properties associated with the remote end of the dialog
  • remote.sdp: the remote session description protocol
  • remote.uri: the remote sip uri
  • remote.contact: the local contact
  • id: a unique identifier for the dialog with the drachtio framework
  • dialogType: either 'INVITE' or 'SUBSCRIBE'

methods

  • destroy(opts, callback): terminates a dialog, by sending a BYE (in the case of an INVITE dialog), or a NOTIFY with Subscription-State: terminated (in the case of a SUBSCRIBE dialog).
  • modify(sdp, callback): modifies a SIP INVITE dialog by putting it on or off hold or re-INVITing to a new session description protocol.
  • request(opts, callback): sends a SIP request within a dialog.

events

  • destroy(req): emitted when the remote end has terminated the dialog. The req parameter represents the SIP request sent from the remote end to terminate the dialog. (Note: no action is required by the application, as the drachtio server will have sent a 200 OK to the request).
  • hold(req): emitted when the remote end has placed the call on hold. The req parameter represents the INVITE on hold sent. (Note: no action is required by the application, as the drachtio server will have sent a 200 OK to the request).
  • modify(req,res): emitted when the remote end has sent a re-INVITE with a changed session description protocol. The application must respond, using the res parameter provided.
  • refresh(req): emitted when the remote end has sent a refreshing re-INVITE. (Note: no action is required by the application, as the drachtio server will have sent a 200 OK to the request).
  • unhold(req): emitted when the remote end has taken the call off hold. The req parameter represents the INVITE off hold sent. (Note: no action is required by the application, as the drachtio server will have sent a 200 OK to the request).
  • info(req, res)
  • messsage(req, res)
  • options(req, res)
  • publish(req, res)
  • refer(req, res)
  • subscribe(req, res)
  • update(req, res): these and the above events are emitted when a SIP request of the specified type is received within a dialog. The application must respond, using the res parameter provided. If the application does not have a listener registered for the event, then the drachtio server will automatically respond with a 200 OK.

Pro tip: while there are many operations you might want to perform on a Dialog object, the one thing you should always do is to listen for the 'destroy' event. You should attach a listener for 'destroy' whenever you create a new dialog. This will tell you when the remote side has hung up, and after this you will no longer be able to operate on the dialog.

UAS

Whew! With that background under our belt we can finally get to the meat of the matter -- creating and managing calls.

When we receive an incoming call and connect it to an IVR, or to a conference bridge, our application is acting as a UAS. Let's look at these scenarios first.

The one piece of information we need when acting as a UAS is the session description protocol (sdp) that we want to offer in our 200 OK. Creating media endpoints is outside the scope of drachtio-srf, so the examples below assume that our application has obtained them through other means.

Note: check out drachtio-fsmrf, which is a npm module that can be used with drachtio-srf to control media resources on a Freeswitch media server in order to provide IVR and conferencing features to drachtio applications.

srf.invite((req, res) => {
  let sdp = 'some-session-description-protocol'
  srf.createUAS(req, res, {
    localSdp: sdp  
  })
    .then((dialog) => {
      console.log('successfully created UAS dialog');
      dialog.on('destroy', () => console.log('remote party hung up'));
    })
    .catch((err) => {
      console.log(`Error creating UAS dialog: ${err}`);
    }) ;
});

In the example above, the local sdp is provided as a string, but we can alternatively provide a function that returns a Promise which resolves to a string value representing the session description protocol. This is useful when we have to perform some sort of asynchronous operation to obtain the sdp.

function getMySdp() {
  return doSomeNetworkOperation()
    .then((results) => {
      return results.sdp;
    });
}
srf.invite((req, res) => {
  let sdp = 'some-session-description-protocol'
  srf.createUAS(req, res, {
    localSdp: getMySdp
  })
    .then((dialog) => { .. })
    .catch((err) => { .. });
});

Of course, we can supply SIP headers in the usual manner:

srf.invite((req, res) => {
  srf.createUAS(req, res, {
    localSdp: sdp, 
    headers: {
     'User-Agent': 'drachtio/iechyd-da',
     'X-Linked-UUID': '1e2587c'
    }
  })
    .then((dialog) => { .. });
});

If Srf#createUAS fails to create a dialog for some reason, an error object will be returned via either callback or the Promise. If the failure is due to a SIP non-success status code, then a SipError will be returned. In the UAS scenario, the only time this will happen is if the call is canceled by the caller before we answer it, in which case a '487 Request Terminated' will be the final SIP status.

srf.invite((req, res) => {
  srf.createUAS(req, res, {
    localSdp: sdp, 
    headers: {
     'User-Agent': 'drachtio/iechyd-da',
     'X-Linked-UUID': '1e2587c'
    }
  })
    .then((dialog) => { .. })
    .catch((err) => {
      if (err instanceof Srf.SipError && err.status === 487) {
        console.log('call canceled by caller');
      }
    })

Finally, as noted above, Srf#createUAS can be invoked with a callback as an alternative to Promises. In most of the examples in this document we will use Promises, but an example of using a callback is presented below.

srf.invite((req, res) => {
  let sdp = 'some-session-description-protocol'
  srf.createUAS(req, res, {
    localSdp: sdp  
  }, (err, dialog) => {
    if (err) console.log(`Error creating UAS dialog: ${err}`);
    else {
      console.log('successfully created UAS dialog');
      dialog.on('destroy', () => console.log('remote party hung up'));
    }
  });
});

UAC

When we initiate a dialog by sending an INVITE, we are acting as a UAC. We use the Srf#createUAC method to accomplish this. Just as with Srf#createUAS, either a callback approach or a Promise-based approach is supported.

In the simplest example, we can provide only the Request-URI we want to send to, and the session description protocol we are offering in the INVITE in that example:

let mySdp; // populated somehow with SDP we want to offer
srf.createUac('sip:1234@10.10.100.1', {localSdp: mySdp})
  .then((dialog) => {
    dialog.on('destroy', () => console.log('called party ended call'));
  })
  .catch((err) => {
    console.log(`call failed with ${err});
  });

Pro tip: we can specify the Request-Uri either a full sip uri, as above, or simply provide an ip address (or ip address:port, if we want to send to a non-default SIP port).

What did drachtio server do in all this?

In the example above, we supplied only the Request-URI and body of the INVITE, so the drachtio server must have done quite a bit in terms of filling out the rest of the message and managing various aspects of the transaction.

In fact, the drachtio server would have done all of the following to create the outgoing INVITE:

  • generated a (unique) SIP Call-ID and a CSeq
  • generated the appropriate Via header (pro tip: never provide a Via: header, always let drachtio generate that)
  • generated the From header (with no user element in the URI), and generated a From tag
  • generated the To header (according to the Request-URI we provided)
  • generated a Contact header, based on the Request-URI and the address:port(s) that the server is listening on for SIP messages
  • examined the body that we provided and -- seeing that it was an sdp -- created a Content-Type of 'application/sdp'
  • calculated the Content-Length header (pro tip: never provide a Content-Length: header, always let drachtio generate that)

It would have then sent the INVITE, and managed any provisional and final responses. It would have generated the final ACK request as well. If a reliable provisional response were received, it would have responded with the required PRACK request.

Beyond this basic usage, there are several other common patterns. Let's look at some of them.

Receiving provisional responses

When we send out an INVITE, we may get some provisional (1XX) responses back before we get a final response. In the example above, we did not care to do anything with these provisional responses, but if we want to receive them we can add a callback that will be invoked when we receive a provisional response. This callback, named cbProvisional, along with another we will describe shortly, are provided in an optional Object parameter as shown below.

srf.createUac('sip:1234@10.10.100.1', {localSdp: mySdp}, {
    cbProvisional: (res) => console.log(`got provisional response: ${res.status}`))
  })
  .then((dialog) => {
    dialog.on('destroy', () => console.log('called party ended call'));
  })
  .catch((err) => {
    console.log(`call failed with ${err.status}`);
  });

Note: if the INVITE fails, a SipError object will be returned, and the final SIP non-success status can be retrieved from err.status as shown above.

Canceling an INVITE we sent

Sometimes, we may want to cancel an INVITE that we have sent before it is answered. Related to this, we may simply want to get access to details of the actual INVITE that was sent out over the wire. To do this, we can provide a cbRequest callback in the callback object mentioned above. This callback receives a req object representing the INVITE that was sent over the wire. If we later want to cancel the INVITE, we simply call req.cancel().

let invite, dlg;
srf.createUac('sip:1234@10.10.100.1', {localSdp: mySdp}, {
    cbRequest: (err, req) => invite = req),
    cbProvisional: (res) => console.log(`got provisional response: ${res.status}`))
  })
  .then((dialog) => {
    dlg = dialog
    dialog.on('destroy', () => console.log('called party ended call'));
  })
  .catch((err) => {
    console.log(`call failed with ${err}`);
  });

  someEmitter.on('some-event', () => {
    // something happened to make us want to cancel the call
    if (!dlg && invite) invite.cancel();
  });

Authentication

If we send an INVITE that is challenged (with either a 401 Unauthorized or a 407 Proxy Authentication Required), we can have the drachtio framework handle this if we provide the username and password in the opts.auth parameter:

let mySdp; // populated somehow with SDP we want to offer
srf.createUac('sip:1234@10.10.100.1', {
  localSdp: mySdp,
  auth: {
    username: 'dhorton',
    password: 'foobar'
  }
})
  .then((dialog) => {
    dialog.on('destroy', () => console.log('called party ended call'));
  })
  .catch((err) => {
    console.log(`call failed with ${err});
  });

Sending through an outbound SIP proxy

If we want the INVITE to be sent through an outbound SIP proxy, rather than directly to the endpoint specified in the Request-URI, we can specify an opts.proxy parameter:

let mySdp; // populated somehow with SDP we want to offer
srf.createUac('sip:1234@10.10.100.1', {
  localSdp: mySdp,
  proxy: 'sip:proxy.enterprise.com'
})
  .then((dialog) => {
    dialog.on('destroy', () => console.log('called party ended call'));
  })
  .catch((err) => {
    console.log(`call failed with ${err});
  });

Specifying calling number or called number

If we want to specify the calling party number to use in the From header of the INVITE, and/or the called party number to use in the To header as well as the Request-URI, we can do so simply like this:

let mySdp; // populated somehow with SDP we want to offer
srf.createUac('sip:1234@10.10.100.1', {
  localSdp: mySdp,
  callingNumber: '+18584083089',
  calledNumber: '+15083345988'
})
  .then((dialog) => {
    dialog.on('destroy', () => console.log('called party ended call'));
  })
  .catch((err) => {
    console.log(`call failed with ${err});
  });

Pro tip: Where possible, use this approach of providing 'opts.callingNumber' rather than trying to provide a full From header.

Pro tip: If you really want to provide the full From header, for the host part of the uri use the string 'localhost'. The drachtio server will handle this by replacing the host value with the proper IP address for the server.

Sending a 3PCC INVITE

A scenario known as third-party call control (3PCC) occurs when a UAC sends an INVITE with no body -- i.e., no session description protocol is initially offered. In this call flow, after the offer is received in the 200 OK response from the B party, the UAC sends its sdp in the ACK to establish the media flows.

To accomplish this, the Srf#createUAC can be used. However, because of the need to specify an SDP in the ACK, the application must take additional responsibility for generating the ACK.

Furthermore, instead of delivering a Dialog, the Promise (or callback) will render an object containing two properties:

  • sdp: the sdp received in the 200 OK from the B party, and
  • ack: a function that the application must call, providing the SDP to be included in the ACK as the first parameter. The function returns a Promise that resolves to the Dialog created by the ACK.

With that as background, let's see an example:

srf.createUac('sip:1234@10.10.100.1', {
  callingNumber: '+18584083089',
  calledNumber: '+15083345988',
  noAck: true
})
  .then((obj) => {
    console.log(`received sdp offer ${obj.sdp} from B party`);
    let mySdp = allocateSdpSomehow();
    return obj.ack(mySdp);
  })
  .then((dialog) => {
    dialog.on('destroy', () => console.log('called party ended call'));  
  })
  .catch((err) => {
    console.log(`call failed with ${err});
  });

Note a few things in this example:

  • There was no opts.body parameter in the call to Srf#createUAC: the absence of a body signals the drachtio framework that we are intending a 3PCC scenario.
  • The use of opts.noAck indicates that we do not want the framework to generate the ACK for us, and that instead we will explicitly call the obj.ack() function to take responsibility for that.

There is also a fairly common alternative 3PCC special case where you may want to offer a "null" SDP in the ACK, i.e. creating an initially inactive media stream that you will later activate with a re-INVITE. In that case, you can simply remove the opts.noAck parameter and the Promise/callback will deliver the completed dialog as in the normal case -- the framework will generate the appropriate "null" sdp and generate the ACK for you.

srf.createUac('sip:1234@10.10.100.1', {
  callingNumber: '+18584083089',
  calledNumber: '+15083345988'
})
  .then((dialog) => {
    console.log(`created dialog with inactive media`);
    dialog.on('destroy', () => console.log('called party ended call'));  
  })
  .catch((err) => {
    console.log(`call failed with ${err});
  });

B2BUA

When we receive an incoming INVITE (the A leg), and then send a new outgoing INVITE on a different SIP transaction (the B leg), we are acting as a back to back user agent. We use the Srf#createB2BUA method to accomplish this. Once again, either a callback approach or a Promise-based approach is supported.

As with the UAC scenario, the simplest usage is to provide the Request-URI to send the B leg to and the sdp to offer on the B leg. If successful, our application receives two SIP dialogs: a UAS dialog (the A leg) and a UAC dialog (the B leg);

srf.invite((req, res) => {
  srf.createB2BUA(req, res, 'sip:1234@10.10.100.1', {localSdpB: req.body})
    .then({uas, uac} => {
      console.log('call successfully connected');

      // when one side hangs up, we hang up the other
      uas.on('destroy', () => uac.destroy());
      uac.on('destroy', () => uas.destroy());
    })
    .catch((err) => console.log(`call failed to connect: ${err}`));
});

Beyond this simple example, there are many options. Let's look at some of them:

Copying headers from the A leg onto the B leg

It's quite common for us to want to include on the B leg INVITE some of the headers that we received on the A leg; vice-versa, we may want to include on the A leg response some of the headers that we received on the B leg response.

This can be achieved with the opts.proxyRequestHeaders and the opts.proxyResponseHeaders properties in the optional opts parameter. If provided, these should include an array of header names that should be copied from one to the other.

The example below illustrates a B2BUA app that wants to pass authentication headers between endpoints

srf.invite((req, res) => {
  srf.createB2BUA(req, res, 'sip:1234@10.10.100.1' {
    localSdpB: req.body,
    proxyRequestHeaders: ['Proxy-Authorization', 'Authorization'],
    proxyResponseHeaders: ['WWW-Authenticate', 'Proxy-Authentication']
  })
    .then({uas, uac} => {
      console.log('call successfully connected');

      // when one side hangs up, we hang up the other
      uas.on('destroy', () => uac.destroy());
      uac.on('destroy', () => uas.destroy());
    })
    .catch((err) => console.log(`call failed to connect: ${err}`));
});

Obtaining the UAC dialog as soon as possible

When the Srf#createB2BUA completes successfully, it provides us the two dialog that have been established.

However, in rare cases it may be desirable to receive the UAC dialog as soon as it is established -- that is, as soon as we have received a 200 OK from the B party, before we have sent the 200 OK back to the A party, and before the Srf#createB2BUA](/docs/api#Srf+createB2BUA) method has resolved the Promise that it returns.

For this need, similar to Srf#createUAC, there is an optional callback object that contains a callback named cbFinalizedUac that, if provided, is invoked with the UAC dialog as soon as it is created. (Note: the cbProvisional and cbRequest callbacks are also available).

srf.invite((req, res) => {
  srf.createB2BUA(req, res, 'sip:1234@10.10.100.1', {
    localSdpB: req.body
  }, {
    cbFinalizedUac: (uac) => {
      console.log(`successfully connected to B party at ${uac.remote.contact}`);
    }
  })
    .then({uas, uac} => {
      console.log('call successfully connected');

      // when one side hangs up, we hang up the other
      uas.on('destroy', () => uac.destroy());
      uac.on('destroy', () => uas.destroy());
    })
    .catch((err) => console.log(`call failed to connect: ${err}`));
});

Modifying SDP offered to A leg in SIP response

By default, Srf#createUAC will respond with a 200 OK to the A leg INVITE with the sdp that it received from the B party in the 200 OK on the B leg. In other words, it simply proxies the session description protocol offer from B back to A.

Sometimes, however, it is desirable to transform or modify the SDP received on the B leg before sending it back on the A leg. For this purpose, the opts.localSdpA parameter is available. This parameter can either be a string, containing the sdp to offer in the 200 OK back to the A leg, or it can be a function returning a Promise that resolves to the sdp to return in the 200 OK to the A leg. The function has the signature (sdpB, res), where sdpB is the session description offer we received in the 200 OK from the B party, and res is the response object we received on the B leg.

Rather than include an example here, please refer to the source code for the drachtio-b2b-media-proxy application, which is a simple B2BUA that uses rtpengine to proxy the media. This is a great example of when you would want to transform the SDP from B before returning the final session description offer to A.

Choosing not to propagate failure from B leg

By default, if we get a final SIP non-success from the B party it will be propagated back to the A party. There are times where we would prefer not to do so; for instance, if having failed to connect the A party to one endpoint or phone number, we would now wish to try another.

Setting opts.passFailure to value of false enables this behavior.

srf.invite((req, res) => {
  srf.createB2BUA(req, res, 'sip:1234@10.10.100.1', {localSdpB: req.body, passFailure: false})
    .then({uas, uac} => {
      console.log('call connected to primary destination');
    })
    .catch((err) => {
      // try backup if we got a sip non-success response and the caller did not hang up
      if (err.status !== 487) {
        console.log(`failed connecting to primary, will try backup: ${err}`);
        srf.createB2BUA(req, res, 'sip:1234@10.10.100.2', {
          localSdpB: req.body}
        })
        .then({uas, uac} => {
          console.log('call connected to backup destination');
        })
        catch((err) => {
          console.log(`failed connecting to backup uri: ${err}`);
        });
      }
    });
  });

Note that we had to check that the reason for the failure connecting our first attempt was not a 487 Request Cancelled, because this is the error we receive when the caller (A party) hung up before we connected the B party. In that case, we no longer would want to attempt a backup destination.

This also answers a related question you may have had: what happens when the A part hangs up before connected, and does our app need to do anything specifically to cancel the B leg when the A leg cancels? The answer to the latter is no, the drachtio framework will automatically cancel the B leg if the A leg is canceled.

Subscribe Dialogs

SUBSCRIBE requests also establish SIP Dialogs, as per RFC 3265.

A UAC (the subscriber) sends a SUBSCRIBE request with an Event header indicating the event that is being subscribed to, and the UAS (the notifier) responds with a 202 Accepted response. The UAS should then immediately send a NOTIFY to the UAC of the current state of the requested resource. To terminate a SUBSCRIBE dialog, the UAS sends a NOTIFY request with Subscription-State: terminated; while a UAC would send another SUBSCRIBE with an Expires: 0.

The dialog produced by the Srf#createUAS method will be a SUBSCRIBE dialog if the request was a SUBSCRIBE. While the Srf#createUAS method call will send a 202 Accepted, it does not send the initial NOTIFY request that should follow -- the application must do that, since the content can only be determined by the application itself.

srf.subscribe((req, res) => {
  srf.createUAS(req, res, {
    {
      headers: {'Expires': req.get('Expires')
    }  
  })
    .then((dialog) => {
      dialog.on('destroy', () => console.log('remote party terminated dialog'));

      // send initial NOTIFY
      let myContent = 'some content reflecting current resource state..';

      return dialog.request({
        method: 'NOTIFY',
        headers: {
          'Subscription-State': 'active',
          'Event': req.get('Event'),
          'Content-Type': 'application/pidf+xml' // or whatever
        },
        body: myContent
      });
    });
});

Pro tip: If you need to query a dialog to see whether it is an INVITE or a SUBSCRIBE dialog, you can use the dialogType (read-only) property of the Dialog object to determine that.

The Srf#createUAC method can also be used to generate a SUBSCRIBE dialog as a UAC/subscriber. To do this, specify opts.method should be set to 'SUBSCRIBE'.

srf.createUac('sip:resource@example.com', {
    method: 'SUBSCRIBE',
    headers: {
      'From': '<sip:user@locahost>',
      'Event': 'presence',
      'Expires': 3600
    }
  })
  .then((dialog) => {
    dialog.on('destroy', () => console.log('remote party ended dialog'));

    dialog.on('notify', (req, res) => {
      res.send(200);

      console.log(`received NOTIFY for event ${req.get('Event')}`);
      if (req.body) {
        console.log(`received content of type ${req.get('Content-Type')}: ${req.body}`);
      }
    });
  });

Note in the example above the use of 'localhost' as the host part of the uri of the From header. As mentioned earlier, this will cause the drachtio server to replace this with the appropriate IP address of the server.

SIP Proxy

Building a SIP proxy with drachtio is pretty darn simple.

srf.invite((req, res) => {
  srf.proxyRequest(req, 'sip.example1.com')
    .then((results) => console.log(JSON.stringify(results)) );
});

In the example above, we receive an INVITE and then proxy it onwards to the server at 'sip.example1.com'.

Note: as with other methods, a callback variant is also available.

Srf#proxyRequest returns a Promise that resolves when the proxy transaction is complete -- i.e. final responses and ACKs have been transmitted, and the call is either connected or has resulted in a final non-success response. The results value that the Promise resolves provides a complete description of the results.

There are a bunch of options that we can utilize when proxying a call, but before we take a look at those let's consider the two fundamentally different proxy scenarios that we might encounter:

  1. The incoming INVITE has a Request-URI of the dractio server, and we want to proxy it on towards a next sip uri that we supply in the calll to Srf#proxyRequest. An example of this use case occurs when we have a drachtio server acting as a load balancer in front of an array of application or media servers.
  2. The incoming INVITE has a Request-URI of a remote endpoint, and we want to proxy it on towards that endpoint. An example of this use case occurs when we have a drachtio server acting as an outbound SIP proxy.

How we handle these two scenarios is governed by whether we supply a sip uri in the call to Srf#proxyRequest. In the first example above, we supplied a sip uri in the method call and as a result the drachtio server will do the following:

  • If the incoming INVITE had a Request-URI that matches a local address that the drachtio server is listening on, then it will replace the Request-URI in the outbound INVITE to that specified in the method call and will forward the INVITE to that address.
  • Otherwise, the outbound INVITE will leave the Request-URI unchanged while forwarding the INVITE to the sip uri specified in the method call.

An implication of this is that we can call Srf#proxyRequest without specifying a sip uri at all; in this case, drachtio acts as an outbound proxy and forwards the INVITE towards the Request-URI of the incoming INVITE.

SIP proxy acting as a load balancer

srf.invite((req, res) => {
  srf.proxyRequest(req, ['sip.example1.com','sip2.example1.com]')
    .then((results) => console.log(JSON.stringify(results)) );
});

The above example illustrates that we can provide either a string or an Array of strings as the sip uri to proxy an INVITE to. In the latter case, if the INVITE fails on the first sip server it will then be attempted on the second, and so on until a successful response is received or the list is exhausted.

SIP outbound proxy

srf.invite((req, res) => {
  srf.proxyRequest(req)
    .then((results) => console.log(JSON.stringify(results)) );
});

In the above example there is no need to supply a sip uri if the drachtio server is acting as a simple outbound proxy.

SIP Proxy options

srf.invite((req, res) => {
  srf.proxyRequest(req, ['sip.example1.com','sip2.example1.com]', {
    recordRoute: true,
    followRedirects: true,
    forking: true,
    provisionalTimeout: '2s',
    finalTimeout: '18s'
  })
    .then((results) => console.log(JSON.stringify(results)) );
});

See Srf#proxyRequest for a detailed explanation of these options.

Call Detail Records

Generating call detail records (CDRs) is a standard requirement for SIP servers. The drachio-server process generates the following types of CDRs:

  • a call attempt record is generated when a call attempt (i.e., INVITE) is either received or generated by the server
  • a call start record is generated when a call is connected (i.e., 200 OK either sent or received), and
  • a call stop record is generated when a call ends or a non-successful INVITE transaction completes.

It follows from the above that a successful completed call will generate three CDRs (call attempt, call start, call stop), while a failed INVITE will only generate two CDRs (call attempt, call end).

An application registers to receive CDRs by registering for events as illustrated below.

const Srf = require('drachtio-srf');
const srf = new Srf();
const config = require('config');

srf.connect(config.get('drachtio'))
  .on('error', (err) => console.error(`Error connecting: ${err}`));


// register to receive CDRs
srf.on('cdr:attempt', (source, time, msg) => {
  console.log(`got attempt record from ${source} at ${time}: msg.get('Call-Id')`) ;

  // got attempt record from network at 20:05:58.130582: 671261870@42.55.72.99

});

srf.on('cdr:start', (source, time, role, msg) => {
  console.log(`got start record from ${source} at ${time} with role ${role}: msg.get('Call-Id')`) ;

  // got start record from network at 20:05:59.781505 with role uas: 671261870@42.55.72.99
});

srf.on('cdr:stop', (source, time, reason, msg) => {
  console.log(`got stop record from ${source} at ${time} with reason ${reason}: msg.get('Call-Id')`) ;

  // got stop record from network at 20:06:22.695850 with reason normal-release: 671261870@42.55.72.99
});

Note that it is completely possible to have one specialized application connecting and receiving CDRs, while other applications are performing the call control logic. This ability to separate CDR generation from application logic is often desirable in larger, more complex systems.

The cdr events provide the following information elements:

  • source: either 'network' or 'application', depending on whether the INVITE was received by or sent from the drachtio server, respectively.
  • time: the UTC time that the request was sent or received
  • role: the role that the drachtio server is playing with regards to this INVITE request:
    • 'uas': the server is receiving the INVITE as a UAS
    • 'uac': the server is generating the INVITE as a UAC
    • 'proxy-uac': the server is forwarding an INVITE as a proxy
    • 'proxy-uas': the server is receiving an INVITE as a proxy
  • reason: the termination reason for the call
    • 'call-rejected': the INVITE was rejected with a non-success final status
    • 'call-canceled': the INVITE was canceled by the sender before answer
    • 'normal-release': the call was connected and later released normally by one side or the other
    • 'session-expired': the call was connected and later torn down by the drachtio server because the session expired
  • msg:

    • for 'cdr:attempt', the INVITE request
    • for 'cdr:start', the 200 OK
    • for 'cdr:stop', the final non-success response (if reason is 'call-rejected'), otherwise the BYE request taht was sent or received to terminate the Dialog.

    Further information about the call, such as calling and called party numbers, can be retrieved from the msg parameter using the properties and methods of the SIP Message object.

    Note: an application that wishes to receive CDR events must establish an inbound connection to the drachtio server, since CDR events are not currently sent over outbound connections.

Advanced Topics

Outbound connections

The examples so far have illustrated inbound connections; that is, a drachtio application establishing a tcp connection to a drachtio server. These are created by calling Srf#connect:

const Srf = require('drachtio-srf');
const srf = new Srf();

// example of creating inbound connections
srf.connect({
  host: '192.168.1.100',
  port: 9022, 
  secret: 'cymru'
});

srf.on('connect', (hp) => console.log(`connected to drachtio listening on ${hp}`));
srf.on('error', (err) => console.log(`error connecting: ${err}`));

srf.invite((req, res) => {..});

An inbound connection is intended to be a long lasting connection: the application connects when the application starts, and that connection is then used to transmit all SIP events and commands as long as the application is running.

Note: If the connection between the drachtio client and the server is interrupted, it will be automatically reestablished as long as the application has installed a listener for the 'error' event on the Srf instance.

Inbound connections are generally most useful in scenarios when a drachtio server is single-purposed, meaning all SIP requests are handled by a single application. For example, if a drachtio server is specifically purposed to be a SIP proxy, and all incoming calls are treated by the same application logic, then an inbound connection between the drachtio application and server would probably be preferred.

However, it is also possible for the connections between the drachtio application and the server to be reversed: that is, the drachtio server establishes the connection to a drachtio server on a per-call (more specifically, a per SIP request) basis. This is called an outbound connection, and it requires two things:

  1. The drachtio server configuration file must include a request-handler xml section that maps a specific SIP method type to an HTTP web callback, and
  2. The drachtio application must call the Srf#listen method, which causes it to listen for connections from a drachtio server rather than initiate them.

The sequence of events when outbound connections have been enabled are as follows:

  1. The drachtio application starts, and begins listening on a specific IP address and port for connections from drachtio servers.
  2. An incoming INVITE request (for example) is received by a drachtio server.
  3. Because the drachtio server has been configured with a web callback for INVITE request types, an HTTP GET request is made to the web callback. Information about the incoming call is passed to the web callback via url parameters in the request.
  4. The web callback -- which is a user-supplied web app -- returns a JSON body indicating the ip address or dns name and tcp port where the drachtio application is listening.
  5. The drachtio server retrieves the ip address and port from the response to the web callback and establishes a tcp connection to the drachtio application listening on that address:port.
  6. The INVITE information is sent to the drachtio application over this new connection, and the standard drachtio middleware is invoked; e.g. srf.invite((req, res)).
const Srf = require('drachtio-srf');
const srf = new Srf();

// example of listening for outbound connections
srf.listen({
  port: 3001, 
  secret: 'cymru'
});

srf.invite((req, res) => {..});

From the standpoint of the drachtio application you would write, the code is almost exactly the same other than the call to Srf#listen instead of Srf#connect and one other matter related to eventually releasing the connection, which we will describe shortly.

Mixing inbound and outbound connections

Is it possible to mix inbound and outbound connections?

Sort of. Here are the limitations:

  1. A drachtio server can have both inbound and outbound connections, but a given SIP method type (e.g. INVITE) will exclusively use one or the other. Specifically, if there is a <request-handler> element with a sip-method property set to either the method name or *, then outbound connections will be used for all incoming SIP requests of that method type; otherwise inbound connections will be used.
  2. A single Srf instance must exclusively use only inbound or outbound connections; that is to say that it must call either Srf#connect or Srf#listen but not both. A single application that wants to use both must create two (or more) Srf instances, or alternatively the functionality can be split into multiple applications.

Terminating outbound connections

We mentioned above that inbound connections are long-lasting.

Outbound connections are not.

An outbound connection is established when a specific SIP request arrives, and it is intended to last only until the application has determined that all logic related to that request has been performed. From a practical standpoint, since each new arriving request spawns a new tcp connection, it is important that connections are destroyed when the application logic is complete, so that we don't exhaust file descriptor or other resources on the server.

Because the determination of "when all application logic has complete" is, by definition, something that only the application can know, we require the application to destroy the connection via an explicit call to Srf#endSession when it is no longer needed. Typically, an application will call this method when all SIP dialogs or transactions associated with or emanating from the initial SIP request have been destroyed.

In a simple example of a UAS app connecting an incoming call, for instance, when the BYE that terminates the call is sent or received it would be appropriate to call Srf#endSession.

This method call is a 'no-op' (does nothing) when called on an inbound connection, so it is safe to call in code that may be dealing with an outbound or inbound connection.

When to use outbound connections

There are two primary scenarios in which to use outbound connections:

  1. When a single drachtio server is going to handle calls controlled by multiple different types of applications. In this scenario, it can be useful to have a web callback examine the incoming calls and distribute them appropriately to the different drachtio applications based on per-call information. For example, if we wanted to stand up a drachtio server that multiple customers could utilize (i.e. multi-tenant situation) then outbound connections would allow us to have many different customer applications controlling calls on that server, each applying their own logic.
  2. When drachtio applications are going to run in a containerized cluster environment such as Kubernetes, outbound connections can be useful. In this environment, it can be useful to create a Kubernetes Service for the drachtio cluster, and then use outbound connections to route incoming calls to the public address of the Kubernetes Service which is backed by a cluster of drachtio applications running in Kubernetes pods.

In general, outbound connections can make it easier to independently scale drachtio servers and groups of drachtio applications, since you do not need to explicitly "tie" drachtio applications to specific servers.

For more information on configuring drachtio server for outbound connections refer to the drachtio server configuration documentation.

Using TLS to encrypt messages between application and server

As of drachtio server release 0.8.0-rc1 and drachtio-srf release 4.4.0, it is possible to encrypt the messages between the drachtio server and your application. This may be useful in situations where applications are running remotely and you prefer to encrypt the control messages as they pass through intervening networks. Both inbound and outbound connections can use TLS encryption, though the configuration steps are different as described below.

Securing inbound connections using TLS

To use TLS on inbound connections, simply configure the drachtio server to listen on a specific port for TLS traffic, in addition to (or in place of) TCP traffic. For example:

<admin port="9022" tls-port="9023" secret="cymru">127.0.0.1</admin>

would cause the server to listen for tcp connections on port 9022 and tls connections on port 9023.

You can also specify the port on the command line:

drachtio --tls-port 9023

In addition to specifying a port to listen for tls traffic, you must specify minimally a server key, a certificate, and a dhparam file. These are specified in the 'tls' section of the config file:

<tls>
    <key-file>/etc/letsencrypt/live/example.org/privkey.pem</key-file>
    <cert-file>/etc/letsencrypt/live/example.org/cert.pem</cert-file>
    <chain-file>/etc/letsencrypt/live/example.org/chain.pem</chain-file>
    <dh-param>/var/local/private/dh4096.pem</dh-param>
</tls>

Of course, these can also be specified via the command line as well:

drachtio --dh-param /var/local/private/dh4096.pem \
  --cert-file /etc/letsencrypt/live/example.org/cert.pem \
  --chain-file /etc/letsencrypt/live/example.org/chain.pem \
  --key-file /etc/letsencrypt/live/example.org/privkey.pem

drachtio-srf app configuration

On the client side, when connecting to a TLS port the Srf#connect function call must include a 'tls' object parameter in the options passed:

    this.srf.connect({
      host: '127.0.0.1',
      port: 9023, 
      tls: {
        rejectUnauthorized: false
      }
    });

Any of the node.js tls options that can be passed to tls.createSecureContext can be passed. Even if you do not need to include any options, you must still include an empty object as the opts.tls param in order to signal the underlying library that you wish to establish a TLS connection.

Using self-signed certificate on the server

If you are using a self-signed certificate on the server, then you must load that same certificate on the client, as below:

    this.srf.connect({
      host: '127.0.0.1',
      port: 9023, 
      tls: {
        ca: fs.readFileSync('server.crt'),
        rejectUnauthorized: false
      }
    });

Securing outbound connections using TLS

To use TLS to secure outbound connections, there is no specific configuration needed on the server. You just need your http request handler to return a uri with a transport=tls parameter, e.g.:

{"uri": "10.32.100.2:808;transport=tls"}

drachtio-srf configuration

On the application side, to listen for TLS connections you will need to modify the Srf#listen function to pass tls options. Minimally, you must specify a private key and certificate.

  srf.listen({
    port: 8080,
    tls: {
      key: fs.readFileSync('server.key'),
      cert: fs.readFileSync('server.crt'),
      rejectUnauthorized: false      
    }
  });