const Emitter = require('events') ;
const assert = require('assert') ;
const only = require('only') ;
const methods = require('sip-methods') ;
const debug = require('debug')('drachtio-srf') ;
/**
* Arguments provided to Dialog constructor.
* @typedef {Object} Dialog~Options
* @property {Object} req drachtio Request (only provided when creating a 'uas' dialogs)
* @property {Object} res drachtio Response
* @property {Object} [sent] - uas only: actual Response sent when confirming the dialog
*/
/**
* Class representing a SIP Dialog.
*
* Note that instances of this class are not created directly by your code;
* rather they are returned from the {@link Srf#createUAC}, {@link Srf#createUAC}, and {@link Srf#createB2BUA}
* @class
* @extends EventEmitter
*/
class Dialog extends Emitter {
/**
* Constructor that is called internally by Srf when generating a Dialog instance.
* @param {Srf} srf - Srf instance that created this dialog
* @param {string} type - type of SIP dialog: 'uac', or 'uas'
* @param {Dialog~Options} opts
*/
constructor(srf, type, opts) {
super() ;
const types = ['uas', 'uac'] ;
assert.ok(-1 !== types.indexOf(type), 'argument \'type\' must be one of ' + types.join(',')) ;
this.srf = srf ;
this.type = type ;
this.req = opts.req ;
this.res = opts.res ;
this.agent = this.res.agent ;
this.onHold = false ;
this.connected = true ;
/**
* sip properties that uniquely identify this Dialog
* @type {Dialog~SipInfo}
*/
this.sip = {
callId: this.res.get('Call-ID'),
remoteTag: 'uas' === type ?
this.req.getParsedHeader('from').params.tag : this.res.getParsedHeader('to').params.tag,
localTag: 'uas' === type ?
opts.sent.getParsedHeader('to').params.tag : this.req.getParsedHeader('from').params.tag
} ;
/**
* local side of the Dialog
* @type {Dialog~SipEndpointInfo}
*/
this.local = {
uri: 'uas' === type ? opts.sent.getParsedHeader('Contact')[0].uri : this.req.uri,
sdp: 'uas' === type ? opts.sent.body : this.req.body,
contact: 'uas' === type ? opts.sent.get('Contact') : this.req.get('Contact')
} ;
/**
* local side of the Dialog
* @type {Dialog~SipEndpointInfo}
*/
this.remote = {
uri: 'uas' === type ? this.req.getParsedHeader('Contact')[0].uri : this.res.getParsedHeader('Contact')[0].uri,
sdp: 'uas' === type ? this.req.body : this.res.body
} ;
}
get id() {
return this.res.stackDialogId ;
}
get dialogType() {
return this.req.method ;
}
get subscribeEvent() {
return this.dialogType === 'SUBSCRIBE' ? this.req.get('Event') : null ;
}
get socket() {
return this.req.socket;
}
toJSON() {
return only(this, 'id type sip local remote onHold') ;
}
toString() {
return this.toJSON().toString() ;
}
/**
* destroy the sip dialog by generating a BYE request (in the case of INVITE dialog),
* or NOTIFY (in the case of SUBSCRIBE)
* @param {Dialog~requestCallback=} [callback] callback that returns the generated BYE or NOTIFY message
* @return {Promise} if no callback is supplied, otherwise the function returns a reference to the Dialog
*/
destroy(opts, callback) {
opts = opts || {} ;
if (typeof opts === 'function') {
callback = opts ;
opts = {} ;
}
const __x = (callback) => {
if (this.dialogType === 'INVITE') {
this.agent.request({
method: 'BYE',
headers: opts.headers || {},
stackDialogId: this.id,
_socket: this.socket
}, (err, bye) => {
this.connected = false ;
this.srf.removeDialog(this) ;
callback(err, bye) ;
}) ;
}
else if (this.dialogType === 'SUBSCRIBE') {
opts.headers = opts.headers || {} ;
opts.headers['subscription-state'] = 'terminated';
opts.headers['event'] = this.subscribeEvent ;
this.agent.request({
method: 'NOTIFY',
headers: opts.headers || {},
stackDialogId: this.id,
_socket: this.socket
}, (err, notify) => {
this.connected = false ;
this.srf.removeDialog(this) ;
callback(err, notify) ;
}) ;
}
};
if (callback) {
__x(callback) ;
return this ;
}
return new Promise((resolve, reject) => {
__x((err, msg) => {
if (err) return reject(err);
resolve(msg);
});
});
}
/**
* modify the dialog session by changing attributes of the media connection
* @param {string} sdp - 'hold', 'unhold', or a session description protocol
* @param {Dialog~modifySessionCallback} [callback] - callback invoked when operation has completed
* @return {Promise} if no callback is supplied, otherwise the function returns a reference to the Dialog
*/
modify(sdp, callback) {
const __x = (callback) => {
switch (sdp) {
case 'hold':
this.local.sdp = this.local.sdp.replace(/a=sendrecv/, 'a=inactive') ;
this.onHold = true ;
break ;
case 'unhold':
if (this.onHold) {
this.local.sdp = this.local.sdp.replace(/a=inactive/, 'a=sendrecv') ;
}
else {
console.error('Dialog#modify: attempt to \'unhold\' session which is not on hold');
return process.nextTick(() => {
callback(new Error('attempt to unhold session that is not on hold'));
}) ;
}
break ;
default:
this.local.sdp = sdp ;
break ;
}
debug('Dialog#modify: sending reINVITE for dialog id: %s, sdp: ', this.id, this.local.sdp) ;
this.agent.request({
method: 'INVITE',
stackDialogId: this.id,
body: this.local.sdp,
_socket: this.socket,
headers: {
'Contact': this.local.contact
}
}, (err, req) => {
req.on('response', (res, ack) => {
debug('Dialog#modifySession: received response to reINVITE with status %d', res.status) ;
if (res.status >= 200) {
ack() ;
if (200 === res.status) {
this.remote.sdp = res.body ;
return callback(null);
}
callback(new Error('' + res.status + ' ' + res.reason, 'E_SIP' + res.status)) ;
}
}) ;
}) ;
};
if (callback) {
__x(callback) ;
return this ;
}
return new Promise((resolve, reject) => {
__x((err) => {
if (err) return reject(err);
resolve();
});
});
}
/**
* send a request within a dialog.
* Note that you may also call <code>request.info(..)</code> as a shortcut
* to send an INFO message, <code>request.notify(..)</code>
* to send a NOTIFY, etc..
* @param {Dialog~requestOptions} opts - configuration options
* @param {Dialog~requestCallback} [callback] - callback invoked when operation has completed
* @return {Promise} if no callback is supplied a Promise that resolves to the response received,
* otherwise the function returns a reference to the Dialog
*/
request(opts, callback) {
assert.ok(typeof opts.method === 'string' &&
-1 !== methods.indexOf(opts.method), '\'opts.method\' is required and must be a SIP method') ;
const __x = (callback) => {
var method = opts.method.toUpperCase() ;
this.agent.request({
method: method,
stackDialogId: this.id,
headers: opts.headers || {},
_socket: this.socket,
body: opts.body
}, (err, req) => {
if (err) {
return callback(err) ;
}
req.on('response', (res, ack) => {
if ('BYE' === method) {
this.srf.removeDialog(this) ;
}
if (res.status >= 200) {
if ('INVITE' === method) { ack() ; }
if (this.dialogType === 'SUBSCRIBE' && 'NOTIFY' === method &&
/terminated/.test(req.get('Subscription-State'))) {
debug('received a 200 OK to a NOTIFY we sent with Subscription-State terminated; dialog is ended') ;
this.connected = false ;
this.srf.removeDialog(this) ;
this.emit('destroy', req.msg) ;
}
callback(null, res) ;
}
}) ;
}) ;
} ;
if (callback) {
__x(callback) ;
return this ;
}
return new Promise((resolve, reject) => {
__x((err) => {
if (err) return reject(err);
resolve();
});
});
}
handle(req, res) {
debug('Dialog handling message: ', req.method) ;
var eventName = req.method.toLowerCase() ;
switch (req.method) {
case 'BYE':
let reason = 'normal release';
if (req.meta.source === 'application') {
if (req.has('Reason')) {
reason = req.get('Reason');
const arr = /text=\"(.*)\"/.exec(reason);
if (arr) reason = arr[1];
}
}
this.connected = false ;
this.srf.removeDialog(this) ;
res.send(200) ;
this.emit('destroy', req.msg, reason) ;
break ;
case 'INVITE':
var origRedacted = this.remote.sdp.replace(/^o=.*$/m, 'o=REDACTED') ;
var newRedacted = req.body.replace(/^o=.*$/m, 'o=REDACTED') ;
var refresh = req.body === this.remote.sdp ;
var hold = origRedacted.replace(/a=sendrecv\r\n/g, 'a=sendonly\r\n') === newRedacted ;
var unhold = origRedacted.replace(/a=sendonly\r\n/g, 'a=sendrecv\r\n') === newRedacted ;
var modify = !hold && !unhold && !refresh ;
this.remote.sdp = req.body ;
if (refresh) {
this.emit('refresh', req.msg);
}
else if (hold) {
this.local.sdp = this.local.sdp.replace(/a=sendrecv\r\n/g, 'a=recvonly\r\n') ;
this.emit('hold', req.msg) ;
}
else if (unhold) {
this.local.sdp = this.local.sdp.replace(/a=recvonly\r\n/g, 'a=sendrecv\r\n') ;
this.emit('unhold', req.msg) ;
}
if ((refresh || hold || unhold) || (modify && 0 === this.listeners('modify').length)) {
debug('responding with 200 OK to reINVITE') ;
res.send(200, {
body: this.local.sdp,
headers: {
'Contact': this.local.contact,
'Content-Type': 'application/sdp'
}
}) ;
}
else if (modify) {
this.emit('modify', req, res) ;
}
break ;
case 'NOTIFY':
// if this is a subscribe dialog and subscription-state: terminated, then destroy the dialog
if (req.has('subscription-state') && /terminated/.test(req.get('subscription-state'))) {
setImmediate(() => {
debug('received a NOTIFY with Subscription-State terminated; dialog is ended') ;
this.connected = false ;
this.srf.removeDialog(this) ;
this.emit('destroy', req.msg) ;
}) ;
}
if (0 === this.listeners(eventName).length) {
res.send(200) ;
}
else {
this.emit(eventName, req, res) ;
}
break ;
case 'SUBSCRIBE':
case 'INFO':
case 'REFER':
case 'OPTIONS':
case 'MESSAGE':
case 'PUBLISH':
case 'UPDATE':
if (0 === this.listeners(eventName).length) {
res.send(200) ;
}
else {
this.emit(eventName, req, res) ;
}
break ;
case 'ACK':
this.emit('ack', req.msg) ;
break ;
default:
console.error('Dialog#handle received invalid method within an INVITE dialog: %s', req.method) ;
res.send(501) ;
break ;
}
}
}
module.exports = exports = Dialog ;
methods.forEach((method) => {
Dialog.prototype[method.toLowerCase()] = (opts, cb) => {
opts = opts || {} ;
opts.method = method ;
return this.request(opts, cb) ;
};
}) ;
/**
* @typedef {Object} Dialog~requestOptions
* @property {String} method - SIP method to use for the request
* @property {Object} [headers] - SIP headers to apply to the request
* @property {String} [body] - body of the SIP request
*/
/**
* This callback provides the response to a sip request sent within the dialog.
* @callback Dialog~requestCallback
* @param {Error} err - error, if any
* @param {Response} response - response received from the far end
*/
/**
* This callback provides the SIP request that was generated
* @callback Dialog~requestCallback
* @param {Error} err error returned on non-success
* @param {Request} req Request that was generated
*/
/**
* This callback provides the response a modifySession request.
* @callback Dialog~modifySessionCallback
* @param {Error} err non-success sip response code received from far end
*/
/**
* SIP Dialog identifiers
* @typedef {Object} Dialog~SipInfo
* @property {String} callId - SIP Call-ID
* @property {String} localTag - tag generated by local side of the Dialog
* @property {String} remoteTag - tag generated by the remote side of the Dialog
*/
/**
* SIP Endpoint identifiers
* @typedef {Object} Dialog~SipEndpointInfo
* @property {String} uri - sip
* @property {String} sdp - session description protocol
*/
/**
* a <code>destroy</code> event is triggered when the Dialog is torn down from the far end
* @event Dialog#destroy
* @param {Object} msg - incoming BYE request message
*/
/**
* a <code>modify</code> event is triggered when the far end modifies the session by sending a re-INVITE.
* When an application adds a handler for this event it must generate
* the SIP response by calling <code>res.send</code> on the provided drachtio response object.
* When no handler is found for this event a 200 OK with the current local SDP
* will be automatically generated.
*
* @event Dialog#modify
* @param {Object} req - drachtio request object
* @param {Object} res - drachtio response object
* @memberOf Dialog
*/
/**
* a <code>refresh</code> event is triggered when the far end sends a session refresh.
* There is no need for the application to respond to this event; this is purely
* a notification.
* @event Dialog#refresh
* @param {Object} msg - incoming re-INVITE request message
*/
/**
* an <code>info</code> event is triggered when the far end sends an INFO message.
* When an application adds a handler for this event it must generate
* the SIP response by calling <code>res.send</code> on the provided drachtio response object.
* When no handler is found for this event a 200 OK will be automatically generated.
* @event Dialog#info
* @param {Object} req - drachtio request object
* @param {Object} res - drachtio response object
*/
/**
* a <code>notify</code> event is triggered when the far end sends a NOTIFY message.
* When an application adds a handler for this event it must generate
* the SIP response by calling <code>res.send</code> on the provided drachtio response object.
* When no handler is found for this event a 200 OK will be automatically generated.
* @event Dialog#notify
* @param {Object} req - drachtio request object
* @param {Object} res - drachtio response object
*/
/**
* an <code>update</code> event is triggered when the far end sends an UPDATE message.
* When an application adds a handler for this event it must generate
* the SIP response by calling <code>res.send</code> on the provided drachtio response object.
* When no handler is found for this event a 200 OK will be automatically generated.
* @event Dialog#update
* @param {Object} req - drachtio request object
* @param {Object} res - drachtio response object
*/
/**
* a <code>refer</code> event is triggered when the far end sends a REFER message.
* When an application adds a handler for this event it must generate
* the SIP response by calling <code>res.send</code> on the provided drachtio response object.
* When no handler is found for this event a 200 OK will be automatically generated.
* @event Dialog#refer
* @param {Object} req - drachtio request object
* @param {Object} res - drachtio response object
*/