diff --git a/lib/dialog.js b/lib/dialog.js index 23df4f5..29fde0a 100644 --- a/lib/dialog.js +++ b/lib/dialog.js @@ -111,6 +111,11 @@ class Dialog extends Emitter { return this.req.socket; } + set stateEmitter({emitter, state}) { + this._emitter = emitter; + this._state = state; + } + set queueRequests(enqueue) { debug(`dialog ${this.id}: queueing requests: ${enqueue ? 'ON' : 'OFF'}`); this._queueRequests = enqueue; @@ -191,6 +196,11 @@ class Dialog extends Emitter { callback(err, bye) ; this.removeAllListeners(); }) ; + if (this._emitter) { + Object.assign(this._state, {state: 'terminated'}); + this._emitter.emit('stateChange', this._state); + this._emitter = null; + } } else if (this.dialogType === 'SUBSCRIBE') { opts.headers = opts.headers || {} ; @@ -400,6 +410,12 @@ class Dialog extends Emitter { const eventName = req.method.toLowerCase() ; switch (req.method) { case 'BYE': + if (this._emitter) { + Object.assign(this._state, {state: 'terminated'}); + this._emitter.emit('stateChange', this._state); + this._emitter = null; + } + let reason = 'normal release'; if (req.meta.source === 'application') { if (req.has('Reason')) { diff --git a/lib/srf.js b/lib/srf.js index ab21532..cb0969e 100644 --- a/lib/srf.js +++ b/lib/srf.js @@ -11,6 +11,21 @@ const deprecate = require('deprecate'); const debug = require('debug')('drachtio:srf') ; const Socket = require('net').Socket; const noop = () => {}; +const idgen = require('short-uuid')(); + +class DialogState {} +class DialogDirection {} + +DialogState.Trying = 'trying'; +DialogState.Proceeding = 'proceeding'; +DialogState.Early = 'early'; +DialogState.Confirmed = 'confirmed', +DialogState.Terminated = 'terminated'; +DialogState.Rejected = 'rejected'; +DialogState.Cancelled = 'cancelled'; + +DialogDirection.Initiator = 'initiator'; +DialogDirection.Recipient = 'recipient'; const noncopyableHdrs = ['via', 'from', 'to', 'call-id', 'cseq', 'contact', 'content-length', 'content-type']; function copyAllHeaders(msg, obj) { @@ -202,12 +217,35 @@ class Srf extends Emitter { callback(err); }; + if (req.method === 'INVITE' + && opts.dialogStateEmitter && opts.dialogStateEmitter.listenerCount('stateChange') > 0) { + if (!req._dialogState) { + const from = req.getParsedHeader('from'); + const uri = Srf.parseUri(from.uri); + if (uri.user && uri.host) { + req._dialogState = { + state: DialogState.Trying, + direction: DialogDirection.Initiator, + aor: `${uri.user || 'unknown'}@${uri.host || 'unknown'}`, + callId: req.get('Call-ID'), + localTag: from.params.tag, + id: idgen.new() + }; + opts.dialogStateEmitter.emit('stateChange', req._dialogState); + } + } + } + const __send = (callback, content) => { let called = false; debug('createUAS sending'); req.on('cancel', () => { req.canceled = called = true ; + if (req._dialogState) { + Object.assign(req._dialogState, {state: DialogState.Cancelled}); + opts.dialogStateEmitter.emit('stateChange', req._dialogState); + } callback(new SipError(487, 'Request Terminated')) ; }) ; @@ -217,6 +255,12 @@ class Srf extends Emitter { }, (err, response) => { if (err) { debug(`createUAS: send failed with ${err}`); + if (req._dialogState) { + Object.assign(req._dialogState, { + state: DialogState.Rejected + }); + opts.dialogStateEmitter.emit('stateChange', req._dialogState); + } if (!called) { called = true; callback(err); @@ -224,10 +268,27 @@ class Srf extends Emitter { return; } + if (req._dialogState) { + const to = response.getParsedHeader('to'); + Object.assign(req._dialogState, { + state: DialogState.Confirmed, + localTag: to.params.tag + }); + opts.dialogStateEmitter.emit('stateChange', req._dialogState); + } + + // note: we used to invoke callback after ACK was received // now we send it at the time we send the 200 OK // this is in keeping with the RFC 3261 spec const dialog = new Dialog(this, 'uas', {req: req, res: res, sent: response}) ; + if (req._dialogState) { + dialog.stateEmitter = { + emitter: opts.dialogStateEmitter, + state: req._dialogState + }; + } + this.addDialog(dialog); callback(null, dialog); @@ -412,16 +473,55 @@ class Srf extends Emitter { cbRequest(err); return callback(err) ; } + if ('INVITE' === method && + opts.dialogStateEmitter && opts.dialogStateEmitter.listenerCount('stateChange') > 0) { + + const from = req.getParsedHeader('from'); + const to = req.getParsedHeader('to'); + const uri = Srf.parseUri(to.uri); + if (uri.user && uri.host) { + req._dialogState = { + state: DialogState.Trying, + direction: DialogDirection.Recipient, + aor: `${uri.user || 'unknown'}@${uri.host || 'unknown'}`, + callId: req.get('Call-ID'), + localTag: from.params.tag, + id: idgen.new() + }; + } + opts.dialogStateEmitter.emit('stateChange', req._dialogState); + } cbRequest(null, req) ; req.on('response', (res, ack) => { if (res.status < 200) { + if (req._dialogState && req._dialogState.state !== DialogState.Early) { + const to = res.getParsedHeader('to'); + if (to.params.tag) { + Object.assign(req._dialogState, {remoteTag: to.params.tag, state: DialogState.Early}); + opts.dialogStateEmitter.emit('stateChange', req._dialogState); + } + else if (req._dialogState.state === DialogState.Trying) { + Object.assign(req._dialogState, {state: DialogState.Proceeding}); + opts.dialogStateEmitter.emit('stateChange', req._dialogState); + } + } cbProvisional(res) ; if (res.has('RSeq')) { ack() ; // send PRACK } } else { + if (req._dialogState) { + const to = res.getParsedHeader('to'); + const state = (200 === res.status ? + DialogState.Confirmed : + (487 === res.status ? DialogState.Cancelled : DialogState.Rejected)); + Object.assign(req._dialogState, { + remoteTag: to.params.tag, + state}); + opts.dialogStateEmitter.emit('stateChange', req._dialogState); + } if (is3pcc && 200 === res.status && !!res.body) { if (opts.noAck === true) { @@ -458,6 +558,12 @@ class Srf extends Emitter { if ((200 === res.status && method === 'INVITE') || ((202 === res.status || 200 === res.status) && method === 'SUBSCRIBE')) { const dialog = new Dialog(this, 'uac', {req: req, res: res}) ; + if (req._dialogState) { + dialog.stateEmitter = { + emitter: opts.dialogStateEmitter, + state: req._dialogState + }; + } this.addDialog(dialog) ; return callback(null, dialog) ; } @@ -808,6 +914,23 @@ class Srf extends Emitter { opts._socket = req.socket ; + // emit dialog events, per https://tools.ietf.org/html/rfc4235#section-3.7.1 + if (opts.dialogStateEmitter && opts.dialogStateEmitter.listenerCount('stateChange') > 0) { + const from = req.getParsedHeader('from'); + const uri = Srf.parseUri(from.uri); + if (uri.user && uri.host) { + req._dialogState = { + state: DialogState.Trying, + direction: DialogDirection.Initiator, + aor: `${uri.user || 'unknown'}@${uri.host || 'unknown'}`, + callId: req.get('Call-ID'), + remoteTag: from.params.tag, + id: idgen.new() + }; + opts.dialogStateEmitter.emit('stateChange', req._dialogState); + } + } + this.createUAC(opts, {cbRequest: handleUACSent, cbProvisional: handleUACProvisionalResponse}) .then((uac) => { @@ -821,7 +944,8 @@ class Srf extends Emitter { return this.createUAS(req, res, { headers: copyUACHeadersToUAS(uac.res), - localSdp: generateSdpA.bind(null, uac.res) + localSdp: generateSdpA.bind(null, uac.res), + dialogStateEmitter: opts.dialogStateEmitter }) .then((uas) => { debug('createB2BUA: successfully created UAS..done!'); @@ -1497,6 +1621,13 @@ class Srf extends Emitter { static get SipResponse() { return require('./response'); } + + static get DialogState() { + return DialogState; + } + static get DialogDirection() { + return DialogDirection; + } } module.exports = exports = Srf ; diff --git a/package.json b/package.json index 205a1ed..e830e7e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "drachtio-srf", - "version": "4.4.43", + "version": "4.4.44", "description": "drachtio signaling resource framework", "main": "lib/srf.js", "scripts": { @@ -32,6 +32,7 @@ "node-noop": "0.0.1", "only": "0.0.2", "sdp-transform": "^2.14.1", + "short-uuid": "^4.1.0", "sip-methods": "^0.3.0", "utils-merge": "1.0.0", "uuid": "^3.4.0"