UNPKG

20 kBJavaScriptView Raw
1const Emitter = require('events') ;
2const assert = require('assert') ;
3const only = require('only') ;
4const methods = require('sip-methods') ;
5const debug = require('debug')('drachtio:srf') ;
6const SipError = require('./sip_error');
7const { parseUri } = require('drachtio-sip').parser ;
8
9
10/**
11 * Class representing a SIP Dialog.
12 *
13 * Note that instances of this class are not created directly by your code;
14 * rather they are returned from the {@link Srf#createUAC}, {@link Srf#createUAC}, and {@link Srf#createB2BUA}
15 * @class
16 * @extends EventEmitter
17 */
18class Dialog extends Emitter {
19
20 /**
21 * Constructor that is called internally by Srf when generating a Dialog instance.
22 * @param {Srf} srf - Srf instance that created this dialog
23 * @param {string} type - type of SIP dialog: 'uac', or 'uas'
24 * @param {Dialog~Options} opts
25 */
26 constructor(srf, type, opts) {
27 super() ;
28
29 const types = ['uas', 'uac'] ;
30 assert.ok(-1 !== types.indexOf(type), 'argument \'type\' must be one of ' + types.join(',')) ;
31
32 this.srf = srf ;
33 this.type = type ;
34 this.req = opts.req ;
35 this.res = opts.res ;
36 this.agent = this.res.agent ;
37 this.onHold = false ;
38 this.connected = true ;
39 this.queuedRequests = [];
40 this._queueRequests = false;
41
42 /**
43 * sip properties that uniquely identify this Dialog
44 * @type {Object}
45 * @property {String} callId - SIP Call-ID
46 * @property {String} localTag - tag generated by local side of the Dialog
47 * @property {String} remoteTag - tag generated by the remote side of the Dialog
48 */
49 this.sip = {
50 callId: this.res.get('Call-ID'),
51 remoteTag: 'uas' === type ?
52 this.req.getParsedHeader('from').params.tag : this.res.getParsedHeader('to').params.tag,
53 localTag: 'uas' === type ?
54 opts.sent.getParsedHeader('to').params.tag : this.req.getParsedHeader('from').params.tag
55 } ;
56
57 /**
58 * local side of the Dialog
59 * @type {Object}
60 * @property {String} uri - sip
61 * @property {String} sdp - session description protocol
62 */
63 this.local = {
64 uri: 'uas' === type ? opts.sent.getParsedHeader('Contact')[0].uri : this.req.uri,
65 sdp: 'uas' === type ? opts.sent.body : this.req.body,
66 contact: 'uas' === type ? opts.sent.get('Contact') : this.req.get('Contact')
67 } ;
68
69 /**
70 * remote side of the Dialog
71 * @type {Object}
72 * @property {String} uri - sip
73 * @property {String} sdp - session description protocol
74 */
75 this.remote = {
76 uri: 'uas' === type ? this.req.getParsedHeader('Contact')[0].uri : this.res.getParsedHeader('Contact')[0].uri,
77 sdp: 'uas' === type ? this.req.body : this.res.body
78 } ;
79
80 /*
81 * subscriptions created by this dialog
82 */
83 this.subscriptions = [];
84
85 // if this is a SUBSCRIBE, then we have our first subscription
86 if (this.req.method === 'SUBSCRIBE') {
87 this.addSubscription(this.req);
88 }
89 }
90
91 get id() {
92 return this.res.stackDialogId ;
93 }
94
95 get dialogType() {
96 return this.req.method ;
97 }
98
99 get subscribeEvent() {
100 return this.dialogType === 'SUBSCRIBE' ? this.req.get('Event') : null ;
101 }
102
103 get socket() {
104 return this.req.socket;
105 }
106
107 set queueRequests(enqueue) {
108 debug(`dialog ${this.id}: queueing requests: ${enqueue ? 'ON' : 'OFF'}`);
109 this._queueRequests = enqueue;
110 if (!enqueue) {
111 // process any queued messages
112 if (this.queuedRequests.length > 0) {
113 setImmediate(() => {
114 debug(`dialog ${this.id}: processing ${this.queuedRequests.length} queued requests`);
115 this.queuedRequests.forEach(({req, res}) => this.handle(req, res));
116 this.queueRequests = [];
117 });
118 }
119 }
120 }
121
122 toJSON() {
123 return only(this, 'id type sip local remote onHold') ;
124 }
125
126 toString() {
127 return this.toJSON().toString() ;
128 }
129
130 getCountOfSubscriptions() {
131 return this.subscriptions.length;
132 }
133
134 addSubscription(req) {
135 const to = req.getParsedHeader('To');
136 const u = parseUri(to.uri);
137 debug(`Dialog#addSubscription: to header: ${JSON.stringify(to)}, uri ${JSON.stringify(u)}`);
138 const entity = `${u.user}@${u.host}:${req.get('Event')}`;
139 this.subscriptions.push(entity);
140 debug(`Dialog#addSubscription: adding subscription ${entity}; current count ${this.subscriptions.length}`);
141 return this.subscriptions.length;
142 }
143
144 removeSubscription(uri, event) {
145 const u = parseUri(uri);
146 const entity = `${u.user}@${u.host}:${event}`;
147 const idx = this.subscriptions.indexOf(entity);
148 if (-1 === idx) {
149 console.error(`Dialog#removeSubscription: no subscription found for ${entity}: subs: ${this.subscriptions}`);
150 }
151 else {
152 this.subscriptions.splice(idx, 1);
153 }
154 return this.subscriptions.length;
155 }
156
157 /**
158 * destroy the sip dialog by generating a BYE request (in the case of INVITE dialog),
159 * or NOTIFY (in the case of SUBSCRIBE)
160 * @param {Object} [opts] configuration options
161 * @param {Object} [opts.headers] SIP headers to add to the outgoing BYE or NOTIFY
162 * @param {function} [callback] if provided, callback with signature <code>(err, msg)</code>
163 * that provides the BYE or NOTIFY message that was sent to terminate the dialog
164 * @return {Promise|Dialog} if no callback is supplied, otherwise a reference to the Dialog
165 */
166 destroy(opts, callback) {
167 opts = opts || {} ;
168 if (typeof opts === 'function') {
169 callback = opts ;
170 opts = {} ;
171 }
172 this.queuedRequests = [];
173
174 const __x = (callback) => {
175 if (this.dialogType === 'INVITE') {
176 this.agent.request({
177 method: 'BYE',
178 headers: opts.headers || {},
179 stackDialogId: this.id,
180 _socket: this.socket
181 }, (err, bye) => {
182 this.connected = false ;
183 this.srf.removeDialog(this) ;
184 callback(err, bye) ;
185 this.removeAllListeners();
186 }) ;
187 }
188 else if (this.dialogType === 'SUBSCRIBE') {
189 opts.headers = opts.headers || {} ;
190 opts.headers['subscription-state'] = 'terminated';
191 opts.headers['event'] = this.subscribeEvent ;
192 this.agent.request({
193 method: 'NOTIFY',
194 headers: opts.headers || {},
195 stackDialogId: this.id,
196 _socket: this.socket
197 }, (err, notify) => {
198 this.connected = false ;
199 this.srf.removeDialog(this) ;
200 callback(err, notify) ;
201 this.removeAllListeners();
202 }) ;
203 }
204 };
205
206 if (callback) {
207 __x(callback) ;
208 return this ;
209 }
210
211 return new Promise((resolve, reject) => {
212 __x((err, msg) => {
213 if (err) return reject(err);
214 resolve(msg);
215 });
216 });
217 }
218
219 /**
220 * modify the dialog session by changing attributes of the media connection
221 * @param {string} sdp - 'hold', 'unhold', or a session description protocol
222 * @param {function} [callback] - callback invoked with signature <code>(err)</code> when operation has completed
223 * @return {Promise|Dialog} if no callback is supplied, otherwise the function returns a reference to the Dialog
224 */
225 modify(sdp, callback) {
226
227 const __x = (callback) => {
228 switch (sdp) {
229 case 'hold':
230 this.local.sdp = this.local.sdp.replace(/a=sendrecv/, 'a=inactive') ;
231 this.onHold = true ;
232 break ;
233 case 'unhold':
234 if (this.onHold) {
235 this.local.sdp = this.local.sdp.replace(/a=inactive/, 'a=sendrecv') ;
236 }
237 else {
238 console.error('Dialog#modify: attempt to \'unhold\' session which is not on hold');
239 return process.nextTick(() => {
240 callback(new Error('attempt to unhold session that is not on hold'));
241 }) ;
242 }
243 break ;
244 default:
245 this.local.sdp = sdp ;
246 break ;
247 }
248
249 debug(`Dialog#modify: sending reINVITE for dialog id: ${this.id}, sdp: ${this.local.sdp}`) ;
250 this.agent.request({
251 method: 'INVITE',
252 stackDialogId: this.id,
253 body: this.local.sdp,
254 _socket: this.socket,
255 headers: {
256 'Contact': this.local.contact
257 }
258 }, (err, req) => {
259 if (err) return callback(err);
260 req.on('response', (res, ack) => {
261 debug(`Dialog#modifySession: received response to reINVITE with status ${res.status}`) ;
262 if (res.status >= 200) {
263 ack() ;
264 if (200 === res.status) {
265 this.remote.sdp = res.body ;
266 return callback(null, res.body);
267 }
268 callback(new SipError(res.status, res.reason)) ;
269 }
270 }) ;
271 }) ;
272 };
273
274 if (callback) {
275 __x(callback) ;
276 return this ;
277 }
278
279 return new Promise((resolve, reject) => {
280 __x((err, sdp) => {
281 if (err) return reject(err);
282 resolve(sdp);
283 });
284 });
285 }
286
287 /**
288 * send a request within a dialog.
289 * Note that you may also call <code>request.info(..)</code> as a shortcut
290 * to send an INFO message, <code>request.notify(..)</code>
291 * to send a NOTIFY, etc..
292 * @param {Object} [opts]
293 * @param {string} opts.method - SIP method to use for the request
294 * @param {Object} [opts.headers] - SIP headers to apply to the request
295 * @param {string} [opts.body] - body of the SIP request
296 * @param {function} [callback] - callback invoked with signature <code>(err, req)</code>
297 * when operation has completed
298 * @return {Promise|Dialog} if no callback is supplied a Promise that resolves to the response received,
299 * otherwise the function returns a reference to the Dialog
300 */
301 request(opts, callback) {
302 assert.ok(typeof opts.method === 'string' &&
303 -1 !== methods.indexOf(opts.method), '\'opts.method\' is required and must be a SIP method') ;
304
305 const __x = (callback) => {
306 const method = opts.method.toUpperCase() ;
307
308 this.agent.request({
309 method: method,
310 stackDialogId: this.id,
311 headers: opts.headers || {},
312 auth: opts.auth,
313 _socket: this.socket,
314 body: opts.body
315 }, (err, req) => {
316 if (err) {
317 return callback(err) ;
318 }
319
320 req.on('response', (res, ack) => {
321 if ('BYE' === method) {
322 this.srf.removeDialog(this) ;
323 }
324 if (res.status >= 200) {
325 if ('INVITE' === method) ack() ;
326
327 if (this.dialogType === 'SUBSCRIBE' && 'NOTIFY' === method &&
328 /terminated/.test(req.get('Subscription-State'))) {
329 debug('received response to a NOTIFY we sent terminating final subscription; dialog is ended') ;
330
331 const from = req.getParsedHeader('From');
332 if (this.removeSubscription(from.uri, req.get('Event')) === 0) {
333 this.connected = false ;
334 this.srf.removeDialog(this) ;
335 this.emit('destroy', req) ;
336 this.removeAllListeners();
337 }
338 }
339 callback(null, res) ;
340 }
341 }) ;
342 }) ;
343 } ;
344
345 if (callback) {
346 __x(callback) ;
347 return this ;
348 }
349
350 return new Promise((resolve, reject) => {
351 __x((err, res) => {
352 if (err) return reject(err);
353 resolve(res);
354 });
355 });
356 }
357
358 handle(req, res) {
359 debug(`dialog ${this.id}: handle: ${req.method}`) ;
360 if (this._queueRequests === true) {
361 debug(`dialog ${this.id}: queueing incoming request: ${req.method}`) ;
362 this.queuedRequests.push({req, res});
363 return;
364 }
365 const eventName = req.method.toLowerCase() ;
366 switch (req.method) {
367 case 'BYE':
368 let reason = 'normal release';
369 if (req.meta.source === 'application') {
370 if (req.has('Reason')) {
371 reason = req.get('Reason');
372 const arr = /text=\"(.*)\"/.exec(reason);
373 if (arr) reason = arr[1];
374 }
375 }
376 this.connected = false ;
377 this.srf.removeDialog(this) ;
378 res.send(200) ;
379 this.emit('destroy', req, reason) ;
380 this.removeAllListeners();
381 break ;
382
383 case 'INVITE':
384 const origRedacted = this.remote.sdp.replace(/^o=.*$/m, 'o=REDACTED') ;
385 const newRedacted = req.body.replace(/^o=.*$/m, 'o=REDACTED') ;
386 const refresh = req.body === this.remote.sdp &&
387 this.listeners('refresh').length > 0;
388 const hold = origRedacted.replace(/a=sendrecv\r\n/g, 'a=sendonly\r\n') === newRedacted &&
389 this.listeners('hold').length > 0;
390 const unhold = origRedacted.replace(/a=sendonly\r\n/g, 'a=sendrecv\r\n') === newRedacted &&
391 this.listeners('unhold').length > 0;
392 const modify = !hold && !unhold && !refresh ;
393 this.remote.sdp = req.body ;
394
395 if (refresh) {
396 this.emit('refresh', req);
397 }
398 else if (hold) {
399 this.local.sdp = this.local.sdp.replace(/a=sendrecv\r\n/g, 'a=recvonly\r\n') ;
400 this.emit('hold', req) ;
401 }
402 else if (unhold) {
403 this.local.sdp = this.local.sdp.replace(/a=recvonly\r\n/g, 'a=sendrecv\r\n') ;
404 this.emit('unhold', req) ;
405 }
406 if ((refresh || hold || unhold) || (modify && 0 === this.listeners('modify').length)) {
407 debug('responding with 200 OK to reINVITE') ;
408 res.send(200, {
409 body: this.local.sdp,
410 headers: {
411 'Contact': this.local.contact,
412 'Content-Type': 'application/sdp'
413 }
414 }) ;
415 }
416 else if (modify) {
417 this.emit('modify', req, res) ;
418 }
419 break ;
420
421 case 'NOTIFY':
422 // if this is a subscribe dialog and subscription-state: terminated, then remove the subscription
423 // and if there are no more subscriptions then remove the dialog
424 if (this.dialogType === 'SUBSCRIBE' &&
425 req.has('subscription-state') &&
426 /terminated/.test(req.get('subscription-state'))) {
427
428 setImmediate(() => {
429 const to = req.getParsedHeader('to');
430 if (this.removeSubscription(to.uri, req.get('Event')) === 0) {
431 debug('received a NOTIFY with Subscription-State terminated for final subscription; dialog is ended') ;
432 this.connected = false ;
433 this.srf.removeDialog(this) ;
434 this.emit('destroy', req) ;
435 }
436 }) ;
437 }
438 if (0 === this.listeners(eventName).length) {
439 res.send(200) ;
440 }
441 else {
442 this.emit(eventName, req, res) ;
443 }
444 break ;
445
446 case 'INFO':
447 case 'REFER':
448 case 'OPTIONS':
449 case 'MESSAGE':
450 case 'PUBLISH':
451 case 'UPDATE':
452
453 // N.B.: this is because an app may be using the Promises version
454 // of Srf#createUAS or Srf#createB2B and if so the 'then()' code
455 // may be enqueued at the back of the job queue right now if a very
456 // quick INFO or other request within the dialog just arrived.
457 // We need the dialog to be resolved in the calling app first so it
458 // has time to attach event handlers.
459 setImmediate(() => {
460 if (0 === this.listeners(eventName).length) res.send(200) ;
461 else this.emit(eventName, req, res);
462 });
463 break ;
464
465 case 'SUBSCRIBE':
466 if (req.has('Expires') && 0 === parseInt(req.get('Expires'))) {
467 res.send(202) ;
468 this.emit('unsubscribe', req, 'unsubscribe') ;
469 }
470 else {
471 if (0 === this.listeners('subscribe').length) {
472 res.send(489, 'Bad Event - no dialog handler');
473 }
474 else this.emit('subscribe', req, res);
475 }
476 break;
477
478 case 'ACK':
479 setImmediate(() => this.emit('ack', req));
480 break ;
481
482 default:
483 console.error(`Dialog#handle received invalid method within an INVITE dialog: ${req.method}`) ;
484 res.send(501) ;
485 break ;
486 }
487
488 }
489}
490
491module.exports = exports = Dialog ;
492
493methods.forEach((method) => {
494 Dialog.prototype[method.toLowerCase()] = (opts, cb) => {
495 opts = opts || {} ;
496 opts.method = method ;
497 return this.request(opts, cb) ;
498 };
499}) ;
500
501/**
502 * a <code>destroy</code> event is triggered when the Dialog is torn down from the far end
503 * @event Dialog#destroy
504 * @param {Object} msg - incoming BYE request message
505 */
506/**
507 * a <code>modify</code> event is triggered when the far end modifies the session by sending a re-INVITE.
508 * When an application adds a handler for this event it must generate
509 * the SIP response by calling <code>res.send</code> on the provided drachtio response object.
510 * When no handler is found for this event a 200 OK with the current local SDP
511 * will be automatically generated.
512 *
513 * @event Dialog#modify
514 * @param {Object} req - drachtio request object
515 * @param {Object} res - drachtio response object
516 * @memberOf Dialog
517 */
518/**
519 * a <code>refresh</code> event is triggered when the far end sends a session refresh.
520 * There is no need for the application to respond to this event; this is purely
521 * a notification.
522 * @event Dialog#refresh
523 * @param {Object} msg - incoming re-INVITE request message
524 */
525/**
526 * an <code>info</code> event is triggered when the far end sends an INFO message.
527 * When an application adds a handler for this event it must generate
528 * the SIP response by calling <code>res.send</code> on the provided drachtio response object.
529 * When no handler is found for this event a 200 OK will be automatically generated.
530 * @event Dialog#info
531 * @param {Object} req - drachtio request object
532 * @param {Object} res - drachtio response object
533 */
534/**
535 * a <code>notify</code> event is triggered when the far end sends a NOTIFY message.
536 * When an application adds a handler for this event it must generate
537 * the SIP response by calling <code>res.send</code> on the provided drachtio response object.
538 * When no handler is found for this event a 200 OK will be automatically generated.
539 * @event Dialog#notify
540 * @param {Object} req - drachtio request object
541 * @param {Object} res - drachtio response object
542 */
543/**
544 * an <code>options</code> event is triggered when the far end sends an OPTIONS message.
545 * When an application adds a handler for this event it must generate
546 * the SIP response by calling <code>res.send</code> on the provided drachtio response object.
547 * When no handler is found for this event a 200 OK will be automatically generated.
548 * @event Dialog#options
549 * @param {Object} req - drachtio request object
550 * @param {Object} res - drachtio response object
551 */
552/**
553 * an <code>update</code> event is triggered when the far end sends an UPDATE message.
554 * When an application adds a handler for this event it must generate
555 * the SIP response by calling <code>res.send</code> on the provided drachtio response object.
556 * When no handler is found for this event a 200 OK will be automatically generated.
557 * @event Dialog#update
558 * @param {Object} req - drachtio request object
559 * @param {Object} res - drachtio response object
560 */
561/**
562 * a <code>refer</code> event is triggered when the far end sends a REFER message.
563 * When an application adds a handler for this event it must generate
564 * the SIP response by calling <code>res.send</code> on the provided drachtio response object.
565 * When no handler is found for this event a 200 OK will be automatically generated.
566 * @event Dialog#refer
567 * @param {Object} req - drachtio request object
568 * @param {Object} res - drachtio response object
569 */
570/**
571 * a <code>message</code> event is triggered when the far end sends a MESSAGE message.
572 * When an application adds a handler for this event it must generate
573 * the SIP response by calling <code>res.send</code> on the provided drachtio response object.
574 * When no handler is found for this event a 200 OK will be automatically generated.
575 * @event Dialog#message
576 * @param {Object} req - drachtio request object
577 * @param {Object} res - drachtio response object
578 */
579