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 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?
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.
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'
}
});
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:
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.
The following properties are available on both req
and res
objects:
type
: 'request' or 'response'body
: the SIP message body, if anypayload
: 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 messagesource_address
: the IP address of the sendersource_port
: the source port of the senderprotocol
: the transport protocol being used (e.g., 'udp', 'tcp')stackTime
: the time the message was sent or received by the drachtio server sip stackcalledNumber
: the phone number (if any) parsed from the user part of the request uricallingNumber
: 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 messageThe following methods are available on both req
and res
objects as well:
has(name)
: returns true if the message includes the specified headerget(name)
: returns the value of a specified SIP headerset(name, value)
: sets the value of a specified SIP headergetParsedHeader(name)
: returns an object that represents the specified SIP header parsed into componentsThe req
object additionally has the following properties:
method
: the SIP method of the requestthe 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 applicationproxy(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.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.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.
srf.options((req, res) => {
res.send(200);
});
srf.options((req, res) => {
res.send(200, {
headers: {
'Subject': 'All\'s well here'
}
});
});
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}`);
});
});
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`);
});
});
const dtmf =
`Signal=5
Duration=160`;
srf.request('sip:1234@example.com', {
method: 'INFO',
headers: {
'Content-Type': 'application/dtmf-relay'
},
body: dtmf
});
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
})
})
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.
Some basic terminology is going to be helpful before diving into the API and some examples.
A drachtio application can:
By the way, these are not the only types of applications we can build with drachtio. We can also build:
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.
The Dialog class is an event emitter, and has the following properties, methods, and events.
sip
: an object containing properties that identify the SIP dialogsip.callId
: the SIP Call-Id associated with this dialogsip.remoteTag
: the remote tag associated with the dialogsip.localTag
: the local tag associated with the dialoglocal
: an object containing properties associated with the local end of the dialoglocal.sdp
: the local session description protocollocal.uri
: the local sip uri local.contact
: the local contactremote
: an object containing properties associated with the remote end of the dialogremote.sdp
: the remote session description protocolremote.uri
: the remote sip uri remote.contact
: the local contactid
: a unique identifier for the dialog with the drachtio frameworkdialogType
: either 'INVITE' or 'SUBSCRIBE'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).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).res
parameter provided.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).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.
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'));
}
});
});
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).
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:
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.
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.
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();
});
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});
});
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});
});
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.
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, andack
: 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:
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.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});
});
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:
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}`));
});
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}`));
});
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.
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 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.
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:
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:
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.
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.
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.
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.
Generating call detail records (CDRs) is a standard requirement for SIP servers. The drachio-server process generates the following types of CDRs:
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 receivedrole
: the role that the drachtio server is playing with regards to this INVITE request: reason
: the termination reason for the callmsg
:
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.
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:
The sequence of events when outbound connections have been enabled are as follows:
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.
Is it possible to mix inbound and outbound connections?
Sort of. Here are the limitations:
<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.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.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.
There are two primary scenarios in which to use outbound connections:
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.
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.
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
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.
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
}
});
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"}
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
}
});