/* * SIP version 0.7.8 * Copyright (c) 2014-2017 Junction Networks, Inc * Homepage: http://sipjs.com * License: http://sipjs.com/license/ * * * ~~~SIP.js contains substantial portions of JsSIP under the following license~~~ * Homepage: http://jssip.net * Copyright (c) 2012-2013 José Luis Millán - Versatica * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * ~~~ end JsSIP license ~~~ */ (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.SIP = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0 && this._events[type].length > m) { this._events[type].warned = true; console.error('(node) warning: possible EventEmitter memory ' + 'leak detected. %d listeners added. ' + 'Use emitter.setMaxListeners() to increase limit.', this._events[type].length); if (typeof console.trace === 'function') { // not supported in IE 10 console.trace(); } } } return this; }; EventEmitter.prototype.on = EventEmitter.prototype.addListener; EventEmitter.prototype.once = function(type, listener) { if (!isFunction(listener)) throw TypeError('listener must be a function'); var fired = false; function g() { this.removeListener(type, g); if (!fired) { fired = true; listener.apply(this, arguments); } } g.listener = listener; this.on(type, g); return this; }; // emits a 'removeListener' event iff the listener was removed EventEmitter.prototype.removeListener = function(type, listener) { var list, position, length, i; if (!isFunction(listener)) throw TypeError('listener must be a function'); if (!this._events || !this._events[type]) return this; list = this._events[type]; length = list.length; position = -1; if (list === listener || (isFunction(list.listener) && list.listener === listener)) { delete this._events[type]; if (this._events.removeListener) this.emit('removeListener', type, listener); } else if (isObject(list)) { for (i = length; i-- > 0;) { if (list[i] === listener || (list[i].listener && list[i].listener === listener)) { position = i; break; } } if (position < 0) return this; if (list.length === 1) { list.length = 0; delete this._events[type]; } else { list.splice(position, 1); } if (this._events.removeListener) this.emit('removeListener', type, listener); } return this; }; EventEmitter.prototype.removeAllListeners = function(type) { var key, listeners; if (!this._events) return this; // not listening for removeListener, no need to emit if (!this._events.removeListener) { if (arguments.length === 0) this._events = {}; else if (this._events[type]) delete this._events[type]; return this; } // emit removeListener for all listeners on all events if (arguments.length === 0) { for (key in this._events) { if (key === 'removeListener') continue; this.removeAllListeners(key); } this.removeAllListeners('removeListener'); this._events = {}; return this; } listeners = this._events[type]; if (isFunction(listeners)) { this.removeListener(type, listeners); } else { // LIFO order while (listeners.length) this.removeListener(type, listeners[listeners.length - 1]); } delete this._events[type]; return this; }; EventEmitter.prototype.listeners = function(type) { var ret; if (!this._events || !this._events[type]) ret = []; else if (isFunction(this._events[type])) ret = [this._events[type]]; else ret = this._events[type].slice(); return ret; }; EventEmitter.listenerCount = function(emitter, type) { var ret; if (!emitter._events || !emitter._events[type]) ret = 0; else if (isFunction(emitter._events[type])) ret = 1; else ret = emitter._events[type].length; return ret; }; function isFunction(arg) { return typeof arg === 'function'; } function isNumber(arg) { return typeof arg === 'number'; } function isObject(arg) { return typeof arg === 'object' && arg !== null; } function isUndefined(arg) { return arg === void 0; } },{}],2:[function(require,module,exports){ module.exports={ "name": "sip.js", "title": "SIP.js", "description": "A simple, intuitive, and powerful JavaScript signaling library", "version": "0.7.8", "main": "src/index.js", "browser": { "./src/environment.js": "./src/environment_browser.js" }, "homepage": "http://sipjs.com", "author": "OnSIP (http://sipjs.com/authors/)", "contributors": [ { "url": "https://github.com/onsip/SIP.js/blob/master/THANKS.md" } ], "repository": { "type": "git", "url": "https://github.com/onsip/SIP.js.git" }, "keywords": [ "sip", "websocket", "webrtc", "library", "javascript" ], "devDependencies": { "beefy": "^2.1.5", "browserify": "^4.1.8", "grunt": "~0.4.0", "grunt-browserify": "^4.0.1", "grunt-cli": "~0.1.6", "grunt-contrib-copy": "^0.5.0", "grunt-contrib-jasmine": "^1.0.3", "grunt-contrib-jshint": ">0.5.0", "grunt-contrib-uglify": "~0.2.0", "grunt-peg": "~1.3.1", "grunt-trimtrailingspaces": "^0.4.0", "pegjs": "^0.8.0" }, "engines": { "node": ">=0.12" }, "license": "MIT", "scripts": { "repl": "beefy test/repl.js --open", "build": "grunt build", "prepublish": "cd src/Grammar && mkdir -p dist && pegjs --extra-options-file peg.json src/Grammar.pegjs dist/Grammar.js", "test": "grunt travis --verbose" }, "dependencies": { "ws": "^1.0.1" }, "optionalDependencies": { "promiscuous": "^0.6.0" } } },{}],3:[function(require,module,exports){ "use strict"; module.exports = function (SIP) { var ClientContext; ClientContext = function (ua, method, target, options) { var originalTarget = target; // Validate arguments if (target === undefined) { throw new TypeError('Not enough arguments'); } this.ua = ua; this.logger = ua.getLogger('sip.clientcontext'); this.method = method; target = ua.normalizeTarget(target); if (!target) { throw new TypeError('Invalid target: ' + originalTarget); } /* Options * - extraHeaders * - params * - contentType * - body */ options = Object.create(options || Object.prototype); options.extraHeaders = (options.extraHeaders || []).slice(); // Build the request this.request = new SIP.OutgoingRequest(this.method, target, this.ua, options.params, options.extraHeaders); if (options.body) { this.body = {}; this.body.body = options.body; if (options.contentType) { this.body.contentType = options.contentType; } this.request.body = this.body; } /* Set other properties from the request */ this.localIdentity = this.request.from; this.remoteIdentity = this.request.to; this.data = {}; }; ClientContext.prototype = Object.create(SIP.EventEmitter.prototype); ClientContext.prototype.send = function () { (new SIP.RequestSender(this, this.ua)).send(); return this; }; ClientContext.prototype.cancel = function (options) { options = options || {}; options.extraHeaders = (options.extraHeaders || []).slice(); var cancel_reason = SIP.Utils.getCancelReason(options.status_code, options.reason_phrase); this.request.cancel(cancel_reason, options.extraHeaders); this.emit('cancel'); }; ClientContext.prototype.receiveResponse = function (response) { var cause = SIP.Utils.getReasonPhrase(response.status_code); switch(true) { case /^1[0-9]{2}$/.test(response.status_code): this.emit('progress', response, cause); break; case /^2[0-9]{2}$/.test(response.status_code): if(this.ua.applicants[this]) { delete this.ua.applicants[this]; } this.emit('accepted', response, cause); break; default: if(this.ua.applicants[this]) { delete this.ua.applicants[this]; } this.emit('rejected', response, cause); this.emit('failed', response, cause); break; } }; ClientContext.prototype.onRequestTimeout = function () { this.emit('failed', null, SIP.C.causes.REQUEST_TIMEOUT); }; ClientContext.prototype.onTransportError = function () { this.emit('failed', null, SIP.C.causes.CONNECTION_ERROR); }; SIP.ClientContext = ClientContext; }; },{}],4:[function(require,module,exports){ "use strict"; /** * @fileoverview SIP Constants */ /** * SIP Constants. * @augments SIP */ module.exports = function (name, version) { return { USER_AGENT: name +'/'+ version, // SIP scheme SIP: 'sip', SIPS: 'sips', // End and Failure causes causes: { // Generic error causes CONNECTION_ERROR: 'Connection Error', REQUEST_TIMEOUT: 'Request Timeout', SIP_FAILURE_CODE: 'SIP Failure Code', INTERNAL_ERROR: 'Internal Error', // SIP error causes BUSY: 'Busy', REJECTED: 'Rejected', REDIRECTED: 'Redirected', UNAVAILABLE: 'Unavailable', NOT_FOUND: 'Not Found', ADDRESS_INCOMPLETE: 'Address Incomplete', INCOMPATIBLE_SDP: 'Incompatible SDP', AUTHENTICATION_ERROR: 'Authentication Error', DIALOG_ERROR: 'Dialog Error', // Session error causes WEBRTC_NOT_SUPPORTED: 'WebRTC Not Supported', WEBRTC_ERROR: 'WebRTC Error', CANCELED: 'Canceled', NO_ANSWER: 'No Answer', EXPIRES: 'Expires', NO_ACK: 'No ACK', NO_PRACK: 'No PRACK', USER_DENIED_MEDIA_ACCESS: 'User Denied Media Access', BAD_MEDIA_DESCRIPTION: 'Bad Media Description', RTP_TIMEOUT: 'RTP Timeout' }, supported: { UNSUPPORTED: 'none', SUPPORTED: 'supported', REQUIRED: 'required' }, SIP_ERROR_CAUSES: { REDIRECTED: [300,301,302,305,380], BUSY: [486,600], REJECTED: [403,603], NOT_FOUND: [404,604], UNAVAILABLE: [480,410,408,430], ADDRESS_INCOMPLETE: [484], INCOMPATIBLE_SDP: [488,606], AUTHENTICATION_ERROR:[401,407] }, // SIP Methods ACK: 'ACK', BYE: 'BYE', CANCEL: 'CANCEL', INFO: 'INFO', INVITE: 'INVITE', MESSAGE: 'MESSAGE', NOTIFY: 'NOTIFY', OPTIONS: 'OPTIONS', REGISTER: 'REGISTER', UPDATE: 'UPDATE', SUBSCRIBE: 'SUBSCRIBE', REFER: 'REFER', PRACK: 'PRACK', /* SIP Response Reasons * DOC: http://www.iana.org/assignments/sip-parameters * Copied from https://github.com/versatica/OverSIP/blob/master/lib/oversip/sip/constants.rb#L7 */ REASON_PHRASE: { 100: 'Trying', 180: 'Ringing', 181: 'Call Is Being Forwarded', 182: 'Queued', 183: 'Session Progress', 199: 'Early Dialog Terminated', // draft-ietf-sipcore-199 200: 'OK', 202: 'Accepted', // RFC 3265 204: 'No Notification', //RFC 5839 300: 'Multiple Choices', 301: 'Moved Permanently', 302: 'Moved Temporarily', 305: 'Use Proxy', 380: 'Alternative Service', 400: 'Bad Request', 401: 'Unauthorized', 402: 'Payment Required', 403: 'Forbidden', 404: 'Not Found', 405: 'Method Not Allowed', 406: 'Not Acceptable', 407: 'Proxy Authentication Required', 408: 'Request Timeout', 410: 'Gone', 412: 'Conditional Request Failed', // RFC 3903 413: 'Request Entity Too Large', 414: 'Request-URI Too Long', 415: 'Unsupported Media Type', 416: 'Unsupported URI Scheme', 417: 'Unknown Resource-Priority', // RFC 4412 420: 'Bad Extension', 421: 'Extension Required', 422: 'Session Interval Too Small', // RFC 4028 423: 'Interval Too Brief', 428: 'Use Identity Header', // RFC 4474 429: 'Provide Referrer Identity', // RFC 3892 430: 'Flow Failed', // RFC 5626 433: 'Anonymity Disallowed', // RFC 5079 436: 'Bad Identity-Info', // RFC 4474 437: 'Unsupported Certificate', // RFC 4744 438: 'Invalid Identity Header', // RFC 4744 439: 'First Hop Lacks Outbound Support', // RFC 5626 440: 'Max-Breadth Exceeded', // RFC 5393 469: 'Bad Info Package', // draft-ietf-sipcore-info-events 470: 'Consent Needed', // RFC 5360 478: 'Unresolvable Destination', // Custom code copied from Kamailio. 480: 'Temporarily Unavailable', 481: 'Call/Transaction Does Not Exist', 482: 'Loop Detected', 483: 'Too Many Hops', 484: 'Address Incomplete', 485: 'Ambiguous', 486: 'Busy Here', 487: 'Request Terminated', 488: 'Not Acceptable Here', 489: 'Bad Event', // RFC 3265 491: 'Request Pending', 493: 'Undecipherable', 494: 'Security Agreement Required', // RFC 3329 500: 'Internal Server Error', 501: 'Not Implemented', 502: 'Bad Gateway', 503: 'Service Unavailable', 504: 'Server Time-out', 505: 'Version Not Supported', 513: 'Message Too Large', 580: 'Precondition Failure', // RFC 3312 600: 'Busy Everywhere', 603: 'Decline', 604: 'Does Not Exist Anywhere', 606: 'Not Acceptable' }, /* SIP Option Tags * DOC: http://www.iana.org/assignments/sip-parameters/sip-parameters.xhtml#sip-parameters-4 */ OPTION_TAGS: { '100rel': true, // RFC 3262 199: true, // RFC 6228 answermode: true, // RFC 5373 'early-session': true, // RFC 3959 eventlist: true, // RFC 4662 explicitsub: true, // RFC-ietf-sipcore-refer-explicit-subscription-03 'from-change': true, // RFC 4916 'geolocation-http': true, // RFC 6442 'geolocation-sip': true, // RFC 6442 gin: true, // RFC 6140 gruu: true, // RFC 5627 histinfo: true, // RFC 7044 ice: true, // RFC 5768 join: true, // RFC 3911 'multiple-refer': true, // RFC 5368 norefersub: true, // RFC 4488 nosub: true, // RFC-ietf-sipcore-refer-explicit-subscription-03 outbound: true, // RFC 5626 path: true, // RFC 3327 policy: true, // RFC 6794 precondition: true, // RFC 3312 pref: true, // RFC 3840 privacy: true, // RFC 3323 'recipient-list-invite': true, // RFC 5366 'recipient-list-message': true, // RFC 5365 'recipient-list-subscribe': true, // RFC 5367 replaces: true, // RFC 3891 'resource-priority': true, // RFC 4412 'sdp-anat': true, // RFC 4092 'sec-agree': true, // RFC 3329 tdialog: true, // RFC 4538 timer: true, // RFC 4028 uui: true // RFC 7433 } }; }; },{}],5:[function(require,module,exports){ "use strict"; /** * @fileoverview In-Dialog Request Sender */ /** * @augments SIP.Dialog * @class Class creating an In-dialog request sender. * @param {SIP.Dialog} dialog * @param {Object} applicant * @param {SIP.OutgoingRequest} request */ /** * @fileoverview in-Dialog Request Sender */ module.exports = function (SIP) { var RequestSender; RequestSender = function(dialog, applicant, request) { this.dialog = dialog; this.applicant = applicant; this.request = request; // RFC3261 14.1 Modifying an Existing Session. UAC Behavior. this.reattempt = false; this.reattemptTimer = null; }; RequestSender.prototype = { send: function() { var self = this, request_sender = new SIP.RequestSender(this, this.dialog.owner.ua); request_sender.send(); // RFC3261 14.2 Modifying an Existing Session -UAC BEHAVIOR- if (this.request.method === SIP.C.INVITE && request_sender.clientTransaction.state !== SIP.Transactions.C.STATUS_TERMINATED) { this.dialog.uac_pending_reply = true; request_sender.clientTransaction.on('stateChanged', function stateChanged(){ if (this.state === SIP.Transactions.C.STATUS_ACCEPTED || this.state === SIP.Transactions.C.STATUS_COMPLETED || this.state === SIP.Transactions.C.STATUS_TERMINATED) { this.removeListener('stateChanged', stateChanged); self.dialog.uac_pending_reply = false; if (self.dialog.uas_pending_reply === false) { self.dialog.owner.onReadyToReinvite(); } } }); } }, onRequestTimeout: function() { this.applicant.onRequestTimeout(); }, onTransportError: function() { this.applicant.onTransportError(); }, receiveResponse: function(response) { var self = this; // RFC3261 12.2.1.2 408 or 481 is received for a request within a dialog. if (response.status_code === 408 || response.status_code === 481) { this.applicant.onDialogError(response); } else if (response.method === SIP.C.INVITE && response.status_code === 491) { if (this.reattempt) { this.applicant.receiveResponse(response); } else { this.request.cseq.value = this.dialog.local_seqnum += 1; this.reattemptTimer = SIP.Timers.setTimeout( function() { if (self.applicant.owner.status !== SIP.Session.C.STATUS_TERMINATED) { self.reattempt = true; self.request_sender.send(); } }, this.getReattemptTimeout() ); } } else { this.applicant.receiveResponse(response); } } }; return RequestSender; }; },{}],6:[function(require,module,exports){ "use strict"; /** * @fileoverview SIP Dialog */ /** * @augments SIP * @class Class creating a SIP dialog. * @param {SIP.RTCSession} owner * @param {SIP.IncomingRequest|SIP.IncomingResponse} message * @param {Enum} type UAC / UAS * @param {Enum} state SIP.Dialog.C.STATUS_EARLY / SIP.Dialog.C.STATUS_CONFIRMED */ module.exports = function (SIP) { var RequestSender = require('./Dialog/RequestSender')(SIP); var Dialog, C = { // Dialog states STATUS_EARLY: 1, STATUS_CONFIRMED: 2 }; // RFC 3261 12.1 Dialog = function(owner, message, type, state) { var contact; this.uac_pending_reply = false; this.uas_pending_reply = false; if(!message.hasHeader('contact')) { return { error: 'unable to create a Dialog without Contact header field' }; } if(message instanceof SIP.IncomingResponse) { state = (message.status_code < 200) ? C.STATUS_EARLY : C.STATUS_CONFIRMED; } else { // Create confirmed dialog if state is not defined state = state || C.STATUS_CONFIRMED; } contact = message.parseHeader('contact'); // RFC 3261 12.1.1 if(type === 'UAS') { this.id = { call_id: message.call_id, local_tag: message.to_tag, remote_tag: message.from_tag, toString: function() { return this.call_id + this.local_tag + this.remote_tag; } }; this.state = state; this.remote_seqnum = message.cseq; this.local_uri = message.parseHeader('to').uri; this.remote_uri = message.parseHeader('from').uri; this.remote_target = contact.uri; this.route_set = message.getHeaders('record-route'); this.invite_seqnum = message.cseq; this.local_seqnum = message.cseq; } // RFC 3261 12.1.2 else if(type === 'UAC') { this.id = { call_id: message.call_id, local_tag: message.from_tag, remote_tag: message.to_tag, toString: function() { return this.call_id + this.local_tag + this.remote_tag; } }; this.state = state; this.invite_seqnum = message.cseq; this.local_seqnum = message.cseq; this.local_uri = message.parseHeader('from').uri; this.pracked = []; this.remote_uri = message.parseHeader('to').uri; this.remote_target = contact.uri; this.route_set = message.getHeaders('record-route').reverse(); //RENDERBODY if (this.state === C.STATUS_EARLY && (!owner.hasOffer)) { this.mediaHandler = owner.mediaHandlerFactory(owner); } } this.logger = owner.ua.getLogger('sip.dialog', this.id.toString()); this.owner = owner; owner.ua.dialogs[this.id.toString()] = this; this.logger.log('new ' + type + ' dialog created with status ' + (this.state === C.STATUS_EARLY ? 'EARLY': 'CONFIRMED')); owner.emit('dialog', this); }; Dialog.prototype = { /** * @param {SIP.IncomingMessage} message * @param {Enum} UAC/UAS */ update: function(message, type) { this.state = C.STATUS_CONFIRMED; this.logger.log('dialog '+ this.id.toString() +' changed to CONFIRMED state'); if(type === 'UAC') { // RFC 3261 13.2.2.4 this.route_set = message.getHeaders('record-route').reverse(); } }, terminate: function() { this.logger.log('dialog ' + this.id.toString() + ' deleted'); if (this.mediaHandler && this.state !== C.STATUS_CONFIRMED) { this.mediaHandler.peerConnection.close(); } delete this.owner.ua.dialogs[this.id.toString()]; }, /** * @param {String} method request method * @param {Object} extraHeaders extra headers * @returns {SIP.OutgoingRequest} */ // RFC 3261 12.2.1.1 createRequest: function(method, extraHeaders, body) { var cseq, request; extraHeaders = (extraHeaders || []).slice(); if(!this.local_seqnum) { this.local_seqnum = Math.floor(Math.random() * 10000); } cseq = (method === SIP.C.CANCEL || method === SIP.C.ACK) ? this.invite_seqnum : this.local_seqnum += 1; request = new SIP.OutgoingRequest( method, this.remote_target, this.owner.ua, { 'cseq': cseq, 'call_id': this.id.call_id, 'from_uri': this.local_uri, 'from_tag': this.id.local_tag, 'to_uri': this.remote_uri, 'to_tag': this.id.remote_tag, 'route_set': this.route_set }, extraHeaders, body); request.dialog = this; return request; }, /** * @param {SIP.IncomingRequest} request * @returns {Boolean} */ // RFC 3261 12.2.2 checkInDialogRequest: function(request) { var self = this; if(!this.remote_seqnum) { this.remote_seqnum = request.cseq; } else if(request.cseq < this.remote_seqnum) { //Do not try to reply to an ACK request. if (request.method !== SIP.C.ACK) { request.reply(500); } if (request.cseq === this.invite_seqnum) { return true; } return false; } else if(request.cseq > this.remote_seqnum) { this.remote_seqnum = request.cseq; } switch(request.method) { // RFC3261 14.2 Modifying an Existing Session -UAS BEHAVIOR- case SIP.C.INVITE: if (this.uac_pending_reply === true) { request.reply(491); } else if (this.uas_pending_reply === true) { var retryAfter = (Math.random() * 10 | 0) + 1; request.reply(500, null, ['Retry-After:' + retryAfter]); return false; } else { this.uas_pending_reply = true; request.server_transaction.on('stateChanged', function stateChanged(){ if (this.state === SIP.Transactions.C.STATUS_ACCEPTED || this.state === SIP.Transactions.C.STATUS_COMPLETED || this.state === SIP.Transactions.C.STATUS_TERMINATED) { this.removeListener('stateChanged', stateChanged); self.uas_pending_reply = false; if (self.uac_pending_reply === false) { self.owner.onReadyToReinvite(); } } }); } // RFC3261 12.2.2 Replace the dialog`s remote target URI if the request is accepted if(request.hasHeader('contact')) { request.server_transaction.on('stateChanged', function(){ if (this.state === SIP.Transactions.C.STATUS_ACCEPTED) { self.remote_target = request.parseHeader('contact').uri; } }); } break; case SIP.C.NOTIFY: // RFC6665 3.2 Replace the dialog`s remote target URI if the request is accepted if(request.hasHeader('contact')) { request.server_transaction.on('stateChanged', function(){ if (this.state === SIP.Transactions.C.STATUS_COMPLETED) { self.remote_target = request.parseHeader('contact').uri; } }); } break; } return true; }, sendRequest: function(applicant, method, options) { options = options || {}; var extraHeaders = (options.extraHeaders || []).slice(); var body = null; if (options.body) { if (options.body.body) { body = options.body; } else { body = {}; body.body = options.body; if (options.contentType) { body.contentType = options.contentType; } } } var request = this.createRequest(method, extraHeaders, body), request_sender = new RequestSender(this, applicant, request); request_sender.send(); return request; }, /** * @param {SIP.IncomingRequest} request */ receiveRequest: function(request) { //Check in-dialog request if(!this.checkInDialogRequest(request)) { return; } this.owner.receiveRequest(request); } }; Dialog.C = C; SIP.Dialog = Dialog; }; },{"./Dialog/RequestSender":5}],7:[function(require,module,exports){ "use strict"; /** * @fileoverview SIP Digest Authentication */ /** * SIP Digest Authentication. * @augments SIP. * @function Digest Authentication * @param {SIP.UA} ua */ module.exports = function (Utils) { var DigestAuthentication; DigestAuthentication = function(ua) { this.logger = ua.getLogger('sipjs.digestauthentication'); this.username = ua.configuration.authorizationUser; this.password = ua.configuration.password; this.cnonce = null; this.nc = 0; this.ncHex = '00000000'; this.response = null; }; /** * Performs Digest authentication given a SIP request and the challenge * received in a response to that request. * Returns true if credentials were successfully generated, false otherwise. * * @param {SIP.OutgoingRequest} request * @param {Object} challenge */ DigestAuthentication.prototype.authenticate = function(request, challenge) { // Inspect and validate the challenge. this.algorithm = challenge.algorithm; this.realm = challenge.realm; this.nonce = challenge.nonce; this.opaque = challenge.opaque; this.stale = challenge.stale; if (this.algorithm) { if (this.algorithm !== 'MD5') { this.logger.warn('challenge with Digest algorithm different than "MD5", authentication aborted'); return false; } } else { this.algorithm = 'MD5'; } if (! this.realm) { this.logger.warn('challenge without Digest realm, authentication aborted'); return false; } if (! this.nonce) { this.logger.warn('challenge without Digest nonce, authentication aborted'); return false; } // 'qop' can contain a list of values (Array). Let's choose just one. if (challenge.qop) { if (challenge.qop.indexOf('auth') > -1) { this.qop = 'auth'; } else if (challenge.qop.indexOf('auth-int') > -1) { this.qop = 'auth-int'; } else { // Otherwise 'qop' is present but does not contain 'auth' or 'auth-int', so abort here. this.logger.warn('challenge without Digest qop different than "auth" or "auth-int", authentication aborted'); return false; } } else { this.qop = null; } // Fill other attributes. this.method = request.method; this.uri = request.ruri; this.cnonce = Utils.createRandomToken(12); this.nc += 1; this.updateNcHex(); // nc-value = 8LHEX. Max value = 'FFFFFFFF'. if (this.nc === 4294967296) { this.nc = 1; this.ncHex = '00000001'; } // Calculate the Digest "response" value. this.calculateResponse(); return true; }; /** * Generate Digest 'response' value. * @private */ DigestAuthentication.prototype.calculateResponse = function() { var ha1, ha2; // HA1 = MD5(A1) = MD5(username:realm:password) ha1 = Utils.calculateMD5(this.username + ":" + this.realm + ":" + this.password); if (this.qop === 'auth') { // HA2 = MD5(A2) = MD5(method:digestURI) ha2 = Utils.calculateMD5(this.method + ":" + this.uri); // response = MD5(HA1:nonce:nonceCount:credentialsNonce:qop:HA2) this.response = Utils.calculateMD5(ha1 + ":" + this.nonce + ":" + this.ncHex + ":" + this.cnonce + ":auth:" + ha2); } else if (this.qop === 'auth-int') { // HA2 = MD5(A2) = MD5(method:digestURI:MD5(entityBody)) ha2 = Utils.calculateMD5(this.method + ":" + this.uri + ":" + Utils.calculateMD5(this.body ? this.body : "")); // response = MD5(HA1:nonce:nonceCount:credentialsNonce:qop:HA2) this.response = Utils.calculateMD5(ha1 + ":" + this.nonce + ":" + this.ncHex + ":" + this.cnonce + ":auth-int:" + ha2); } else if (this.qop === null) { // HA2 = MD5(A2) = MD5(method:digestURI) ha2 = Utils.calculateMD5(this.method + ":" + this.uri); // response = MD5(HA1:nonce:HA2) this.response = Utils.calculateMD5(ha1 + ":" + this.nonce + ":" + ha2); } }; /** * Return the Proxy-Authorization or WWW-Authorization header value. */ DigestAuthentication.prototype.toString = function() { var auth_params = []; if (! this.response) { throw new Error('response field does not exist, cannot generate Authorization header'); } auth_params.push('algorithm=' + this.algorithm); auth_params.push('username="' + this.username + '"'); auth_params.push('realm="' + this.realm + '"'); auth_params.push('nonce="' + this.nonce + '"'); auth_params.push('uri="' + this.uri + '"'); auth_params.push('response="' + this.response + '"'); if (this.opaque) { auth_params.push('opaque="' + this.opaque + '"'); } if (this.qop) { auth_params.push('qop=' + this.qop); auth_params.push('cnonce="' + this.cnonce + '"'); auth_params.push('nc=' + this.ncHex); } return 'Digest ' + auth_params.join(', '); }; /** * Generate the 'nc' value as required by Digest in this.ncHex by reading this.nc. * @private */ DigestAuthentication.prototype.updateNcHex = function() { var hex = Number(this.nc).toString(16); this.ncHex = '00000000'.substr(0, 8-hex.length) + hex; }; return DigestAuthentication; }; },{}],8:[function(require,module,exports){ "use strict"; var NodeEventEmitter = require('events').EventEmitter; module.exports = function (console) { // Don't use `new SIP.EventEmitter()` for inheriting. // Use Object.create(SIP.EventEmitter.prototoype); function EventEmitter () { NodeEventEmitter.call(this); } EventEmitter.prototype = Object.create(NodeEventEmitter.prototype, { constructor: { value: EventEmitter, enumerable: false, writable: true, configurable: true } }); EventEmitter.prototype.off = function off (eventName, listener) { var warning = ''; warning += 'SIP.EventEmitter#off is deprecated and may be removed in future SIP.js versions.\n'; warning += 'Please use removeListener or removeAllListeners instead.\n'; warning += 'See here for more details:\n'; warning += 'http://nodejs.org/api/events.html#events_emitter_removelistener_event_listener'; console.warn(warning); if (arguments.length < 2) { return this.removeAllListeners.apply(this, arguments); } else { return this.removeListener(eventName, listener); } }; return EventEmitter; }; },{"events":1}],9:[function(require,module,exports){ "use strict"; /** * @fileoverview Exceptions */ /** * SIP Exceptions. * @augments SIP */ module.exports = { ConfigurationError: (function(){ var exception = function(parameter, value) { this.code = 1; this.name = 'CONFIGURATION_ERROR'; this.parameter = parameter; this.value = value; this.message = (!this.value)? 'Missing parameter: '+ this.parameter : 'Invalid value '+ JSON.stringify(this.value) +' for parameter "'+ this.parameter +'"'; }; exception.prototype = new Error(); return exception; }()), InvalidStateError: (function(){ var exception = function(status) { this.code = 2; this.name = 'INVALID_STATE_ERROR'; this.status = status; this.message = 'Invalid status: ' + status; }; exception.prototype = new Error(); return exception; }()), NotSupportedError: (function(){ var exception = function(message) { this.code = 3; this.name = 'NOT_SUPPORTED_ERROR'; this.message = message; }; exception.prototype = new Error(); return exception; }()), GetDescriptionError: (function(){ var exception = function(message) { this.code = 4; this.name = 'GET_DESCRIPTION_ERROR'; this.message = message; }; exception.prototype = new Error(); return exception; }()) }; },{}],10:[function(require,module,exports){ "use strict"; var Grammar = require('./Grammar/dist/Grammar'); module.exports = function (SIP) { return { parse: function parseCustom (input, startRule) { var options = {startRule: startRule, SIP: SIP}; try { Grammar.parse(input, options); } catch (e) { options.data = -1; } return options.data; } }; }; },{"./Grammar/dist/Grammar":11}],11:[function(require,module,exports){ module.exports = (function() { /* * Generated by PEG.js 0.8.0. * * http://pegjs.majda.cz/ */ function peg$subclass(child, parent) { function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); } function SyntaxError(message, expected, found, offset, line, column) { this.message = message; this.expected = expected; this.found = found; this.offset = offset; this.line = line; this.column = column; this.name = "SyntaxError"; } peg$subclass(SyntaxError, Error); function parse(input) { var options = arguments.length > 1 ? arguments[1] : {}, peg$FAILED = {}, peg$startRuleIndices = { Contact: 118, Name_Addr_Header: 155, Record_Route: 175, Request_Response: 81, SIP_URI: 45, Subscription_State: 185, Supported: 190, Require: 181, Via: 193, absoluteURI: 84, Call_ID: 117, Content_Disposition: 129, Content_Length: 134, Content_Type: 135, CSeq: 145, displayName: 121, Event: 148, From: 150, host: 52, Max_Forwards: 153, Min_SE: 212, Proxy_Authenticate: 156, quoted_string: 40, Refer_To: 177, Replaces: 178, Session_Expires: 209, stun_URI: 216, To: 191, turn_URI: 223, uuid: 226, WWW_Authenticate: 208, challenge: 157 }, peg$startRuleIndex = 118, peg$consts = [ "\r\n", { type: "literal", value: "\r\n", description: "\"\\r\\n\"" }, /^[0-9]/, { type: "class", value: "[0-9]", description: "[0-9]" }, /^[a-zA-Z]/, { type: "class", value: "[a-zA-Z]", description: "[a-zA-Z]" }, /^[0-9a-fA-F]/, { type: "class", value: "[0-9a-fA-F]", description: "[0-9a-fA-F]" }, /^[\0-\xFF]/, { type: "class", value: "[\\0-\\xFF]", description: "[\\0-\\xFF]" }, /^["]/, { type: "class", value: "[\"]", description: "[\"]" }, " ", { type: "literal", value: " ", description: "\" \"" }, "\t", { type: "literal", value: "\t", description: "\"\\t\"" }, /^[a-zA-Z0-9]/, { type: "class", value: "[a-zA-Z0-9]", description: "[a-zA-Z0-9]" }, ";", { type: "literal", value: ";", description: "\";\"" }, "/", { type: "literal", value: "/", description: "\"/\"" }, "?", { type: "literal", value: "?", description: "\"?\"" }, ":", { type: "literal", value: ":", description: "\":\"" }, "@", { type: "literal", value: "@", description: "\"@\"" }, "&", { type: "literal", value: "&", description: "\"&\"" }, "=", { type: "literal", value: "=", description: "\"=\"" }, "+", { type: "literal", value: "+", description: "\"+\"" }, "$", { type: "literal", value: "$", description: "\"$\"" }, ",", { type: "literal", value: ",", description: "\",\"" }, "-", { type: "literal", value: "-", description: "\"-\"" }, "_", { type: "literal", value: "_", description: "\"_\"" }, ".", { type: "literal", value: ".", description: "\".\"" }, "!", { type: "literal", value: "!", description: "\"!\"" }, "~", { type: "literal", value: "~", description: "\"~\"" }, "*", { type: "literal", value: "*", description: "\"*\"" }, "'", { type: "literal", value: "'", description: "\"'\"" }, "(", { type: "literal", value: "(", description: "\"(\"" }, ")", { type: "literal", value: ")", description: "\")\"" }, peg$FAILED, "%", { type: "literal", value: "%", description: "\"%\"" }, null, [], function() {return " "; }, function() {return ':'; }, /^[!-~]/, { type: "class", value: "[!-~]", description: "[!-~]" }, /^[\x80-\uFFFF]/, { type: "class", value: "[\\x80-\\uFFFF]", description: "[\\x80-\\uFFFF]" }, /^[\x80-\xBF]/, { type: "class", value: "[\\x80-\\xBF]", description: "[\\x80-\\xBF]" }, /^[a-f]/, { type: "class", value: "[a-f]", description: "[a-f]" }, "`", { type: "literal", value: "`", description: "\"`\"" }, "<", { type: "literal", value: "<", description: "\"<\"" }, ">", { type: "literal", value: ">", description: "\">\"" }, "\\", { type: "literal", value: "\\", description: "\"\\\\\"" }, "[", { type: "literal", value: "[", description: "\"[\"" }, "]", { type: "literal", value: "]", description: "\"]\"" }, "{", { type: "literal", value: "{", description: "\"{\"" }, "}", { type: "literal", value: "}", description: "\"}\"" }, function() {return "*"; }, function() {return "/"; }, function() {return "="; }, function() {return "("; }, function() {return ")"; }, function() {return ">"; }, function() {return "<"; }, function() {return ","; }, function() {return ";"; }, function() {return ":"; }, function() {return "\""; }, /^[!-']/, { type: "class", value: "[!-']", description: "[!-']" }, /^[*-[]/, { type: "class", value: "[*-[]", description: "[*-[]" }, /^[\]-~]/, { type: "class", value: "[\\]-~]", description: "[\\]-~]" }, function(contents) { return contents; }, /^[#-[]/, { type: "class", value: "[#-[]", description: "[#-[]" }, /^[\0-\t]/, { type: "class", value: "[\\0-\\t]", description: "[\\0-\\t]" }, /^[\x0B-\f]/, { type: "class", value: "[\\x0B-\\f]", description: "[\\x0B-\\f]" }, /^[\x0E-]/, { type: "class", value: "[\\x0E-]", description: "[\\x0E-]" }, function() { options.data.uri = new options.SIP.URI(options.data.scheme, options.data.user, options.data.host, options.data.port); delete options.data.scheme; delete options.data.user; delete options.data.host; delete options.data.host_type; delete options.data.port; }, function() { options.data.uri = new options.SIP.URI(options.data.scheme, options.data.user, options.data.host, options.data.port, options.data.uri_params, options.data.uri_headers); delete options.data.scheme; delete options.data.user; delete options.data.host; delete options.data.host_type; delete options.data.port; delete options.data.uri_params; if (options.startRule === 'SIP_URI') { options.data = options.data.uri;} }, "sips", { type: "literal", value: "sips", description: "\"sips\"" }, "sip", { type: "literal", value: "sip", description: "\"sip\"" }, function(uri_scheme) { options.data.scheme = uri_scheme; }, function() { options.data.user = decodeURIComponent(text().slice(0, -1));}, function() { options.data.password = text(); }, function() { options.data.host = text(); return options.data.host; }, function() { options.data.host_type = 'domain'; return text(); }, /^[a-zA-Z0-9_\-]/, { type: "class", value: "[a-zA-Z0-9_\\-]", description: "[a-zA-Z0-9_\\-]" }, /^[a-zA-Z0-9\-]/, { type: "class", value: "[a-zA-Z0-9\\-]", description: "[a-zA-Z0-9\\-]" }, function() { options.data.host_type = 'IPv6'; return text(); }, "::", { type: "literal", value: "::", description: "\"::\"" }, function() { options.data.host_type = 'IPv6'; return text(); }, function() { options.data.host_type = 'IPv4'; return text(); }, "25", { type: "literal", value: "25", description: "\"25\"" }, /^[0-5]/, { type: "class", value: "[0-5]", description: "[0-5]" }, "2", { type: "literal", value: "2", description: "\"2\"" }, /^[0-4]/, { type: "class", value: "[0-4]", description: "[0-4]" }, "1", { type: "literal", value: "1", description: "\"1\"" }, /^[1-9]/, { type: "class", value: "[1-9]", description: "[1-9]" }, function(port) { port = parseInt(port.join('')); options.data.port = port; return port; }, "transport=", { type: "literal", value: "transport=", description: "\"transport=\"" }, "udp", { type: "literal", value: "udp", description: "\"udp\"" }, "tcp", { type: "literal", value: "tcp", description: "\"tcp\"" }, "sctp", { type: "literal", value: "sctp", description: "\"sctp\"" }, "tls", { type: "literal", value: "tls", description: "\"tls\"" }, function(transport) { if(!options.data.uri_params) options.data.uri_params={}; options.data.uri_params['transport'] = transport.toLowerCase(); }, "user=", { type: "literal", value: "user=", description: "\"user=\"" }, "phone", { type: "literal", value: "phone", description: "\"phone\"" }, "ip", { type: "literal", value: "ip", description: "\"ip\"" }, function(user) { if(!options.data.uri_params) options.data.uri_params={}; options.data.uri_params['user'] = user.toLowerCase(); }, "method=", { type: "literal", value: "method=", description: "\"method=\"" }, function(method) { if(!options.data.uri_params) options.data.uri_params={}; options.data.uri_params['method'] = method; }, "ttl=", { type: "literal", value: "ttl=", description: "\"ttl=\"" }, function(ttl) { if(!options.data.params) options.data.params={}; options.data.params['ttl'] = ttl; }, "maddr=", { type: "literal", value: "maddr=", description: "\"maddr=\"" }, function(maddr) { if(!options.data.uri_params) options.data.uri_params={}; options.data.uri_params['maddr'] = maddr; }, "lr", { type: "literal", value: "lr", description: "\"lr\"" }, function() { if(!options.data.uri_params) options.data.uri_params={}; options.data.uri_params['lr'] = undefined; }, function(param, value) { if(!options.data.uri_params) options.data.uri_params = {}; if (value === null){ value = undefined; } else { value = value[1]; } options.data.uri_params[param.toLowerCase()] = value && value.toLowerCase();}, function(hname, hvalue) { hname = hname.join('').toLowerCase(); hvalue = hvalue.join(''); if(!options.data.uri_headers) options.data.uri_headers = {}; if (!options.data.uri_headers[hname]) { options.data.uri_headers[hname] = [hvalue]; } else { options.data.uri_headers[hname].push(hvalue); }}, function() { // lots of tests fail if this isn't guarded... if (options.startRule === 'Refer_To') { options.data.uri = new options.SIP.URI(options.data.scheme, options.data.user, options.data.host, options.data.port, options.data.uri_params, options.data.uri_headers); delete options.data.scheme; delete options.data.user; delete options.data.host; delete options.data.host_type; delete options.data.port; delete options.data.uri_params; } }, "//", { type: "literal", value: "//", description: "\"//\"" }, function() { options.data.scheme= text(); }, { type: "literal", value: "SIP", description: "\"SIP\"" }, function() { options.data.sip_version = text(); }, "INVITE", { type: "literal", value: "INVITE", description: "\"INVITE\"" }, "ACK", { type: "literal", value: "ACK", description: "\"ACK\"" }, "VXACH", { type: "literal", value: "VXACH", description: "\"VXACH\"" }, "OPTIONS", { type: "literal", value: "OPTIONS", description: "\"OPTIONS\"" }, "BYE", { type: "literal", value: "BYE", description: "\"BYE\"" }, "CANCEL", { type: "literal", value: "CANCEL", description: "\"CANCEL\"" }, "REGISTER", { type: "literal", value: "REGISTER", description: "\"REGISTER\"" }, "SUBSCRIBE", { type: "literal", value: "SUBSCRIBE", description: "\"SUBSCRIBE\"" }, "NOTIFY", { type: "literal", value: "NOTIFY", description: "\"NOTIFY\"" }, "REFER", { type: "literal", value: "REFER", description: "\"REFER\"" }, function() { options.data.method = text(); return options.data.method; }, function(status_code) { options.data.status_code = parseInt(status_code.join('')); }, function() { options.data.reason_phrase = text(); }, function() { options.data = text(); }, function() { var idx, length; length = options.data.multi_header.length; for (idx = 0; idx < length; idx++) { if (options.data.multi_header[idx].parsed === null) { options.data = null; break; } } if (options.data !== null) { options.data = options.data.multi_header; } else { options.data = -1; }}, function() { var header; if(!options.data.multi_header) options.data.multi_header = []; try { header = new options.SIP.NameAddrHeader(options.data.uri, options.data.displayName, options.data.params); delete options.data.uri; delete options.data.displayName; delete options.data.params; } catch(e) { header = null; } options.data.multi_header.push( { 'position': peg$currPos, 'offset': offset(), 'parsed': header });}, function(displayName) { displayName = text().trim(); if (displayName[0] === '\"') { displayName = displayName.substring(1, displayName.length-1); } options.data.displayName = displayName; }, "q", { type: "literal", value: "q", description: "\"q\"" }, function(q) { if(!options.data.params) options.data.params = {}; options.data.params['q'] = q; }, "expires", { type: "literal", value: "expires", description: "\"expires\"" }, function(expires) { if(!options.data.params) options.data.params = {}; options.data.params['expires'] = expires; }, function(delta_seconds) { return parseInt(delta_seconds.join('')); }, "0", { type: "literal", value: "0", description: "\"0\"" }, function() { return parseFloat(text()); }, function(param, value) { if(!options.data.params) options.data.params = {}; if (value === null){ value = undefined; } else { value = value[1]; } options.data.params[param.toLowerCase()] = value;}, "render", { type: "literal", value: "render", description: "\"render\"" }, "session", { type: "literal", value: "session", description: "\"session\"" }, "icon", { type: "literal", value: "icon", description: "\"icon\"" }, "alert", { type: "literal", value: "alert", description: "\"alert\"" }, function() { if (options.startRule === 'Content_Disposition') { options.data.type = text().toLowerCase(); } }, "handling", { type: "literal", value: "handling", description: "\"handling\"" }, "optional", { type: "literal", value: "optional", description: "\"optional\"" }, "required", { type: "literal", value: "required", description: "\"required\"" }, function(length) { options.data = parseInt(length.join('')); }, function() { options.data = text(); }, "text", { type: "literal", value: "text", description: "\"text\"" }, "image", { type: "literal", value: "image", description: "\"image\"" }, "audio", { type: "literal", value: "audio", description: "\"audio\"" }, "video", { type: "literal", value: "video", description: "\"video\"" }, "application", { type: "literal", value: "application", description: "\"application\"" }, "message", { type: "literal", value: "message", description: "\"message\"" }, "multipart", { type: "literal", value: "multipart", description: "\"multipart\"" }, "x-", { type: "literal", value: "x-", description: "\"x-\"" }, function(cseq_value) { options.data.value=parseInt(cseq_value.join('')); }, function(expires) {options.data = expires; }, function(event_type) { options.data.event = event_type.toLowerCase(); }, function() { var tag = options.data.tag; options.data = new options.SIP.NameAddrHeader(options.data.uri, options.data.displayName, options.data.params); if (tag) {options.data.setParam('tag',tag)} }, "tag", { type: "literal", value: "tag", description: "\"tag\"" }, function(tag) {options.data.tag = tag; }, function(forwards) { options.data = parseInt(forwards.join('')); }, function(min_expires) {options.data = min_expires; }, function() { options.data = new options.SIP.NameAddrHeader(options.data.uri, options.data.displayName, options.data.params); }, "digest", { type: "literal", value: "Digest", description: "\"Digest\"" }, "realm", { type: "literal", value: "realm", description: "\"realm\"" }, function(realm) { options.data.realm = realm; }, "domain", { type: "literal", value: "domain", description: "\"domain\"" }, "nonce", { type: "literal", value: "nonce", description: "\"nonce\"" }, function(nonce) { options.data.nonce=nonce; }, "opaque", { type: "literal", value: "opaque", description: "\"opaque\"" }, function(opaque) { options.data.opaque=opaque; }, "stale", { type: "literal", value: "stale", description: "\"stale\"" }, "true", { type: "literal", value: "true", description: "\"true\"" }, function() { options.data.stale=true; }, "false", { type: "literal", value: "false", description: "\"false\"" }, function() { options.data.stale=false; }, "algorithm", { type: "literal", value: "algorithm", description: "\"algorithm\"" }, "md5", { type: "literal", value: "MD5", description: "\"MD5\"" }, "md5-sess", { type: "literal", value: "MD5-sess", description: "\"MD5-sess\"" }, function(algorithm) { options.data.algorithm=algorithm.toUpperCase(); }, "qop", { type: "literal", value: "qop", description: "\"qop\"" }, "auth-int", { type: "literal", value: "auth-int", description: "\"auth-int\"" }, "auth", { type: "literal", value: "auth", description: "\"auth\"" }, function(qop_value) { options.data.qop || (options.data.qop=[]); options.data.qop.push(qop_value.toLowerCase()); }, function(rack_value) { options.data.value=parseInt(rack_value.join('')); }, function() { var idx, length; length = options.data.multi_header.length; for (idx = 0; idx < length; idx++) { if (options.data.multi_header[idx].parsed === null) { options.data = null; break; } } if (options.data !== null) { options.data = options.data.multi_header; } else { options.data = -1; }}, function() { var header; if(!options.data.multi_header) options.data.multi_header = []; try { header = new options.SIP.NameAddrHeader(options.data.uri, options.data.displayName, options.data.params); delete options.data.uri; delete options.data.displayName; delete options.data.params; } catch(e) { header = null; } options.data.multi_header.push( { 'position': peg$currPos, 'offset': offset(), 'parsed': header });}, function() { options.data = new options.SIP.NameAddrHeader(options.data.uri, options.data.displayName, options.data.params); }, function() { if (!(options.data.replaces_from_tag && options.data.replaces_to_tag)) { options.data = -1; } }, function() { options.data = { call_id: options.data }; }, "from-tag", { type: "literal", value: "from-tag", description: "\"from-tag\"" }, function(from_tag) { options.data.replaces_from_tag = from_tag; }, "to-tag", { type: "literal", value: "to-tag", description: "\"to-tag\"" }, function(to_tag) { options.data.replaces_to_tag = to_tag; }, "early-only", { type: "literal", value: "early-only", description: "\"early-only\"" }, function() { options.data.early_only = true; }, function(r) {return r;}, function(first, rest) { return list(first, rest); }, function(value) { if (options.startRule === 'Require') { options.data = value || []; } }, function(rseq_value) { options.data.value=parseInt(rseq_value.join('')); }, "active", { type: "literal", value: "active", description: "\"active\"" }, "pending", { type: "literal", value: "pending", description: "\"pending\"" }, "terminated", { type: "literal", value: "terminated", description: "\"terminated\"" }, function() { options.data.state = text(); }, "reason", { type: "literal", value: "reason", description: "\"reason\"" }, function(reason) { if (typeof reason !== 'undefined') options.data.reason = reason; }, function(expires) { if (typeof expires !== 'undefined') options.data.expires = expires; }, "retry_after", { type: "literal", value: "retry_after", description: "\"retry_after\"" }, function(retry_after) { if (typeof retry_after !== 'undefined') options.data.retry_after = retry_after; }, "deactivated", { type: "literal", value: "deactivated", description: "\"deactivated\"" }, "probation", { type: "literal", value: "probation", description: "\"probation\"" }, "rejected", { type: "literal", value: "rejected", description: "\"rejected\"" }, "timeout", { type: "literal", value: "timeout", description: "\"timeout\"" }, "giveup", { type: "literal", value: "giveup", description: "\"giveup\"" }, "noresource", { type: "literal", value: "noresource", description: "\"noresource\"" }, "invariant", { type: "literal", value: "invariant", description: "\"invariant\"" }, function(value) { if (options.startRule === 'Supported') { options.data = value || []; } }, function() { var tag = options.data.tag; options.data = new options.SIP.NameAddrHeader(options.data.uri, options.data.displayName, options.data.params); if (tag) {options.data.setParam('tag',tag)} }, "ttl", { type: "literal", value: "ttl", description: "\"ttl\"" }, function(via_ttl_value) { options.data.ttl = via_ttl_value; }, "maddr", { type: "literal", value: "maddr", description: "\"maddr\"" }, function(via_maddr) { options.data.maddr = via_maddr; }, "received", { type: "literal", value: "received", description: "\"received\"" }, function(via_received) { options.data.received = via_received; }, "branch", { type: "literal", value: "branch", description: "\"branch\"" }, function(via_branch) { options.data.branch = via_branch; }, "rport", { type: "literal", value: "rport", description: "\"rport\"" }, function() { if(typeof response_port !== 'undefined') options.data.rport = response_port.join(''); }, function(via_protocol) { options.data.protocol = via_protocol; }, { type: "literal", value: "UDP", description: "\"UDP\"" }, { type: "literal", value: "TCP", description: "\"TCP\"" }, { type: "literal", value: "TLS", description: "\"TLS\"" }, { type: "literal", value: "SCTP", description: "\"SCTP\"" }, function(via_transport) { options.data.transport = via_transport; }, function() { options.data.host = text(); }, function(via_sent_by_port) { options.data.port = parseInt(via_sent_by_port.join('')); }, function(ttl) { return parseInt(ttl.join('')); }, function(deltaSeconds) { if (options.startRule === 'Session_Expires') { options.data.deltaSeconds = deltaSeconds; } }, "refresher", { type: "literal", value: "refresher", description: "\"refresher\"" }, "uas", { type: "literal", value: "uas", description: "\"uas\"" }, "uac", { type: "literal", value: "uac", description: "\"uac\"" }, function(endpoint) { if (options.startRule === 'Session_Expires') { options.data.refresher = endpoint; } }, function(deltaSeconds) { if (options.startRule === 'Min_SE') { options.data = deltaSeconds; } }, "stuns", { type: "literal", value: "stuns", description: "\"stuns\"" }, "stun", { type: "literal", value: "stun", description: "\"stun\"" }, function(scheme) { options.data.scheme = scheme; }, function(host) { options.data.host = host; }, "?transport=", { type: "literal", value: "?transport=", description: "\"?transport=\"" }, "turns", { type: "literal", value: "turns", description: "\"turns\"" }, "turn", { type: "literal", value: "turn", description: "\"turn\"" }, function() { options.data.transport = transport; }, function() { options.data = text(); } ], peg$bytecode = [ peg$decode(". \"\"2 3!"), peg$decode("0\"\"\"1!3#"), peg$decode("0$\"\"1!3%"), peg$decode("0&\"\"1!3'"), peg$decode("7'*# \"7("), peg$decode("0(\"\"1!3)"), peg$decode("0*\"\"1!3+"), peg$decode(".,\"\"2,3-"), peg$decode("..\"\"2.3/"), peg$decode("00\"\"1!31"), peg$decode(".2\"\"2233*\x89 \".4\"\"2435*} \".6\"\"2637*q \".8\"\"2839*e \".:\"\"2:3;*Y \".<\"\"2<3=*M \".>\"\"2>3?*A \".@\"\"2@3A*5 \".B\"\"2B3C*) \".D\"\"2D3E"), peg$decode("7)*# \"7,"), peg$decode(".F\"\"2F3G*} \".H\"\"2H3I*q \".J\"\"2J3K*e \".L\"\"2L3M*Y \".N\"\"2N3O*M \".P\"\"2P3Q*A \".R\"\"2R3S*5 \".T\"\"2T3U*) \".V\"\"2V3W"), peg$decode("!!.Y\"\"2Y3Z+7$7#+-%7#+#%'#%$## X$\"# X\"# X+! (%"), peg$decode("!! \\7$,#&7$\"+-$7 +#%'\"%$\"# X\"# X*# \" [+@$ \\7$+&$,#&7$\"\"\" X+'%4\"6]\" %$\"# X\"# X"), peg$decode("7.*# \" ["), peg$decode("! \\7'*# \"7(,)&7'*# \"7(\"+A$.8\"\"2839+1%7/+'%4#6^# %$## X$\"# X\"# X"), peg$decode("!! \\72+&$,#&72\"\"\" X+o$ \\! \\7.,#&7.\"+-$72+#%'\"%$\"# X\"# X,@&! \\7.,#&7.\"+-$72+#%'\"%$\"# X\"# X\"+#%'\"%$\"# X\"# X+! (%"), peg$decode("0_\"\"1!3`*# \"73"), peg$decode("0a\"\"1!3b"), peg$decode("0c\"\"1!3d"), peg$decode("7!*) \"0e\"\"1!3f"), peg$decode("! \\7)*\x95 \".F\"\"2F3G*\x89 \".J\"\"2J3K*} \".L\"\"2L3M*q \".Y\"\"2Y3Z*e \".P\"\"2P3Q*Y \".H\"\"2H3I*M \".@\"\"2@3A*A \".g\"\"2g3h*5 \".R\"\"2R3S*) \".N\"\"2N3O+\x9E$,\x9B&7)*\x95 \".F\"\"2F3G*\x89 \".J\"\"2J3K*} \".L\"\"2L3M*q \".Y\"\"2Y3Z*e \".P\"\"2P3Q*Y \".H\"\"2H3I*M \".@\"\"2@3A*A \".g\"\"2g3h*5 \".R\"\"2R3S*) \".N\"\"2N3O\"\"\" X+! (%"), peg$decode("! \\7)*\x89 \".F\"\"2F3G*} \".L\"\"2L3M*q \".Y\"\"2Y3Z*e \".P\"\"2P3Q*Y \".H\"\"2H3I*M \".@\"\"2@3A*A \".g\"\"2g3h*5 \".R\"\"2R3S*) \".N\"\"2N3O+\x92$,\x8F&7)*\x89 \".F\"\"2F3G*} \".L\"\"2L3M*q \".Y\"\"2Y3Z*e \".P\"\"2P3Q*Y \".H\"\"2H3I*M \".@\"\"2@3A*A \".g\"\"2g3h*5 \".R\"\"2R3S*) \".N\"\"2N3O\"\"\" X+! (%"), peg$decode(".T\"\"2T3U*\xE3 \".V\"\"2V3W*\xD7 \".i\"\"2i3j*\xCB \".k\"\"2k3l*\xBF \".:\"\"2:3;*\xB3 \".D\"\"2D3E*\xA7 \".2\"\"2233*\x9B \".8\"\"2839*\x8F \".m\"\"2m3n*\x83 \"7&*} \".4\"\"2435*q \".o\"\"2o3p*e \".q\"\"2q3r*Y \".6\"\"2637*M \".>\"\"2>3?*A \".s\"\"2s3t*5 \".u\"\"2u3v*) \"7'*# \"7("), peg$decode("! \\7)*\u012B \".F\"\"2F3G*\u011F \".J\"\"2J3K*\u0113 \".L\"\"2L3M*\u0107 \".Y\"\"2Y3Z*\xFB \".P\"\"2P3Q*\xEF \".H\"\"2H3I*\xE3 \".@\"\"2@3A*\xD7 \".g\"\"2g3h*\xCB \".R\"\"2R3S*\xBF \".N\"\"2N3O*\xB3 \".T\"\"2T3U*\xA7 \".V\"\"2V3W*\x9B \".i\"\"2i3j*\x8F \".k\"\"2k3l*\x83 \".8\"\"2839*w \".m\"\"2m3n*k \"7&*e \".4\"\"2435*Y \".o\"\"2o3p*M \".q\"\"2q3r*A \".6\"\"2637*5 \".s\"\"2s3t*) \".u\"\"2u3v+\u0134$,\u0131&7)*\u012B \".F\"\"2F3G*\u011F \".J\"\"2J3K*\u0113 \".L\"\"2L3M*\u0107 \".Y\"\"2Y3Z*\xFB \".P\"\"2P3Q*\xEF \".H\"\"2H3I*\xE3 \".@\"\"2@3A*\xD7 \".g\"\"2g3h*\xCB \".R\"\"2R3S*\xBF \".N\"\"2N3O*\xB3 \".T\"\"2T3U*\xA7 \".V\"\"2V3W*\x9B \".i\"\"2i3j*\x8F \".k\"\"2k3l*\x83 \".8\"\"2839*w \".m\"\"2m3n*k \"7&*e \".4\"\"2435*Y \".o\"\"2o3p*M \".q\"\"2q3r*A \".6\"\"2637*5 \".s\"\"2s3t*) \".u\"\"2u3v\"\"\" X+! (%"), peg$decode("!7/+A$.P\"\"2P3Q+1%7/+'%4#6w# %$## X$\"# X\"# X"), peg$decode("!7/+A$.4\"\"2435+1%7/+'%4#6x# %$## X$\"# X\"# X"), peg$decode("!7/+A$.>\"\"2>3?+1%7/+'%4#6y# %$## X$\"# X\"# X"), peg$decode("!7/+A$.T\"\"2T3U+1%7/+'%4#6z# %$## X$\"# X\"# X"), peg$decode("!7/+A$.V\"\"2V3W+1%7/+'%4#6{# %$## X$\"# X\"# X"), peg$decode("!.k\"\"2k3l+1$7/+'%4\"6|\" %$\"# X\"# X"), peg$decode("!7/+7$.i\"\"2i3j+'%4\"6}\" %$\"# X\"# X"), peg$decode("!7/+A$.D\"\"2D3E+1%7/+'%4#6~# %$## X$\"# X\"# X"), peg$decode("!7/+A$.2\"\"2233+1%7/+'%4#6# %$## X$\"# X\"# X"), peg$decode("!7/+A$.8\"\"2839+1%7/+'%4#6\x80# %$## X$\"# X\"# X"), peg$decode("!7/+1$7&+'%4\"6\x81\" %$\"# X\"# X"), peg$decode("!7&+1$7/+'%4\"6\x81\" %$\"# X\"# X"), peg$decode("!7=+W$ \\7G*) \"7K*# \"7F,/&7G*) \"7K*# \"7F\"+-%7>+#%'#%$## X$\"# X\"# X"), peg$decode("0\x82\"\"1!3\x83*A \"0\x84\"\"1!3\x85*5 \"0\x86\"\"1!3\x87*) \"73*# \"7."), peg$decode("!!7/+U$7&+K% \\7J*# \"7K,)&7J*# \"7K\"+-%7&+#%'$%$$# X$## X$\"# X\"# X+! (%"), peg$decode("!7/+`$7&+V%! \\7J*# \"7K,)&7J*# \"7K\"+! (%+2%7&+(%4$6\x88$!!%$$# X$## X$\"# X\"# X"), peg$decode("7.*G \".L\"\"2L3M*; \"0\x89\"\"1!3\x8A*/ \"0\x86\"\"1!3\x87*# \"73"), peg$decode("!.m\"\"2m3n+K$0\x8B\"\"1!3\x8C*5 \"0\x8D\"\"1!3\x8E*) \"0\x8F\"\"1!3\x90+#%'\"%$\"# X\"# X"), peg$decode("!7N+Q$.8\"\"2839+A%7O*# \" [+1%7S+'%4$6\x91$ %$$# X$## X$\"# X\"# X"), peg$decode("!7N+k$.8\"\"2839+[%7O*# \" [+K%7S+A%7_+7%7l*# \" [+'%4&6\x92& %$&# X$%# X$$# X$## X$\"# X\"# X"), peg$decode("!/\x93\"\"1$3\x94*) \"/\x95\"\"1#3\x96+' 4!6\x97!! %"), peg$decode("!7P+b$!.8\"\"2839+-$7R+#%'\"%$\"# X\"# X*# \" [+7%.:\"\"2:3;+'%4#6\x98# %$## X$\"# X\"# X"), peg$decode(" \\7+*) \"7-*# \"7Q+2$,/&7+*) \"7-*# \"7Q\"\"\" X"), peg$decode(".<\"\"2<3=*q \".>\"\"2>3?*e \".@\"\"2@3A*Y \".B\"\"2B3C*M \".D\"\"2D3E*A \".2\"\"2233*5 \".6\"\"2637*) \".4\"\"2435"), peg$decode("! \\7+*_ \"7-*Y \".<\"\"2<3=*M \".>\"\"2>3?*A \".@\"\"2@3A*5 \".B\"\"2B3C*) \".D\"\"2D3E,e&7+*_ \"7-*Y \".<\"\"2<3=*M \".>\"\"2>3?*A \".@\"\"2@3A*5 \".B\"\"2B3C*) \".D\"\"2D3E\"+& 4!6\x99! %"), peg$decode("!7T+N$!.8\"\"2839+-$7^+#%'\"%$\"# X\"# X*# \" [+#%'\"%$\"# X\"# X"), peg$decode("!7U*) \"7\\*# \"7X+& 4!6\x9A! %"), peg$decode("! \\!7V+3$.J\"\"2J3K+#%'\"%$\"# X\"# X,>&!7V+3$.J\"\"2J3K+#%'\"%$\"# X\"# X\"+G$7W+=%.J\"\"2J3K*# \" [+'%4#6\x9B# %$## X$\"# X\"# X"), peg$decode(" \\0\x9C\"\"1!3\x9D+,$,)&0\x9C\"\"1!3\x9D\"\"\" X"), peg$decode("!0$\"\"1!3%+A$ \\0\x9E\"\"1!3\x9F,)&0\x9E\"\"1!3\x9F\"+#%'\"%$\"# X\"# X"), peg$decode("!.o\"\"2o3p+A$7Y+7%.q\"\"2q3r+'%4#6\xA0# %$## X$\"# X\"# X"), peg$decode("!!7Z+\xBF$.8\"\"2839+\xAF%7Z+\xA5%.8\"\"2839+\x95%7Z+\x8B%.8\"\"2839+{%7Z+q%.8\"\"2839+a%7Z+W%.8\"\"2839+G%7Z+=%.8\"\"2839+-%7[+#%'-%$-# X$,# X$+# X$*# X$)# X$(# X$'# X$&# X$%# X$$# X$## X$\"# X\"# X*\u0838 \"!.\xA1\"\"2\xA13\xA2+\xAF$7Z+\xA5%.8\"\"2839+\x95%7Z+\x8B%.8\"\"2839+{%7Z+q%.8\"\"2839+a%7Z+W%.8\"\"2839+G%7Z+=%.8\"\"2839+-%7[+#%',%$,# X$+# X$*# X$)# X$(# X$'# X$&# X$%# X$$# X$## X$\"# X\"# X*\u0795 \"!.\xA1\"\"2\xA13\xA2+\x95$7Z+\x8B%.8\"\"2839+{%7Z+q%.8\"\"2839+a%7Z+W%.8\"\"2839+G%7Z+=%.8\"\"2839+-%7[+#%'*%$*# X$)# X$(# X$'# X$&# X$%# X$$# X$## X$\"# X\"# X*\u070C \"!.\xA1\"\"2\xA13\xA2+{$7Z+q%.8\"\"2839+a%7Z+W%.8\"\"2839+G%7Z+=%.8\"\"2839+-%7[+#%'(%$(# X$'# X$&# X$%# X$$# X$## X$\"# X\"# X*\u069D \"!.\xA1\"\"2\xA13\xA2+a$7Z+W%.8\"\"2839+G%7Z+=%.8\"\"2839+-%7[+#%'&%$&# X$%# X$$# X$## X$\"# X\"# X*\u0648 \"!.\xA1\"\"2\xA13\xA2+G$7Z+=%.8\"\"2839+-%7[+#%'$%$$# X$## X$\"# X\"# X*\u060D \"!.\xA1\"\"2\xA13\xA2+-$7[+#%'\"%$\"# X\"# X*\u05EC \"!.\xA1\"\"2\xA13\xA2+-$7Z+#%'\"%$\"# X\"# X*\u05CB \"!7Z+\xA5$.\xA1\"\"2\xA13\xA2+\x95%7Z+\x8B%.8\"\"2839+{%7Z+q%.8\"\"2839+a%7Z+W%.8\"\"2839+G%7Z+=%.8\"\"2839+-%7[+#%'+%$+# X$*# X$)# X$(# X$'# X$&# X$%# X$$# X$## X$\"# X\"# X*\u0538 \"!7Z+\xB6$!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+\x8B%.\xA1\"\"2\xA13\xA2+{%7Z+q%.8\"\"2839+a%7Z+W%.8\"\"2839+G%7Z+=%.8\"\"2839+-%7[+#%'*%$*# X$)# X$(# X$'# X$&# X$%# X$$# X$## X$\"# X\"# X*\u0494 \"!7Z+\xC7$!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+\x9C%!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+q%.\xA1\"\"2\xA13\xA2+a%7Z+W%.8\"\"2839+G%7Z+=%.8\"\"2839+-%7[+#%')%$)# X$(# X$'# X$&# X$%# X$$# X$## X$\"# X\"# X*\u03DF \"!7Z+\xD8$!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+\xAD%!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+\x82%!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+W%.\xA1\"\"2\xA13\xA2+G%7Z+=%.8\"\"2839+-%7[+#%'(%$(# X$'# X$&# X$%# X$$# X$## X$\"# X\"# X*\u0319 \"!7Z+\xE9$!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+\xBE%!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+\x93%!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+h%!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+=%.\xA1\"\"2\xA13\xA2+-%7[+#%''%$'# X$&# X$%# X$$# X$## X$\"# X\"# X*\u0242 \"!7Z+\u0114$!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+\xE9%!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+\xBE%!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+\x93%!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+h%!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+=%.\xA1\"\"2\xA13\xA2+-%7Z+#%'(%$(# X$'# X$&# X$%# X$$# X$## X$\"# X\"# X*\u0140 \"!7Z+\u0135$!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+\u010A%!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+\xDF%!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+\xB4%!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+\x89%!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+^%!.8\"\"2839+-$7Z+#%'\"%$\"# X\"# X*# \" [+3%.\xA1\"\"2\xA13\xA2+#%'(%$(# X$'# X$&# X$%# X$$# X$## X$\"# X\"# X+& 4!6\xA3! %"), peg$decode("!7#+S$7#*# \" [+C%7#*# \" [+3%7#*# \" [+#%'$%$$# X$## X$\"# X\"# X"), peg$decode("!7Z+=$.8\"\"2839+-%7Z+#%'#%$## X$\"# X\"# X*# \"7\\"), peg$decode("!7]+u$.J\"\"2J3K+e%7]+[%.J\"\"2J3K+K%7]+A%.J\"\"2J3K+1%7]+'%4'6\xA4' %$'# X$&# X$%# X$$# X$## X$\"# X\"# X"), peg$decode("!.\xA5\"\"2\xA53\xA6+3$0\xA7\"\"1!3\xA8+#%'\"%$\"# X\"# X*\xA0 \"!.\xA9\"\"2\xA93\xAA+=$0\xAB\"\"1!3\xAC+-%7!+#%'#%$## X$\"# X\"# X*o \"!.\xAD\"\"2\xAD3\xAE+7$7!+-%7!+#%'#%$## X$\"# X\"# X*D \"!0\xAF\"\"1!3\xB0+-$7!+#%'\"%$\"# X\"# X*# \"7!"), peg$decode("!!7!*# \" [+c$7!*# \" [+S%7!*# \" [+C%7!*# \" [+3%7!*# \" [+#%'%%$%# X$$# X$## X$\"# X\"# X+' 4!6\xB1!! %"), peg$decode(" \\!.2\"\"2233+-$7`+#%'\"%$\"# X\"# X,>&!.2\"\"2233+-$7`+#%'\"%$\"# X\"# X\""), peg$decode("7a*A \"7b*; \"7c*5 \"7d*/ \"7e*) \"7f*# \"7g"), peg$decode("!/\xB2\"\"1*3\xB3+b$/\xB4\"\"1#3\xB5*G \"/\xB6\"\"1#3\xB7*; \"/\xB8\"\"1$3\xB9*/ \"/\xBA\"\"1#3\xBB*# \"76+(%4\"6\xBC\"! %$\"# X\"# X"), peg$decode("!/\xBD\"\"1%3\xBE+J$/\xBF\"\"1%3\xC0*/ \"/\xC1\"\"1\"3\xC2*# \"76+(%4\"6\xC3\"! %$\"# X\"# X"), peg$decode("!/\xC4\"\"1'3\xC5+2$7\x8F+(%4\"6\xC6\"! %$\"# X\"# X"), peg$decode("!/\xC7\"\"1$3\xC8+2$7\xEF+(%4\"6\xC9\"! %$\"# X\"# X"), peg$decode("!/\xCA\"\"1&3\xCB+2$7T+(%4\"6\xCC\"! %$\"# X\"# X"), peg$decode("!/\xCD\"\"1\"3\xCE+R$!.>\"\"2>3?+-$76+#%'\"%$\"# X\"# X*# \" [+'%4\"6\xCF\" %$\"# X\"# X"), peg$decode("!7h+T$!.>\"\"2>3?+-$7i+#%'\"%$\"# X\"# X*# \" [+)%4\"6\xD0\"\"! %$\"# X\"# X"), peg$decode("! \\7j+&$,#&7j\"\"\" X+! (%"), peg$decode("! \\7j+&$,#&7j\"\"\" X+! (%"), peg$decode("7k*) \"7+*# \"7-"), peg$decode(".o\"\"2o3p*e \".q\"\"2q3r*Y \".4\"\"2435*M \".8\"\"2839*A \".<\"\"2<3=*5 \".@\"\"2@3A*) \".B\"\"2B3C"), peg$decode("!.6\"\"2637+u$7m+k% \\!.<\"\"2<3=+-$7m+#%'\"%$\"# X\"# X,>&!.<\"\"2<3=+-$7m+#%'\"%$\"# X\"# X\"+#%'#%$## X$\"# X\"# X"), peg$decode("!7n+C$.>\"\"2>3?+3%7o+)%4#6\xD1#\"\" %$## X$\"# X\"# X"), peg$decode(" \\7p*) \"7+*# \"7-+2$,/&7p*) \"7+*# \"7-\"\"\" X"), peg$decode(" \\7p*) \"7+*# \"7-,/&7p*) \"7+*# \"7-\""), peg$decode(".o\"\"2o3p*e \".q\"\"2q3r*Y \".4\"\"2435*M \".6\"\"2637*A \".8\"\"2839*5 \".@\"\"2@3A*) \".B\"\"2B3C"), peg$decode("7\x90*# \"7r"), peg$decode("!7\x8F+K$7'+A%7s+7%7'+-%7\x84+#%'%%$%# X$$# X$## X$\"# X\"# X"), peg$decode("7M*# \"7t"), peg$decode("!7+G$.8\"\"2839+7%7u*# \"7x+'%4#6\xD2# %$## X$\"# X\"# X"), peg$decode("!7v*# \"7w+N$!.6\"\"2637+-$7\x83+#%'\"%$\"# X\"# X*# \" [+#%'\"%$\"# X\"# X"), peg$decode("!.\xD3\"\"2\xD33\xD4+=$7\x80+3%7w*# \" [+#%'#%$## X$\"# X\"# X"), peg$decode("!.4\"\"2435+-$7{+#%'\"%$\"# X\"# X"), peg$decode("!7z+5$ \\7y,#&7y\"+#%'\"%$\"# X\"# X"), peg$decode("7**) \"7+*# \"7-"), peg$decode("7+*\x8F \"7-*\x89 \".2\"\"2233*} \".6\"\"2637*q \".8\"\"2839*e \".:\"\"2:3;*Y \".<\"\"2<3=*M \".>\"\"2>3?*A \".@\"\"2@3A*5 \".B\"\"2B3C*) \".D\"\"2D3E"), peg$decode("!7|+k$ \\!.4\"\"2435+-$7|+#%'\"%$\"# X\"# X,>&!.4\"\"2435+-$7|+#%'\"%$\"# X\"# X\"+#%'\"%$\"# X\"# X"), peg$decode("! \\7~,#&7~\"+k$ \\!.2\"\"2233+-$7}+#%'\"%$\"# X\"# X,>&!.2\"\"2233+-$7}+#%'\"%$\"# X\"# X\"+#%'\"%$\"# X\"# X"), peg$decode(" \\7~,#&7~\""), peg$decode("7+*w \"7-*q \".8\"\"2839*e \".:\"\"2:3;*Y \".<\"\"2<3=*M \".>\"\"2>3?*A \".@\"\"2@3A*5 \".B\"\"2B3C*) \".D\"\"2D3E"), peg$decode("!7\"+\x8D$ \\7\"*G \"7!*A \".@\"\"2@3A*5 \".F\"\"2F3G*) \".J\"\"2J3K,M&7\"*G \"7!*A \".@\"\"2@3A*5 \".F\"\"2F3G*) \".J\"\"2J3K\"+'%4\"6\xD5\" %$\"# X\"# X"), peg$decode("7\x81*# \"7\x82"), peg$decode("!!7O+3$.:\"\"2:3;+#%'\"%$\"# X\"# X*# \" [+-$7S+#%'\"%$\"# X\"# X*# \" ["), peg$decode(" \\7+*\x83 \"7-*} \".B\"\"2B3C*q \".D\"\"2D3E*e \".2\"\"2233*Y \".8\"\"2839*M \".:\"\"2:3;*A \".<\"\"2<3=*5 \".>\"\"2>3?*) \".@\"\"2@3A+\x8C$,\x89&7+*\x83 \"7-*} \".B\"\"2B3C*q \".D\"\"2D3E*e \".2\"\"2233*Y \".8\"\"2839*M \".:\"\"2:3;*A \".<\"\"2<3=*5 \".>\"\"2>3?*) \".@\"\"2@3A\"\"\" X"), peg$decode(" \\7y,#&7y\""), peg$decode("!/\x95\"\"1#3\xD6+y$.4\"\"2435+i% \\7!+&$,#&7!\"\"\" X+P%.J\"\"2J3K+@% \\7!+&$,#&7!\"\"\" X+'%4%6\xD7% %$%# X$$# X$## X$\"# X\"# X"), peg$decode(".\xD8\"\"2\xD83\xD9"), peg$decode(".\xDA\"\"2\xDA3\xDB"), peg$decode(".\xDC\"\"2\xDC3\xDD"), peg$decode(".\xDE\"\"2\xDE3\xDF"), peg$decode(".\xE0\"\"2\xE03\xE1"), peg$decode(".\xE2\"\"2\xE23\xE3"), peg$decode(".\xE4\"\"2\xE43\xE5"), peg$decode(".\xE6\"\"2\xE63\xE7"), peg$decode(".\xE8\"\"2\xE83\xE9"), peg$decode(".\xEA\"\"2\xEA3\xEB"), peg$decode("!7\x85*S \"7\x86*M \"7\x88*G \"7\x89*A \"7\x8A*; \"7\x8B*5 \"7\x8C*/ \"7\x8D*) \"7\x8E*# \"76+& 4!6\xEC! %"), peg$decode("!7\x84+K$7'+A%7\x91+7%7'+-%7\x93+#%'%%$%# X$$# X$## X$\"# X\"# X"), peg$decode("!7\x92+' 4!6\xED!! %"), peg$decode("!7!+7$7!+-%7!+#%'#%$## X$\"# X\"# X"), peg$decode("! \\7**A \"7+*; \"7-*5 \"73*/ \"74*) \"7'*# \"7(,G&7**A \"7+*; \"7-*5 \"73*/ \"74*) \"7'*# \"7(\"+& 4!6\xEE! %"), peg$decode("!7\xB5+_$ \\!7A+-$7\xB5+#%'\"%$\"# X\"# X,8&!7A+-$7\xB5+#%'\"%$\"# X\"# X\"+#%'\"%$\"# X\"# X"), peg$decode("!79+R$!.:\"\"2:3;+-$79+#%'\"%$\"# X\"# X*# \" [+'%4\"6\xEF\" %$\"# X\"# X"), peg$decode("!7:*j \"!7\x97+_$ \\!7A+-$7\x97+#%'\"%$\"# X\"# X,8&!7A+-$7\x97+#%'\"%$\"# X\"# X\"+#%'\"%$\"# X\"# X+& 4!6\xF0! %"), peg$decode("!7L*# \"7\x98+c$ \\!7B+-$7\x9A+#%'\"%$\"# X\"# X,8&!7B+-$7\x9A+#%'\"%$\"# X\"# X\"+'%4\"6\xF1\" %$\"# X\"# X"), peg$decode("!7\x99*# \" [+A$7@+7%7M+-%7?+#%'$%$$# X$## X$\"# X\"# X"), peg$decode("!!76+_$ \\!7.+-$76+#%'\"%$\"# X\"# X,8&!7.+-$76+#%'\"%$\"# X\"# X\"+#%'\"%$\"# X\"# X*# \"7H+' 4!6\xF2!! %"), peg$decode("7\x9B*) \"7\x9C*# \"7\x9F"), peg$decode("!/\xF3\"\"1!3\xF4+<$7<+2%7\x9E+(%4#6\xF5#! %$## X$\"# X\"# X"), peg$decode("!/\xF6\"\"1'3\xF7+<$7<+2%7\x9D+(%4#6\xF8#! %$## X$\"# X\"# X"), peg$decode("! \\7!+&$,#&7!\"\"\" X+' 4!6\xF9!! %"), peg$decode("!.\xFA\"\"2\xFA3\xFB+x$!.J\"\"2J3K+S$7!*# \" [+C%7!*# \" [+3%7!*# \" [+#%'$%$$# X$## X$\"# X\"# X*# \" [+'%4\"6\xFC\" %$\"# X\"# X"), peg$decode("!76+N$!7<+-$7\xA0+#%'\"%$\"# X\"# X*# \" [+)%4\"6\xFD\"\"! %$\"# X\"# X"), peg$decode("76*) \"7T*# \"7H"), peg$decode("!7\xA2+_$ \\!7B+-$7\xA3+#%'\"%$\"# X\"# X,8&!7B+-$7\xA3+#%'\"%$\"# X\"# X\"+#%'\"%$\"# X\"# X"), peg$decode("!/\xFE\"\"1&3\xFF*G \"/\u0100\"\"1'3\u0101*; \"/\u0102\"\"1$3\u0103*/ \"/\u0104\"\"1%3\u0105*# \"76+& 4!6\u0106! %"), peg$decode("7\xA4*# \"7\x9F"), peg$decode("!/\u0107\"\"1(3\u0108+O$7<+E%/\u0109\"\"1(3\u010A*/ \"/\u010B\"\"1(3\u010C*# \"76+#%'#%$## X$\"# X\"# X"), peg$decode("!76+_$ \\!7A+-$76+#%'\"%$\"# X\"# X,8&!7A+-$76+#%'\"%$\"# X\"# X\"+#%'\"%$\"# X\"# X"), peg$decode("! \\7!+&$,#&7!\"\"\" X+' 4!6\u010D!! %"), peg$decode("!7\xA8+& 4!6\u010E! %"), peg$decode("!7\xA9+s$7;+i%7\xAE+_% \\!7B+-$7\xAF+#%'\"%$\"# X\"# X,8&!7B+-$7\xAF+#%'\"%$\"# X\"# X\"+#%'$%$$# X$## X$\"# X\"# X"), peg$decode("7\xAA*# \"7\xAB"), peg$decode("/\u010F\"\"1$3\u0110*S \"/\u0111\"\"1%3\u0112*G \"/\u0113\"\"1%3\u0114*; \"/\u0115\"\"1%3\u0116*/ \"/\u0117\"\"1+3\u0118*# \"7\xAC"), peg$decode("/\u0119\"\"1'3\u011A*/ \"/\u011B\"\"1)3\u011C*# \"7\xAC"), peg$decode("76*# \"7\xAD"), peg$decode("!/\u011D\"\"1\"3\u011E+-$76+#%'\"%$\"# X\"# X"), peg$decode("7\xAC*# \"76"), peg$decode("!76+7$7<+-%7\xB0+#%'#%$## X$\"# X\"# X"), peg$decode("76*# \"7H"), peg$decode("!7\xB2+7$7.+-%7\x8F+#%'#%$## X$\"# X\"# X"), peg$decode("! \\7!+&$,#&7!\"\"\" X+' 4!6\u011F!! %"), peg$decode("!7\x9D+' 4!6\u0120!! %"), peg$decode("!7\xB5+d$ \\!7B+-$7\x9F+#%'\"%$\"# X\"# X,8&!7B+-$7\x9F+#%'\"%$\"# X\"# X\"+(%4\"6\u0121\"!!%$\"# X\"# X"), peg$decode("!!77+k$ \\!.J\"\"2J3K+-$77+#%'\"%$\"# X\"# X,>&!.J\"\"2J3K+-$77+#%'\"%$\"# X\"# X\"+#%'\"%$\"# X\"# X+! (%"), peg$decode("!7L*# \"7\x98+c$ \\!7B+-$7\xB7+#%'\"%$\"# X\"# X,8&!7B+-$7\xB7+#%'\"%$\"# X\"# X\"+'%4\"6\u0122\" %$\"# X\"# X"), peg$decode("7\xB8*# \"7\x9F"), peg$decode("!/\u0123\"\"1#3\u0124+<$7<+2%76+(%4#6\u0125#! %$## X$\"# X\"# X"), peg$decode("! \\7!+&$,#&7!\"\"\" X+' 4!6\u0126!! %"), peg$decode("!7\x9D+' 4!6\u0127!! %"), peg$decode("! \\7\x99,#&7\x99\"+\x81$7@+w%7M+m%7?+c% \\!7B+-$7\x9F+#%'\"%$\"# X\"# X,8&!7B+-$7\x9F+#%'\"%$\"# X\"# X\"+'%4%6\u0128% %$%# X$$# X$## X$\"# X\"# X"), peg$decode("7\xBD"), peg$decode("!/\u0129\"\"1&3\u012A+s$7.+i%7\xC0+_% \\!7A+-$7\xC0+#%'\"%$\"# X\"# X,8&!7A+-$7\xC0+#%'\"%$\"# X\"# X\"+#%'$%$$# X$## X$\"# X\"# X*# \"7\xBE"), peg$decode("!76+s$7.+i%7\xBF+_% \\!7A+-$7\xBF+#%'\"%$\"# X\"# X,8&!7A+-$7\xBF+#%'\"%$\"# X\"# X\"+#%'$%$$# X$## X$\"# X\"# X"), peg$decode("!76+=$7<+3%76*# \"7H+#%'#%$## X$\"# X\"# X"), peg$decode("7\xC1*G \"7\xC3*A \"7\xC5*; \"7\xC7*5 \"7\xC8*/ \"7\xC9*) \"7\xCA*# \"7\xBF"), peg$decode("!/\u012B\"\"1%3\u012C+7$7<+-%7\xC2+#%'#%$## X$\"# X\"# X"), peg$decode("!7I+' 4!6\u012D!! %"), peg$decode("!/\u012E\"\"1&3\u012F+\xA5$7<+\x9B%7D+\x91%7\xC4+\x87% \\! \\7'+&$,#&7'\"\"\" X+-$7\xC4+#%'\"%$\"# X\"# X,G&! \\7'+&$,#&7'\"\"\" X+-$7\xC4+#%'\"%$\"# X\"# X\"+-%7E+#%'&%$&# X$%# X$$# X$## X$\"# X\"# X"), peg$decode("7t*# \"7w"), peg$decode("!/\u0130\"\"1%3\u0131+7$7<+-%7\xC6+#%'#%$## X$\"# X\"# X"), peg$decode("!7I+' 4!6\u0132!! %"), peg$decode("!/\u0133\"\"1&3\u0134+<$7<+2%7I+(%4#6\u0135#! %$## X$\"# X\"# X"), peg$decode("!/\u0136\"\"1%3\u0137+_$7<+U%!/\u0138\"\"1$3\u0139+& 4!6\u013A! %*4 \"!/\u013B\"\"1%3\u013C+& 4!6\u013D! %+#%'#%$## X$\"# X\"# X"), peg$decode("!/\u013E\"\"1)3\u013F+T$7<+J%/\u0140\"\"1#3\u0141*/ \"/\u0142\"\"1(3\u0143*# \"76+(%4#6\u0144#! %$## X$\"# X\"# X"), peg$decode("!/\u0145\"\"1#3\u0146+\x9E$7<+\x94%7D+\x8A%!7\xCB+k$ \\!.D\"\"2D3E+-$7\xCB+#%'\"%$\"# X\"# X,>&!.D\"\"2D3E+-$7\xCB+#%'\"%$\"# X\"# X\"+#%'\"%$\"# X\"# X+-%7E+#%'%%$%# X$$# X$## X$\"# X\"# X"), peg$decode("!/\u0147\"\"1(3\u0148*/ \"/\u0149\"\"1$3\u014A*# \"76+' 4!6\u014B!! %"), peg$decode("!76+_$ \\!7A+-$76+#%'\"%$\"# X\"# X,8&!7A+-$76+#%'\"%$\"# X\"# X\"+#%'\"%$\"# X\"# X"), peg$decode("!7\xCE+K$7.+A%7\xCE+7%7.+-%7\x8F+#%'%%$%# X$$# X$## X$\"# X\"# X"), peg$decode("! \\7!+&$,#&7!\"\"\" X+' 4!6\u014C!! %"), peg$decode("!7\xD0+c$ \\!7A+-$7\xD0+#%'\"%$\"# X\"# X,8&!7A+-$7\xD0+#%'\"%$\"# X\"# X\"+'%4\"6\u014D\" %$\"# X\"# X"), peg$decode("!7\x98+c$ \\!7B+-$7\x9F+#%'\"%$\"# X\"# X,8&!7B+-$7\x9F+#%'\"%$\"# X\"# X\"+'%4\"6\u014E\" %$\"# X\"# X"), peg$decode("!7L*T \"7\x98*N \"!7@*# \" [+=$7t+3%7?*# \" [+#%'#%$## X$\"# X\"# X+c$ \\!7B+-$7\x9F+#%'\"%$\"# X\"# X,8&!7B+-$7\x9F+#%'\"%$\"# X\"# X\"+'%4\"6\u014F\" %$\"# X\"# X"), peg$decode("!7\xD3+c$ \\!7B+-$7\xD4+#%'\"%$\"# X\"# X,8&!7B+-$7\xD4+#%'\"%$\"# X\"# X\"+'%4\"6\u0150\" %$\"# X\"# X"), peg$decode("!7\x95+& 4!6\u0151! %"), peg$decode("!/\u0152\"\"1(3\u0153+<$7<+2%76+(%4#6\u0154#! %$## X$\"# X\"# X*j \"!/\u0155\"\"1&3\u0156+<$7<+2%76+(%4#6\u0157#! %$## X$\"# X\"# X*: \"!/\u0158\"\"1*3\u0159+& 4!6\u015A! %*# \"7\x9F"), peg$decode("!!76+o$ \\!7A+2$76+(%4\"6\u015B\"! %$\"# X\"# X,=&!7A+2$76+(%4\"6\u015B\"! %$\"# X\"# X\"+)%4\"6\u015C\"\"! %$\"# X\"# X*# \" [+' 4!6\u015D!! %"), peg$decode("!7\xD7+_$ \\!7A+-$7\xD7+#%'\"%$\"# X\"# X,8&!7A+-$7\xD7+#%'\"%$\"# X\"# X\"+#%'\"%$\"# X\"# X"), peg$decode("!7\x98+_$ \\!7B+-$7\x9F+#%'\"%$\"# X\"# X,8&!7B+-$7\x9F+#%'\"%$\"# X\"# X\"+#%'\"%$\"# X\"# X"), peg$decode("! \\7!+&$,#&7!\"\"\" X+' 4!6\u015E!! %"), peg$decode("!7\xDA+_$ \\!7B+-$7\xDB+#%'\"%$\"# X\"# X,8&!7B+-$7\xDB+#%'\"%$\"# X\"# X\"+#%'\"%$\"# X\"# X"), peg$decode("!/\u015F\"\"1&3\u0160*; \"/\u0161\"\"1'3\u0162*/ \"/\u0163\"\"1*3\u0164*# \"76+& 4!6\u0165! %"), peg$decode("!/\u0166\"\"1&3\u0167+<$7<+2%7\xDC+(%4#6\u0168#! %$## X$\"# X\"# X*\x83 \"!/\xF6\"\"1'3\xF7+<$7<+2%7\x9D+(%4#6\u0169#! %$## X$\"# X\"# X*S \"!/\u016A\"\"1+3\u016B+<$7<+2%7\x9D+(%4#6\u016C#! %$## X$\"# X\"# X*# \"7\x9F"), peg$decode("/\u016D\"\"1+3\u016E*k \"/\u016F\"\"1)3\u0170*_ \"/\u0171\"\"1(3\u0172*S \"/\u0173\"\"1'3\u0174*G \"/\u0175\"\"1&3\u0176*; \"/\u0177\"\"1*3\u0178*/ \"/\u0179\"\"1)3\u017A*# \"76"), peg$decode("71*# \" ["), peg$decode("!!76+o$ \\!7A+2$76+(%4\"6\u015B\"! %$\"# X\"# X,=&!7A+2$76+(%4\"6\u015B\"! %$\"# X\"# X\"+)%4\"6\u015C\"\"! %$\"# X\"# X*# \" [+' 4!6\u017B!! %"), peg$decode("!7L*# \"7\x98+c$ \\!7B+-$7\xE0+#%'\"%$\"# X\"# X,8&!7B+-$7\xE0+#%'\"%$\"# X\"# X\"+'%4\"6\u017C\" %$\"# X\"# X"), peg$decode("7\xB8*# \"7\x9F"), peg$decode("!7\xE2+_$ \\!7A+-$7\xE2+#%'\"%$\"# X\"# X,8&!7A+-$7\xE2+#%'\"%$\"# X\"# X\"+#%'\"%$\"# X\"# X"), peg$decode("!7\xE9+s$7.+i%7\xEC+_% \\!7B+-$7\xE3+#%'\"%$\"# X\"# X,8&!7B+-$7\xE3+#%'\"%$\"# X\"# X\"+#%'$%$$# X$## X$\"# X\"# X"), peg$decode("7\xE4*; \"7\xE5*5 \"7\xE6*/ \"7\xE7*) \"7\xE8*# \"7\x9F"), peg$decode("!/\u017D\"\"1#3\u017E+<$7<+2%7\xEF+(%4#6\u017F#! %$## X$\"# X\"# X"), peg$decode("!/\u0180\"\"1%3\u0181+<$7<+2%7T+(%4#6\u0182#! %$## X$\"# X\"# X"), peg$decode("!/\u0183\"\"1(3\u0184+B$7<+8%7\\*# \"7Y+(%4#6\u0185#! %$## X$\"# X\"# X"), peg$decode("!/\u0186\"\"1&3\u0187+<$7<+2%76+(%4#6\u0188#! %$## X$\"# X\"# X"), peg$decode("!/\u0189\"\"1%3\u018A+T$!7<+5$ \\7!,#&7!\"+#%'\"%$\"# X\"# X*# \" [+'%4\"6\u018B\" %$\"# X\"# X"), peg$decode("!7\xEA+K$7;+A%76+7%7;+-%7\xEB+#%'%%$%# X$$# X$## X$\"# X\"# X"), peg$decode("!/\x95\"\"1#3\xD6*# \"76+' 4!6\u018C!! %"), peg$decode("!/\xB4\"\"1#3\u018D*G \"/\xB6\"\"1#3\u018E*; \"/\xBA\"\"1#3\u018F*/ \"/\xB8\"\"1$3\u0190*# \"76+' 4!6\u0191!! %"), peg$decode("!7\xED+H$!7C+-$7\xEE+#%'\"%$\"# X\"# X*# \" [+#%'\"%$\"# X\"# X"), peg$decode("!7U*) \"7\\*# \"7X+& 4!6\u0192! %"), peg$decode("!!7!*# \" [+c$7!*# \" [+S%7!*# \" [+C%7!*# \" [+3%7!*# \" [+#%'%%$%# X$$# X$## X$\"# X\"# X+' 4!6\u0193!! %"), peg$decode("!!7!+C$7!*# \" [+3%7!*# \" [+#%'#%$## X$\"# X\"# X+' 4!6\u0194!! %"), peg$decode("7\xBD"), peg$decode("!7\x9D+d$ \\!7B+-$7\xF2+#%'\"%$\"# X\"# X,8&!7B+-$7\xF2+#%'\"%$\"# X\"# X\"+(%4\"6\u0195\"!!%$\"# X\"# X"), peg$decode("7\xF3*# \"7\x9F"), peg$decode("!.\u0196\"\"2\u01963\u0197+N$7<+D%.\u0198\"\"2\u01983\u0199*) \".\u019A\"\"2\u019A3\u019B+(%4#6\u019C#! %$## X$\"# X\"# X"), peg$decode("!7\x9D+d$ \\!7B+-$7\x9F+#%'\"%$\"# X\"# X,8&!7B+-$7\x9F+#%'\"%$\"# X\"# X\"+(%4\"6\u019D\"!!%$\"# X\"# X"), peg$decode("!76+7$70+-%7\xF6+#%'#%$## X$\"# X\"# X"), peg$decode(" \\72*) \"74*# \"7.,/&72*) \"74*# \"7.\""), peg$decode(" \\7%,#&7%\""), peg$decode("!7\xF9+=$.8\"\"2839+-%7\xFA+#%'#%$## X$\"# X\"# X"), peg$decode("!/\u019E\"\"1%3\u019F*) \"/\u01A0\"\"1$3\u01A1+' 4!6\u01A2!! %"), peg$decode("!7\xFB+N$!.8\"\"2839+-$7^+#%'\"%$\"# X\"# X*# \" [+#%'\"%$\"# X\"# X"), peg$decode("!7\\*) \"7X*# \"7\x82+' 4!6\u01A3!! %"), peg$decode("! \\7\xFD*) \"7-*# \"7\xFE,/&7\xFD*) \"7-*# \"7\xFE\"+! (%"), peg$decode("7\"*S \"7!*M \".F\"\"2F3G*A \".J\"\"2J3K*5 \".H\"\"2H3I*) \".N\"\"2N3O"), peg$decode(".L\"\"2L3M*\x95 \".B\"\"2B3C*\x89 \".<\"\"2<3=*} \".R\"\"2R3S*q \".T\"\"2T3U*e \".V\"\"2V3W*Y \".P\"\"2P3Q*M \".@\"\"2@3A*A \".D\"\"2D3E*5 \".2\"\"2233*) \".>\"\"2>3?"), peg$decode("!7\u0100+h$.8\"\"2839+X%7\xFA+N%!.\u01A4\"\"2\u01A43\u01A5+-$7\xEB+#%'\"%$\"# X\"# X*# \" [+#%'$%$$# X$## X$\"# X\"# X"), peg$decode("!/\u01A6\"\"1%3\u01A7*) \"/\u01A8\"\"1$3\u01A9+' 4!6\u01A2!! %"), peg$decode("!7\xEB+Q$/\xB4\"\"1#3\xB5*7 \"/\xB6\"\"1#3\xB7*+ \" \\7+,#&7+\"+'%4\"6\u01AA\" %$\"# X\"# X"), peg$decode("!7\u0104+\x8F$.F\"\"2F3G+%7\u0103+u%.F\"\"2F3G+e%7\u0103+[%.F\"\"2F3G+K%7\u0103+A%.F\"\"2F3G+1%7\u0105+'%4)6\u01AB) %$)# X$(# X$'# X$&# X$%# X$$# X$## X$\"# X\"# X"), peg$decode("!7#+A$7#+7%7#+-%7#+#%'$%$$# X$## X$\"# X\"# X"), peg$decode("!7\u0103+-$7\u0103+#%'\"%$\"# X\"# X"), peg$decode("!7\u0103+7$7\u0103+-%7\u0103+#%'#%$## X$\"# X\"# X") ], peg$currPos = 0, peg$reportedPos = 0, peg$cachedPos = 0, peg$cachedPosDetails = { line: 1, column: 1, seenCR: false }, peg$maxFailPos = 0, peg$maxFailExpected = [], peg$silentFails = 0, peg$result; if ("startRule" in options) { if (!(options.startRule in peg$startRuleIndices)) { throw new Error("Can't start parsing from rule \"" + options.startRule + "\"."); } peg$startRuleIndex = peg$startRuleIndices[options.startRule]; } function text() { return input.substring(peg$reportedPos, peg$currPos); } function offset() { return peg$reportedPos; } function line() { return peg$computePosDetails(peg$reportedPos).line; } function column() { return peg$computePosDetails(peg$reportedPos).column; } function expected(description) { throw peg$buildException( null, [{ type: "other", description: description }], peg$reportedPos ); } function error(message) { throw peg$buildException(message, null, peg$reportedPos); } function peg$computePosDetails(pos) { function advance(details, startPos, endPos) { var p, ch; for (p = startPos; p < endPos; p++) { ch = input.charAt(p); if (ch === "\n") { if (!details.seenCR) { details.line++; } details.column = 1; details.seenCR = false; } else if (ch === "\r" || ch === "\u2028" || ch === "\u2029") { details.line++; details.column = 1; details.seenCR = true; } else { details.column++; details.seenCR = false; } } } if (peg$cachedPos !== pos) { if (peg$cachedPos > pos) { peg$cachedPos = 0; peg$cachedPosDetails = { line: 1, column: 1, seenCR: false }; } advance(peg$cachedPosDetails, peg$cachedPos, pos); peg$cachedPos = pos; } return peg$cachedPosDetails; } function peg$fail(expected) { if (peg$currPos < peg$maxFailPos) { return; } if (peg$currPos > peg$maxFailPos) { peg$maxFailPos = peg$currPos; peg$maxFailExpected = []; } peg$maxFailExpected.push(expected); } function peg$buildException(message, expected, pos) { function cleanupExpected(expected) { var i = 1; expected.sort(function(a, b) { if (a.description < b.description) { return -1; } else if (a.description > b.description) { return 1; } else { return 0; } }); while (i < expected.length) { if (expected[i - 1] === expected[i]) { expected.splice(i, 1); } else { i++; } } } function buildMessage(expected, found) { function stringEscape(s) { function hex(ch) { return ch.charCodeAt(0).toString(16).toUpperCase(); } return s .replace(/\\/g, '\\\\') .replace(/"/g, '\\"') .replace(/\x08/g, '\\b') .replace(/\t/g, '\\t') .replace(/\n/g, '\\n') .replace(/\f/g, '\\f') .replace(/\r/g, '\\r') .replace(/[\x00-\x07\x0B\x0E\x0F]/g, function(ch) { return '\\x0' + hex(ch); }) .replace(/[\x10-\x1F\x80-\xFF]/g, function(ch) { return '\\x' + hex(ch); }) .replace(/[\u0180-\u0FFF]/g, function(ch) { return '\\u0' + hex(ch); }) .replace(/[\u1080-\uFFFF]/g, function(ch) { return '\\u' + hex(ch); }); } var expectedDescs = new Array(expected.length), expectedDesc, foundDesc, i; for (i = 0; i < expected.length; i++) { expectedDescs[i] = expected[i].description; } expectedDesc = expected.length > 1 ? expectedDescs.slice(0, -1).join(", ") + " or " + expectedDescs[expected.length - 1] : expectedDescs[0]; foundDesc = found ? "\"" + stringEscape(found) + "\"" : "end of input"; return "Expected " + expectedDesc + " but " + foundDesc + " found."; } var posDetails = peg$computePosDetails(pos), found = pos < input.length ? input.charAt(pos) : null; if (expected !== null) { cleanupExpected(expected); } return new SyntaxError( message !== null ? message : buildMessage(expected, found), expected, found, pos, posDetails.line, posDetails.column ); } function peg$decode(s) { var bc = new Array(s.length), i; for (i = 0; i < s.length; i++) { bc[i] = s.charCodeAt(i) - 32; } return bc; } function peg$parseRule(index) { var bc = peg$bytecode[index], ip = 0, ips = [], end = bc.length, ends = [], stack = [], params, i; function protect(object) { return Object.prototype.toString.apply(object) === "[object Array]" ? [] : object; } while (true) { while (ip < end) { switch (bc[ip]) { case 0: stack.push(protect(peg$consts[bc[ip + 1]])); ip += 2; break; case 1: stack.push(peg$currPos); ip++; break; case 2: stack.pop(); ip++; break; case 3: peg$currPos = stack.pop(); ip++; break; case 4: stack.length -= bc[ip + 1]; ip += 2; break; case 5: stack.splice(-2, 1); ip++; break; case 6: stack[stack.length - 2].push(stack.pop()); ip++; break; case 7: stack.push(stack.splice(stack.length - bc[ip + 1], bc[ip + 1])); ip += 2; break; case 8: stack.pop(); stack.push(input.substring(stack[stack.length - 1], peg$currPos)); ip++; break; case 9: ends.push(end); ips.push(ip + 3 + bc[ip + 1] + bc[ip + 2]); if (stack[stack.length - 1]) { end = ip + 3 + bc[ip + 1]; ip += 3; } else { end = ip + 3 + bc[ip + 1] + bc[ip + 2]; ip += 3 + bc[ip + 1]; } break; case 10: ends.push(end); ips.push(ip + 3 + bc[ip + 1] + bc[ip + 2]); if (stack[stack.length - 1] === peg$FAILED) { end = ip + 3 + bc[ip + 1]; ip += 3; } else { end = ip + 3 + bc[ip + 1] + bc[ip + 2]; ip += 3 + bc[ip + 1]; } break; case 11: ends.push(end); ips.push(ip + 3 + bc[ip + 1] + bc[ip + 2]); if (stack[stack.length - 1] !== peg$FAILED) { end = ip + 3 + bc[ip + 1]; ip += 3; } else { end = ip + 3 + bc[ip + 1] + bc[ip + 2]; ip += 3 + bc[ip + 1]; } break; case 12: if (stack[stack.length - 1] !== peg$FAILED) { ends.push(end); ips.push(ip); end = ip + 2 + bc[ip + 1]; ip += 2; } else { ip += 2 + bc[ip + 1]; } break; case 13: ends.push(end); ips.push(ip + 3 + bc[ip + 1] + bc[ip + 2]); if (input.length > peg$currPos) { end = ip + 3 + bc[ip + 1]; ip += 3; } else { end = ip + 3 + bc[ip + 1] + bc[ip + 2]; ip += 3 + bc[ip + 1]; } break; case 14: ends.push(end); ips.push(ip + 4 + bc[ip + 2] + bc[ip + 3]); if (input.substr(peg$currPos, peg$consts[bc[ip + 1]].length) === peg$consts[bc[ip + 1]]) { end = ip + 4 + bc[ip + 2]; ip += 4; } else { end = ip + 4 + bc[ip + 2] + bc[ip + 3]; ip += 4 + bc[ip + 2]; } break; case 15: ends.push(end); ips.push(ip + 4 + bc[ip + 2] + bc[ip + 3]); if (input.substr(peg$currPos, peg$consts[bc[ip + 1]].length).toLowerCase() === peg$consts[bc[ip + 1]]) { end = ip + 4 + bc[ip + 2]; ip += 4; } else { end = ip + 4 + bc[ip + 2] + bc[ip + 3]; ip += 4 + bc[ip + 2]; } break; case 16: ends.push(end); ips.push(ip + 4 + bc[ip + 2] + bc[ip + 3]); if (peg$consts[bc[ip + 1]].test(input.charAt(peg$currPos))) { end = ip + 4 + bc[ip + 2]; ip += 4; } else { end = ip + 4 + bc[ip + 2] + bc[ip + 3]; ip += 4 + bc[ip + 2]; } break; case 17: stack.push(input.substr(peg$currPos, bc[ip + 1])); peg$currPos += bc[ip + 1]; ip += 2; break; case 18: stack.push(peg$consts[bc[ip + 1]]); peg$currPos += peg$consts[bc[ip + 1]].length; ip += 2; break; case 19: stack.push(peg$FAILED); if (peg$silentFails === 0) { peg$fail(peg$consts[bc[ip + 1]]); } ip += 2; break; case 20: peg$reportedPos = stack[stack.length - 1 - bc[ip + 1]]; ip += 2; break; case 21: peg$reportedPos = peg$currPos; ip++; break; case 22: params = bc.slice(ip + 4, ip + 4 + bc[ip + 3]); for (i = 0; i < bc[ip + 3]; i++) { params[i] = stack[stack.length - 1 - params[i]]; } stack.splice( stack.length - bc[ip + 2], bc[ip + 2], peg$consts[bc[ip + 1]].apply(null, params) ); ip += 4 + bc[ip + 3]; break; case 23: stack.push(peg$parseRule(bc[ip + 1])); ip += 2; break; case 24: peg$silentFails++; ip++; break; case 25: peg$silentFails--; ip++; break; default: throw new Error("Invalid opcode: " + bc[ip] + "."); } } if (ends.length > 0) { end = ends.pop(); ip = ips.pop(); } else { break; } } return stack[0]; } options.data = {}; // Object to which header attributes will be assigned during parsing function list (first, rest) { return [first].concat(rest); } peg$result = peg$parseRule(peg$startRuleIndex); if (peg$result !== peg$FAILED && peg$currPos === input.length) { return peg$result; } else { if (peg$result !== peg$FAILED && peg$currPos < input.length) { peg$fail({ type: "end", description: "end of input" }); } throw peg$buildException(null, peg$maxFailExpected, peg$maxFailPos); } } return { SyntaxError: SyntaxError, parse: parse }; })(); },{}],12:[function(require,module,exports){ "use strict"; /** * @fileoverview Hacks - This file contains all of the things we * wish we didn't have to do, just for interop. It is similar to * Utils, which provides actually useful and relevant functions for * a SIP library. Methods in this file are grouped by vendor, so * as to most easily track when particular hacks may not be necessary anymore. */ module.exports = function (SIP) { //keep to quiet jshint, and remain consistent with other files SIP = SIP; var Hacks = { AllBrowsers: { maskDtls: function (sdp) { if (sdp) { sdp = sdp.replace(/ UDP\/TLS\/RTP\/SAVP/gmi, " RTP/SAVP"); } return sdp; }, unmaskDtls: function (sdp) { /** * Chrome does not handle DTLS correctly (Canaray does, but not production) * keeping Chrome as SDES until DTLS is fixed (comment out 'is_opera' condition) * * UPDATE: May 21, 2014 * Chrome 35 now properly defaults to DTLS. Only Opera remains using SDES * * UPDATE: 2014-09-24 * Opera now supports DTLS by default as well. * **/ return sdp.replace(/ RTP\/SAVP/gmi, " UDP/TLS/RTP/SAVP"); } }, Firefox: { /* Condition to detect if hacks are applicable */ isFirefox: function () { return typeof mozRTCPeerConnection !== 'undefined'; }, cannotHandleExtraWhitespace: function (sdp) { if (this.isFirefox() && sdp) { sdp = sdp.replace(/ \r\n/g, "\r\n"); } return sdp; }, hasMissingCLineInSDP: function (sdp) { /* * This is a Firefox hack to insert valid sdp when getDescription is * called with the constraint offerToReceiveVideo = false. * We search for either a c-line at the top of the sdp above all * m-lines. If that does not exist then we search for a c-line * beneath each m-line. If it is missing a c-line, we insert * a fake c-line with the ip address 0.0.0.0. This is then valid * sdp and no media will be sent for that m-line. * * Valid SDP is: * m= * i= * c= */ var insertAt, mlines; if (sdp.indexOf('c=') > sdp.indexOf('m=')) { // Find all m= lines mlines = sdp.match(/m=.*\r\n.*/g); for (var i=0; i= 0) { insertAt = sdp.indexOf(mlines[i].toString())+mlines[i].toString().length; if (sdp.substr(insertAt,2)!=='c=') { sdp = sdp.substr(0,insertAt) + '\r\nc=IN IP4 0.0.0.0' + sdp.substr(insertAt); } // else add the C line if it's missing } else if (mlines[i].toString().search(/c=.*/) < 0) { insertAt = sdp.indexOf(mlines[i].toString().match(/.*/))+mlines[i].toString().match(/.*/).toString().length; sdp = sdp.substr(0,insertAt) + '\r\nc=IN IP4 0.0.0.0' + sdp.substr(insertAt); } } } return sdp; }, }, Chrome: { needsExplicitlyInactiveSDP: function (sdp) { var sub, index; if (Hacks.Firefox.isFirefox()) { // Fix this in Firefox before sending index = sdp.indexOf('m=video 0'); if (index !== -1) { sub = sdp.substr(index); sub = sub.replace(/\r\nc=IN IP4.*\r\n$/, '\r\nc=IN IP4 0.0.0.0\r\na=inactive\r\n'); return sdp.substr(0, index) + sub; } } return sdp; }, getsConfusedAboutGUM: function (session) { if (session.mediaHandler) { session.mediaHandler.close(); } } } }; return Hacks; }; },{}],13:[function(require,module,exports){ "use strict"; var levels = { 'error': 0, 'warn': 1, 'log': 2, 'debug': 3 }; module.exports = function (console) { var LoggerFactory = function () { var logger, level = 2, builtinEnabled = true, connector = null; this.loggers = {}; logger = this.getLogger('sip.loggerfactory'); Object.defineProperties(this, { builtinEnabled: { get: function(){ return builtinEnabled; }, set: function(value){ if (typeof value === 'boolean') { builtinEnabled = value; } else { logger.error('invalid "builtinEnabled" parameter value: '+ JSON.stringify(value)); } } }, level: { get: function() {return level; }, set: function(value) { if (value >= 0 && value <=3) { level = value; } else if (value > 3) { level = 3; } else if (levels.hasOwnProperty(value)) { level = levels[value]; } else { logger.error('invalid "level" parameter value: '+ JSON.stringify(value)); } } }, connector: { get: function() {return connector; }, set: function(value){ if(value === null || value === "" || value === undefined) { connector = null; } else if (typeof value === 'function') { connector = value; } else { logger.error('invalid "connector" parameter value: '+ JSON.stringify(value)); } } } }); }; LoggerFactory.prototype.print = function(target, category, label, content) { if (typeof content === 'string') { var prefix = [new Date(), category]; if (label) { prefix.push(label); } content = prefix.concat(content).join(' | '); } target.call(console, content); }; function Logger (logger, category, label) { this.logger = logger; this.category = category; this.label = label; } Object.keys(levels).forEach(function (targetName) { Logger.prototype[targetName] = function (content) { this.logger[targetName](this.category, this.label, content); }; LoggerFactory.prototype[targetName] = function (category, label, content) { if (this.level >= levels[targetName]) { if (this.builtinEnabled) { this.print(console[targetName], category, label, content); } if (this.connector) { this.connector(targetName, category, label, content); } } }; }); LoggerFactory.prototype.getLogger = function(category, label) { var logger; if (label && this.level === 3) { return new Logger(this, category, label); } else if (this.loggers[category]) { return this.loggers[category]; } else { logger = new Logger(this, category); this.loggers[category] = logger; return logger; } }; return LoggerFactory; }; },{}],14:[function(require,module,exports){ "use strict"; /** * @fileoverview MediaHandler */ /* MediaHandler * @class PeerConnection helper Class. * @param {SIP.Session} session * @param {Object} [options] */ module.exports = function (EventEmitter) { var MediaHandler = function(session, options) { // keep jshint happy session = session; options = options; }; MediaHandler.prototype = Object.create(EventEmitter.prototype, { isReady: {value: function isReady () {}}, close: {value: function close () {}}, /** * @param {Object} [mediaHint] A custom object describing the media to be used during this session. */ getDescription: {value: function getDescription (mediaHint) { // keep jshint happy mediaHint = mediaHint; }}, /** * Check if a SIP message contains a session description. * @param {SIP.SIPMessage} message * @returns {boolean} */ hasDescription: {value: function hasDescription (message) { // keep jshint happy message = message; }}, /** * Set the session description contained in a SIP message. * @param {SIP.SIPMessage} message * @returns {Promise} */ setDescription: {value: function setDescription (message) { // keep jshint happy message = message; }} }); return MediaHandler; }; },{}],15:[function(require,module,exports){ "use strict"; /** * @fileoverview SIP NameAddrHeader */ /** * @augments SIP * @class Class creating a Name Address SIP header. * * @param {SIP.URI} uri * @param {String} [displayName] * @param {Object} [parameters] * */ module.exports = function (SIP) { var NameAddrHeader; NameAddrHeader = function(uri, displayName, parameters) { var param; // Checks if(!uri || !(uri instanceof SIP.URI)) { throw new TypeError('missing or invalid "uri" parameter'); } // Initialize parameters this.uri = uri; this.parameters = {}; for (param in parameters) { this.setParam(param, parameters[param]); } Object.defineProperties(this, { friendlyName: { get: function() { return this.displayName || uri.aor; } }, displayName: { get: function() { return displayName; }, set: function(value) { displayName = (value === 0) ? '0' : value; } } }); }; NameAddrHeader.prototype = { setParam: function (key, value) { if(key) { this.parameters[key.toLowerCase()] = (typeof value === 'undefined' || value === null) ? null : value.toString(); } }, getParam: SIP.URI.prototype.getParam, hasParam: SIP.URI.prototype.hasParam, deleteParam: SIP.URI.prototype.deleteParam, clearParams: SIP.URI.prototype.clearParams, clone: function() { return new NameAddrHeader( this.uri.clone(), this.displayName, JSON.parse(JSON.stringify(this.parameters))); }, toString: function() { var body, parameter; body = (this.displayName || this.displayName === 0) ? '"' + this.displayName + '" ' : ''; body += '<' + this.uri.toString() + '>'; for (parameter in this.parameters) { body += ';' + parameter; if (this.parameters[parameter] !== null) { body += '='+ this.parameters[parameter]; } } return body; } }; /** * Parse the given string and returns a SIP.NameAddrHeader instance or undefined if * it is an invalid NameAddrHeader. * @public * @param {String} name_addr_header */ NameAddrHeader.parse = function(name_addr_header) { name_addr_header = SIP.Grammar.parse(name_addr_header,'Name_Addr_Header'); if (name_addr_header !== -1) { return name_addr_header; } else { return undefined; } }; SIP.NameAddrHeader = NameAddrHeader; }; },{}],16:[function(require,module,exports){ "use strict"; /** * @fileoverview SIP Message Parser */ /** * Extract and parse every header of a SIP message. * @augments SIP * @namespace */ module.exports = function (SIP) { var Parser; function getHeader(data, headerStart) { var // 'start' position of the header. start = headerStart, // 'end' position of the header. end = 0, // 'partial end' position of the header. partialEnd = 0; //End of message. if (data.substring(start, start + 2).match(/(^\r\n)/)) { return -2; } while(end === 0) { // Partial End of Header. partialEnd = data.indexOf('\r\n', start); // 'indexOf' returns -1 if the value to be found never occurs. if (partialEnd === -1) { return partialEnd; } if(!data.substring(partialEnd + 2, partialEnd + 4).match(/(^\r\n)/) && data.charAt(partialEnd + 2).match(/(^\s+)/)) { // Not the end of the message. Continue from the next position. start = partialEnd + 2; } else { end = partialEnd; } } return end; } function parseHeader(message, data, headerStart, headerEnd) { var header, idx, length, parsed, hcolonIndex = data.indexOf(':', headerStart), headerName = data.substring(headerStart, hcolonIndex).trim(), headerValue = data.substring(hcolonIndex + 1, headerEnd).trim(); // If header-field is well-known, parse it. switch(headerName.toLowerCase()) { case 'via': case 'v': message.addHeader('via', headerValue); if(message.getHeaders('via').length === 1) { parsed = message.parseHeader('Via'); if(parsed) { message.via = parsed; message.via_branch = parsed.branch; } } else { parsed = 0; } break; case 'from': case 'f': message.setHeader('from', headerValue); parsed = message.parseHeader('from'); if(parsed) { message.from = parsed; message.from_tag = parsed.getParam('tag'); } break; case 'to': case 't': message.setHeader('to', headerValue); parsed = message.parseHeader('to'); if(parsed) { message.to = parsed; message.to_tag = parsed.getParam('tag'); } break; case 'record-route': parsed = SIP.Grammar.parse(headerValue, 'Record_Route'); if (parsed === -1) { parsed = undefined; break; } length = parsed.length; for (idx = 0; idx < length; idx++) { header = parsed[idx]; message.addHeader('record-route', headerValue.substring(header.position, header.offset)); message.headers['Record-Route'][message.getHeaders('record-route').length - 1].parsed = header.parsed; } break; case 'call-id': case 'i': message.setHeader('call-id', headerValue); parsed = message.parseHeader('call-id'); if(parsed) { message.call_id = headerValue; } break; case 'contact': case 'm': parsed = SIP.Grammar.parse(headerValue, 'Contact'); if (parsed === -1) { parsed = undefined; break; } length = parsed.length; for (idx = 0; idx < length; idx++) { header = parsed[idx]; message.addHeader('contact', headerValue.substring(header.position, header.offset)); message.headers['Contact'][message.getHeaders('contact').length - 1].parsed = header.parsed; } break; case 'content-length': case 'l': message.setHeader('content-length', headerValue); parsed = message.parseHeader('content-length'); break; case 'content-type': case 'c': message.setHeader('content-type', headerValue); parsed = message.parseHeader('content-type'); break; case 'cseq': message.setHeader('cseq', headerValue); parsed = message.parseHeader('cseq'); if(parsed) { message.cseq = parsed.value; } if(message instanceof SIP.IncomingResponse) { message.method = parsed.method; } break; case 'max-forwards': message.setHeader('max-forwards', headerValue); parsed = message.parseHeader('max-forwards'); break; case 'www-authenticate': message.setHeader('www-authenticate', headerValue); parsed = message.parseHeader('www-authenticate'); break; case 'proxy-authenticate': message.setHeader('proxy-authenticate', headerValue); parsed = message.parseHeader('proxy-authenticate'); break; case 'refer-to': case 'r': message.setHeader('refer-to', headerValue); parsed = message.parseHeader('refer-to'); if (parsed) { message.refer_to = parsed; } break; default: // Do not parse this header. message.setHeader(headerName, headerValue); parsed = 0; } if (parsed === undefined) { return { error: 'error parsing header "'+ headerName +'"' }; } else { return true; } } /** Parse SIP Message * @function * @param {String} message SIP message. * @param {Object} logger object. * @returns {SIP.IncomingRequest|SIP.IncomingResponse|undefined} */ Parser = {}; Parser.parseMessage = function(data, ua) { var message, firstLine, contentLength, bodyStart, parsed, headerStart = 0, headerEnd = data.indexOf('\r\n'), logger = ua.getLogger('sip.parser'); if(headerEnd === -1) { logger.warn('no CRLF found, not a SIP message, discarded'); return; } // Parse first line. Check if it is a Request or a Reply. firstLine = data.substring(0, headerEnd); parsed = SIP.Grammar.parse(firstLine, 'Request_Response'); if(parsed === -1) { logger.warn('error parsing first line of SIP message: "' + firstLine + '"'); return; } else if(!parsed.status_code) { message = new SIP.IncomingRequest(ua); message.method = parsed.method; message.ruri = parsed.uri; } else { message = new SIP.IncomingResponse(ua); message.status_code = parsed.status_code; message.reason_phrase = parsed.reason_phrase; } message.data = data; headerStart = headerEnd + 2; /* Loop over every line in data. Detect the end of each header and parse * it or simply add to the headers collection. */ while(true) { headerEnd = getHeader(data, headerStart); // The SIP message has normally finished. if(headerEnd === -2) { bodyStart = headerStart + 2; break; } // data.indexOf returned -1 due to a malformed message. else if(headerEnd === -1) { logger.error('malformed message'); return; } parsed = parseHeader(message, data, headerStart, headerEnd); if(parsed !== true) { logger.error(parsed.error); return; } headerStart = headerEnd + 2; } /* RFC3261 18.3. * If there are additional bytes in the transport packet * beyond the end of the body, they MUST be discarded. */ if(message.hasHeader('content-length')) { contentLength = message.getHeader('content-length'); message.body = data.substr(bodyStart, contentLength); } else { message.body = data.substring(bodyStart); } return message; }; SIP.Parser = Parser; }; },{}],17:[function(require,module,exports){ "use strict"; module.exports = function (SIP) { var RegisterContext; RegisterContext = function (ua) { var params = {}, regId = 1; this.registrar = ua.configuration.registrarServer; this.expires = ua.configuration.registerExpires; // Contact header this.contact = ua.contact.toString(); if(regId) { this.contact += ';reg-id='+ regId; this.contact += ';+sip.instance=""'; } // Call-ID and CSeq values RFC3261 10.2 this.call_id = SIP.Utils.createRandomToken(22); this.cseq = 80; this.to_uri = ua.configuration.uri; params.to_uri = this.to_uri; params.to_displayName = ua.configuration.displayName; params.call_id = this.call_id; params.cseq = this.cseq; // Extends ClientContext SIP.Utils.augment(this, SIP.ClientContext, [ua, 'REGISTER', this.registrar, {params: params}]); this.registrationTimer = null; this.registrationExpiredTimer = null; // Set status this.registered = false; this.logger = ua.getLogger('sip.registercontext'); }; RegisterContext.prototype = { register: function (options) { var self = this, extraHeaders; // Handle Options this.options = options || {}; extraHeaders = (this.options.extraHeaders || []).slice(); extraHeaders.push('Contact: ' + this.contact + ';expires=' + this.expires); extraHeaders.push('Allow: ' + SIP.UA.C.ALLOWED_METHODS.toString()); // Save original extraHeaders to be used in .close this.closeHeaders = this.options.closeWithHeaders ? (this.options.extraHeaders || []).slice() : []; this.receiveResponse = function(response) { var contact, expires, contacts = response.getHeaders('contact').length, cause; // Discard responses to older REGISTER/un-REGISTER requests. if(response.cseq !== this.cseq) { return; } // Clear registration timer if (this.registrationTimer !== null) { SIP.Timers.clearTimeout(this.registrationTimer); this.registrationTimer = null; } switch(true) { case /^1[0-9]{2}$/.test(response.status_code): this.emit('progress', response); break; case /^2[0-9]{2}$/.test(response.status_code): this.emit('accepted', response); if(response.hasHeader('expires')) { expires = response.getHeader('expires'); } if (this.registrationExpiredTimer !== null) { SIP.Timers.clearTimeout(this.registrationExpiredTimer); this.registrationExpiredTimer = null; } // Search the Contact pointing to us and update the expires value accordingly. if (!contacts) { this.logger.warn('no Contact header in response to REGISTER, response ignored'); break; } while(contacts--) { contact = response.parseHeader('contact', contacts); if(contact.uri.user === this.ua.contact.uri.user) { expires = contact.getParam('expires'); break; } else { contact = null; } } if (!contact) { this.logger.warn('no Contact header pointing to us, response ignored'); break; } if(!expires) { expires = this.expires; } // Re-Register before the expiration interval has elapsed. // For that, decrease the expires value. ie: 3 seconds this.registrationTimer = SIP.Timers.setTimeout(function() { self.registrationTimer = null; self.register(self.options); }, (expires * 1000) - 3000); this.registrationExpiredTimer = SIP.Timers.setTimeout(function () { self.logger.warn('registration expired'); if (self.registered) { self.unregistered(null, SIP.C.causes.EXPIRES); } }, expires * 1000); //Save gruu values if (contact.hasParam('temp-gruu')) { this.ua.contact.temp_gruu = SIP.URI.parse(contact.getParam('temp-gruu').replace(/"/g,'')); } if (contact.hasParam('pub-gruu')) { this.ua.contact.pub_gruu = SIP.URI.parse(contact.getParam('pub-gruu').replace(/"/g,'')); } this.registered = true; this.emit('registered', response || null); break; // Interval too brief RFC3261 10.2.8 case /^423$/.test(response.status_code): if(response.hasHeader('min-expires')) { // Increase our registration interval to the suggested minimum this.expires = response.getHeader('min-expires'); // Attempt the registration again immediately this.register(this.options); } else { //This response MUST contain a Min-Expires header field this.logger.warn('423 response received for REGISTER without Min-Expires'); this.registrationFailure(response, SIP.C.causes.SIP_FAILURE_CODE); } break; default: cause = SIP.Utils.sipErrorCause(response.status_code); this.registrationFailure(response, cause); } }; this.onRequestTimeout = function() { this.registrationFailure(null, SIP.C.causes.REQUEST_TIMEOUT); }; this.onTransportError = function() { this.registrationFailure(null, SIP.C.causes.CONNECTION_ERROR); }; this.cseq++; this.request.cseq = this.cseq; this.request.setHeader('cseq', this.cseq + ' REGISTER'); this.request.extraHeaders = extraHeaders; this.send(); }, registrationFailure: function (response, cause) { this.emit('failed', response || null, cause || null); }, onTransportClosed: function() { this.registered_before = this.registered; if (this.registrationTimer !== null) { SIP.Timers.clearTimeout(this.registrationTimer); this.registrationTimer = null; } if (this.registrationExpiredTimer !== null) { SIP.Timers.clearTimeout(this.registrationExpiredTimer); this.registrationExpiredTimer = null; } if(this.registered) { this.unregistered(null, SIP.C.causes.CONNECTION_ERROR); } }, onTransportConnected: function() { this.register(this.options); }, close: function() { var options = { all: false, extraHeaders: this.closeHeaders }; this.registered_before = this.registered; this.unregister(options); }, unregister: function(options) { var extraHeaders; options = options || {}; if(!this.registered && !options.all) { this.logger.warn('already unregistered'); return; } extraHeaders = (options.extraHeaders || []).slice(); this.registered = false; // Clear the registration timer. if (this.registrationTimer !== null) { SIP.Timers.clearTimeout(this.registrationTimer); this.registrationTimer = null; } if(options.all) { extraHeaders.push('Contact: *'); extraHeaders.push('Expires: 0'); } else { extraHeaders.push('Contact: '+ this.contact + ';expires=0'); } this.receiveResponse = function(response) { var cause; switch(true) { case /^1[0-9]{2}$/.test(response.status_code): this.emit('progress', response); break; case /^2[0-9]{2}$/.test(response.status_code): this.emit('accepted', response); if (this.registrationExpiredTimer !== null) { SIP.Timers.clearTimeout(this.registrationExpiredTimer); this.registrationExpiredTimer = null; } this.unregistered(response); break; default: cause = SIP.Utils.sipErrorCause(response.status_code); this.unregistered(response,cause); } }; this.onRequestTimeout = function() { // Not actually unregistered... //this.unregistered(null, SIP.C.causes.REQUEST_TIMEOUT); }; this.onTransportError = function() { // Not actually unregistered... //this.unregistered(null, SIP.C.causes.CONNECTION_ERROR); }; this.cseq++; this.request.cseq = this.cseq; this.request.setHeader('cseq', this.cseq + ' REGISTER'); this.request.extraHeaders = extraHeaders; this.send(); }, unregistered: function(response, cause) { this.registered = false; this.emit('unregistered', response || null, cause || null); } }; SIP.RegisterContext = RegisterContext; }; },{}],18:[function(require,module,exports){ "use strict"; /** * @fileoverview Request Sender */ /** * @augments SIP * @class Class creating a request sender. * @param {Object} applicant * @param {SIP.UA} ua */ module.exports = function (SIP) { var RequestSender; RequestSender = function(applicant, ua) { this.logger = ua.getLogger('sip.requestsender'); this.ua = ua; this.applicant = applicant; this.method = applicant.request.method; this.request = applicant.request; this.credentials = null; this.challenged = false; this.staled = false; // If ua is in closing process or even closed just allow sending Bye and ACK if (ua.status === SIP.UA.C.STATUS_USER_CLOSED && (this.method !== SIP.C.BYE || this.method !== SIP.C.ACK)) { this.onTransportError(); } }; /** * Create the client transaction and send the message. */ RequestSender.prototype = { send: function() { switch(this.method) { case "INVITE": this.clientTransaction = new SIP.Transactions.InviteClientTransaction(this, this.request, this.ua.transport); break; case "ACK": this.clientTransaction = new SIP.Transactions.AckClientTransaction(this, this.request, this.ua.transport); break; default: this.clientTransaction = new SIP.Transactions.NonInviteClientTransaction(this, this.request, this.ua.transport); } this.clientTransaction.send(); return this.clientTransaction; }, /** * Callback fired when receiving a request timeout error from the client transaction. * To be re-defined by the applicant. * @event */ onRequestTimeout: function() { this.applicant.onRequestTimeout(); }, /** * Callback fired when receiving a transport error from the client transaction. * To be re-defined by the applicant. * @event */ onTransportError: function() { this.applicant.onTransportError(); }, /** * Called from client transaction when receiving a correct response to the request. * Authenticate request if needed or pass the response back to the applicant. * @param {SIP.IncomingResponse} response */ receiveResponse: function(response) { var cseq, challenge, authorization_header_name, status_code = response.status_code; /* * Authentication * Authenticate once. _challenged_ flag used to avoid infinite authentications. */ if (status_code === 401 || status_code === 407) { // Get and parse the appropriate WWW-Authenticate or Proxy-Authenticate header. if (response.status_code === 401) { challenge = response.parseHeader('www-authenticate'); authorization_header_name = 'authorization'; } else { challenge = response.parseHeader('proxy-authenticate'); authorization_header_name = 'proxy-authorization'; } // Verify it seems a valid challenge. if (! challenge) { this.logger.warn(response.status_code + ' with wrong or missing challenge, cannot authenticate'); this.applicant.receiveResponse(response); return; } if (!this.challenged || (!this.staled && challenge.stale === true)) { if (!this.credentials) { this.credentials = this.ua.configuration.authenticationFactory(this.ua); } // Verify that the challenge is really valid. if (!this.credentials.authenticate(this.request, challenge)) { this.applicant.receiveResponse(response); return; } this.challenged = true; if (challenge.stale) { this.staled = true; } if (response.method === SIP.C.REGISTER) { cseq = this.applicant.cseq += 1; } else if (this.request.dialog){ cseq = this.request.dialog.local_seqnum += 1; } else { cseq = this.request.cseq + 1; this.request.cseq = cseq; } this.request.setHeader('cseq', cseq +' '+ this.method); this.request.setHeader(authorization_header_name, this.credentials.toString()); this.send(); } else { this.applicant.receiveResponse(response); } } else { this.applicant.receiveResponse(response); } } }; SIP.RequestSender = RequestSender; }; },{}],19:[function(require,module,exports){ /** * @name SIP * @namespace */ "use strict"; module.exports = function (environment) { var pkg = require('../package.json'), version = pkg.version, title = pkg.title; var SIP = Object.defineProperties({}, { version: { get: function(){ return version; } }, name: { get: function(){ return title; } } }); require('./Utils')(SIP, environment); SIP.LoggerFactory = require('./LoggerFactory')(environment.console); SIP.EventEmitter = require('./EventEmitter')(environment.console); SIP.C = require('./Constants')(SIP.name, SIP.version); SIP.Exceptions = require('./Exceptions'); SIP.Timers = require('./Timers')(environment.timers); SIP.Transport = environment.Transport(SIP, environment.WebSocket); require('./Parser')(SIP); require('./SIPMessage')(SIP); require('./URI')(SIP); require('./NameAddrHeader')(SIP); require('./Transactions')(SIP); require('./Dialogs')(SIP); require('./RequestSender')(SIP); require('./RegisterContext')(SIP); SIP.MediaHandler = require('./MediaHandler')(SIP.EventEmitter); require('./ClientContext')(SIP); require('./ServerContext')(SIP); require('./Session')(SIP, environment); require('./Subscription')(SIP); SIP.WebRTC = require('./WebRTC')(SIP, environment); require('./UA')(SIP, environment); SIP.Hacks = require('./Hacks')(SIP); require('./SanityCheck')(SIP); SIP.DigestAuthentication = require('./DigestAuthentication')(SIP.Utils); SIP.Grammar = require('./Grammar')(SIP); return SIP; }; },{"../package.json":2,"./ClientContext":3,"./Constants":4,"./Dialogs":6,"./DigestAuthentication":7,"./EventEmitter":8,"./Exceptions":9,"./Grammar":10,"./Hacks":12,"./LoggerFactory":13,"./MediaHandler":14,"./NameAddrHeader":15,"./Parser":16,"./RegisterContext":17,"./RequestSender":18,"./SIPMessage":20,"./SanityCheck":21,"./ServerContext":22,"./Session":23,"./Subscription":25,"./Timers":26,"./Transactions":27,"./UA":29,"./URI":30,"./Utils":31,"./WebRTC":32}],20:[function(require,module,exports){ "use strict"; /** * @fileoverview SIP Message */ module.exports = function (SIP) { var OutgoingRequest, IncomingMessage, IncomingRequest, IncomingResponse; function getSupportedHeader (request) { var allowUnregistered = request.ua.configuration.hackAllowUnregisteredOptionTags; var optionTags = []; var optionTagSet = {}; if (request.method === SIP.C.REGISTER) { optionTags.push('path', 'gruu'); } else if (request.method === SIP.C.INVITE && (request.ua.contact.pub_gruu || request.ua.contact.temp_gruu)) { optionTags.push('gruu'); } if (request.ua.configuration.rel100 === SIP.C.supported.SUPPORTED) { optionTags.push('100rel'); } if (request.ua.configuration.replaces === SIP.C.supported.SUPPORTED) { optionTags.push('replaces'); } optionTags.push('outbound'); optionTags = optionTags.concat(request.ua.configuration.extraSupported); optionTags = optionTags.filter(function(optionTag) { var registered = SIP.C.OPTION_TAGS[optionTag]; var unique = !optionTagSet[optionTag]; optionTagSet[optionTag] = true; return (registered || allowUnregistered) && unique; }); return 'Supported: ' + optionTags.join(', ') + '\r\n'; } /** * @augments SIP * @class Class for outgoing SIP request. * @param {String} method request method * @param {String} ruri request uri * @param {SIP.UA} ua * @param {Object} params parameters that will have priority over ua.configuration parameters: *
* - cseq, call_id, from_tag, from_uri, from_displayName, to_uri, to_tag, route_set * @param {Object} [headers] extra headers * @param {String} [body] */ OutgoingRequest = function(method, ruri, ua, params, extraHeaders, body) { var to, from, call_id, cseq, to_uri, from_uri; params = params || {}; // Mandatory parameters check if(!method || !ruri || !ua) { return null; } this.logger = ua.getLogger('sip.sipmessage'); this.ua = ua; this.headers = {}; this.method = method; this.ruri = ruri; this.body = body; this.extraHeaders = (extraHeaders || []).slice(); this.statusCode = params.status_code; this.reasonPhrase = params.reason_phrase; // Fill the Common SIP Request Headers // Route if (params.route_set) { this.setHeader('route', params.route_set); } else if (ua.configuration.usePreloadedRoute){ this.setHeader('route', ua.transport.server.sip_uri); } // Via // Empty Via header. Will be filled by the client transaction. this.setHeader('via', ''); // Max-Forwards this.setHeader('max-forwards', SIP.UA.C.MAX_FORWARDS); // To to_uri = params.to_uri || ruri; to = (params.to_displayName || params.to_displayName === 0) ? '"' + params.to_displayName + '" ' : ''; to += '<' + (to_uri && to_uri.toRaw ? to_uri.toRaw() : to_uri) + '>'; to += params.to_tag ? ';tag=' + params.to_tag : ''; this.to = new SIP.NameAddrHeader.parse(to); this.setHeader('to', to); // From from_uri = params.from_uri || ua.configuration.uri; if (params.from_displayName || params.from_displayName === 0) { from = '"' + params.from_displayName + '" '; } else if (ua.configuration.displayName) { from = '"' + ua.configuration.displayName + '" '; } else { from = ''; } from += '<' + (from_uri && from_uri.toRaw ? from_uri.toRaw() : from_uri) + '>;tag='; from += params.from_tag || SIP.Utils.newTag(); this.from = new SIP.NameAddrHeader.parse(from); this.setHeader('from', from); // Call-ID call_id = params.call_id || (ua.configuration.sipjsId + SIP.Utils.createRandomToken(15)); this.call_id = call_id; this.setHeader('call-id', call_id); // CSeq cseq = params.cseq || Math.floor(Math.random() * 10000); this.cseq = cseq; this.setHeader('cseq', cseq + ' ' + method); }; OutgoingRequest.prototype = { /** * Replace the the given header by the given value. * @param {String} name header name * @param {String | Array} value header value */ setHeader: function(name, value) { this.headers[SIP.Utils.headerize(name)] = (value instanceof Array) ? value : [value]; }, /** * Get the value of the given header name at the given position. * @param {String} name header name * @returns {String|undefined} Returns the specified header, undefined if header doesn't exist. */ getHeader: function(name) { var regexp, idx, length = this.extraHeaders.length, header = this.headers[SIP.Utils.headerize(name)]; if(header) { if(header[0]) { return header[0]; } } else { regexp = new RegExp('^\\s*' + name + '\\s*:','i'); for (idx = 0; idx < length; idx++) { header = this.extraHeaders[idx]; if (regexp.test(header)) { return header.substring(header.indexOf(':')+1).trim(); } } } return; }, /** * Get the header/s of the given name. * @param {String} name header name * @returns {Array} Array with all the headers of the specified name. */ getHeaders: function(name) { var idx, length, regexp, header = this.headers[SIP.Utils.headerize(name)], result = []; if(header) { length = header.length; for (idx = 0; idx < length; idx++) { result.push(header[idx]); } return result; } else { length = this.extraHeaders.length; regexp = new RegExp('^\\s*' + name + '\\s*:','i'); for (idx = 0; idx < length; idx++) { header = this.extraHeaders[idx]; if (regexp.test(header)) { result.push(header.substring(header.indexOf(':')+1).trim()); } } return result; } }, /** * Verify the existence of the given header. * @param {String} name header name * @returns {boolean} true if header with given name exists, false otherwise */ hasHeader: function(name) { var regexp, idx, length = this.extraHeaders.length; if (this.headers[SIP.Utils.headerize(name)]) { return true; } else { regexp = new RegExp('^\\s*' + name + '\\s*:','i'); for (idx = 0; idx < length; idx++) { if (regexp.test(this.extraHeaders[idx])) { return true; } } } return false; }, toString: function() { var msg = '', header, length, idx; msg += this.method + ' ' + (this.ruri.toRaw ? this.ruri.toRaw() : this.ruri) + ' SIP/2.0\r\n'; for (header in this.headers) { length = this.headers[header].length; for (idx = 0; idx < length; idx++) { msg += header + ': ' + this.headers[header][idx] + '\r\n'; } } length = this.extraHeaders.length; for (idx = 0; idx < length; idx++) { msg += this.extraHeaders[idx].trim() +'\r\n'; } msg += getSupportedHeader(this); msg += 'User-Agent: ' + this.ua.configuration.userAgentString +'\r\n'; if (this.body) { if (typeof this.body === 'string') { length = SIP.Utils.str_utf8_length(this.body); msg += 'Content-Length: ' + length + '\r\n\r\n'; msg += this.body; } else { if (this.body.body && this.body.contentType) { length = SIP.Utils.str_utf8_length(this.body.body); msg += 'Content-Type: ' + this.body.contentType + '\r\n'; msg += 'Content-Length: ' + length + '\r\n\r\n'; msg += this.body.body; } else { msg += 'Content-Length: ' + 0 + '\r\n\r\n'; } } } else { msg += 'Content-Length: ' + 0 + '\r\n\r\n'; } return msg; } }; /** * @augments SIP * @class Class for incoming SIP message. */ IncomingMessage = function(){ this.data = null; this.headers = null; this.method = null; this.via = null; this.via_branch = null; this.call_id = null; this.cseq = null; this.from = null; this.from_tag = null; this.to = null; this.to_tag = null; this.body = null; }; IncomingMessage.prototype = { /** * Insert a header of the given name and value into the last position of the * header array. * @param {String} name header name * @param {String} value header value */ addHeader: function(name, value) { var header = { raw: value }; name = SIP.Utils.headerize(name); if(this.headers[name]) { this.headers[name].push(header); } else { this.headers[name] = [header]; } }, /** * Get the value of the given header name at the given position. * @param {String} name header name * @returns {String|undefined} Returns the specified header, null if header doesn't exist. */ getHeader: function(name) { var header = this.headers[SIP.Utils.headerize(name)]; if(header) { if(header[0]) { return header[0].raw; } } else { return; } }, /** * Get the header/s of the given name. * @param {String} name header name * @returns {Array} Array with all the headers of the specified name. */ getHeaders: function(name) { var idx, length, header = this.headers[SIP.Utils.headerize(name)], result = []; if(!header) { return []; } length = header.length; for (idx = 0; idx < length; idx++) { result.push(header[idx].raw); } return result; }, /** * Verify the existence of the given header. * @param {String} name header name * @returns {boolean} true if header with given name exists, false otherwise */ hasHeader: function(name) { return(this.headers[SIP.Utils.headerize(name)]) ? true : false; }, /** * Parse the given header on the given index. * @param {String} name header name * @param {Number} [idx=0] header index * @returns {Object|undefined} Parsed header object, undefined if the header is not present or in case of a parsing error. */ parseHeader: function(name, idx) { var header, value, parsed; name = SIP.Utils.headerize(name); idx = idx || 0; if(!this.headers[name]) { this.logger.log('header "' + name + '" not present'); return; } else if(idx >= this.headers[name].length) { this.logger.log('not so many "' + name + '" headers present'); return; } header = this.headers[name][idx]; value = header.raw; if(header.parsed) { return header.parsed; } //substitute '-' by '_' for grammar rule matching. parsed = SIP.Grammar.parse(value, name.replace(/-/g, '_')); if(parsed === -1) { this.headers[name].splice(idx, 1); //delete from headers this.logger.warn('error parsing "' + name + '" header field with value "' + value + '"'); return; } else { header.parsed = parsed; return parsed; } }, /** * Message Header attribute selector. Alias of parseHeader. * @param {String} name header name * @param {Number} [idx=0] header index * @returns {Object|undefined} Parsed header object, undefined if the header is not present or in case of a parsing error. * * @example * message.s('via',3).port */ s: function(name, idx) { return this.parseHeader(name, idx); }, /** * Replace the value of the given header by the value. * @param {String} name header name * @param {String} value header value */ setHeader: function(name, value) { var header = { raw: value }; this.headers[SIP.Utils.headerize(name)] = [header]; }, toString: function() { return this.data; } }; /** * @augments IncomingMessage * @class Class for incoming SIP request. */ IncomingRequest = function(ua) { this.logger = ua.getLogger('sip.sipmessage'); this.ua = ua; this.headers = {}; this.ruri = null; this.transport = null; this.server_transaction = null; }; IncomingRequest.prototype = new IncomingMessage(); /** * Stateful reply. * @param {Number} code status code * @param {String} reason reason phrase * @param {Object} headers extra headers * @param {String} body body * @param {Function} [onSuccess] onSuccess callback * @param {Function} [onFailure] onFailure callback */ IncomingRequest.prototype.reply = function(code, reason, extraHeaders, body, onSuccess, onFailure) { var rr, vias, length, idx, response, to = this.getHeader('To'), r = 0, v = 0; response = SIP.Utils.buildStatusLine(code, reason); extraHeaders = (extraHeaders || []).slice(); if(this.method === SIP.C.INVITE && code > 100 && code <= 200) { rr = this.getHeaders('record-route'); length = rr.length; for(r; r < length; r++) { response += 'Record-Route: ' + rr[r] + '\r\n'; } } vias = this.getHeaders('via'); length = vias.length; for(v; v < length; v++) { response += 'Via: ' + vias[v] + '\r\n'; } if(!this.to_tag && code > 100) { to += ';tag=' + SIP.Utils.newTag(); } else if(this.to_tag && !this.s('to').hasParam('tag')) { to += ';tag=' + this.to_tag; } response += 'To: ' + to + '\r\n'; response += 'From: ' + this.getHeader('From') + '\r\n'; response += 'Call-ID: ' + this.call_id + '\r\n'; response += 'CSeq: ' + this.cseq + ' ' + this.method + '\r\n'; length = extraHeaders.length; for (idx = 0; idx < length; idx++) { response += extraHeaders[idx].trim() +'\r\n'; } response += getSupportedHeader(this); response += 'User-Agent: ' + this.ua.configuration.userAgentString +'\r\n'; if (body) { if (typeof body === 'string') { length = SIP.Utils.str_utf8_length(body); response += 'Content-Type: application/sdp\r\n'; response += 'Content-Length: ' + length + '\r\n\r\n'; response += body; } else { if (body.body && body.contentType) { length = SIP.Utils.str_utf8_length(body.body); response += 'Content-Type: ' + body.contentType + '\r\n'; response += 'Content-Length: ' + length + '\r\n\r\n'; response += body.body; } else { response += 'Content-Length: ' + 0 + '\r\n\r\n'; } } } else { response += 'Content-Length: ' + 0 + '\r\n\r\n'; } this.server_transaction.receiveResponse(code, response).then(onSuccess, onFailure); return response; }; /** * Stateless reply. * @param {Number} code status code * @param {String} reason reason phrase */ IncomingRequest.prototype.reply_sl = function(code, reason) { var to, response, v = 0, vias = this.getHeaders('via'), length = vias.length; response = SIP.Utils.buildStatusLine(code, reason); for(v; v < length; v++) { response += 'Via: ' + vias[v] + '\r\n'; } to = this.getHeader('To'); if(!this.to_tag && code > 100) { to += ';tag=' + SIP.Utils.newTag(); } else if(this.to_tag && !this.s('to').hasParam('tag')) { to += ';tag=' + this.to_tag; } response += 'To: ' + to + '\r\n'; response += 'From: ' + this.getHeader('From') + '\r\n'; response += 'Call-ID: ' + this.call_id + '\r\n'; response += 'CSeq: ' + this.cseq + ' ' + this.method + '\r\n'; response += 'User-Agent: ' + this.ua.configuration.userAgentString +'\r\n'; response += 'Content-Length: ' + 0 + '\r\n\r\n'; this.transport.send(response); }; /** * @augments IncomingMessage * @class Class for incoming SIP response. */ IncomingResponse = function(ua) { this.logger = ua.getLogger('sip.sipmessage'); this.headers = {}; this.status_code = null; this.reason_phrase = null; }; IncomingResponse.prototype = new IncomingMessage(); SIP.OutgoingRequest = OutgoingRequest; SIP.IncomingRequest = IncomingRequest; SIP.IncomingResponse = IncomingResponse; }; },{}],21:[function(require,module,exports){ "use strict"; /** * @fileoverview Incoming SIP Message Sanity Check */ /** * SIP message sanity check. * @augments SIP * @function * @param {SIP.IncomingMessage} message * @param {SIP.UA} ua * @param {SIP.Transport} transport * @returns {Boolean} */ module.exports = function (SIP) { var sanityCheck, requests = [], responses = [], all = []; // Reply function reply(status_code, message, transport) { var to, response = SIP.Utils.buildStatusLine(status_code), vias = message.getHeaders('via'), length = vias.length, idx = 0; for(idx; idx < length; idx++) { response += "Via: " + vias[idx] + "\r\n"; } to = message.getHeader('To'); if(!message.to_tag) { to += ';tag=' + SIP.Utils.newTag(); } response += "To: " + to + "\r\n"; response += "From: " + message.getHeader('From') + "\r\n"; response += "Call-ID: " + message.call_id + "\r\n"; response += "CSeq: " + message.cseq + " " + message.method + "\r\n"; response += "\r\n"; transport.send(response); } /* * Sanity Check for incoming Messages * * Requests: * - _rfc3261_8_2_2_1_ Receive a Request with a non supported URI scheme * - _rfc3261_16_3_4_ Receive a Request already sent by us * Does not look at via sent-by but at sipjsId, which is inserted as * a prefix in all initial requests generated by the ua * - _rfc3261_18_3_request_ Body Content-Length * - _rfc3261_8_2_2_2_ Merged Requests * * Responses: * - _rfc3261_8_1_3_3_ Multiple Via headers * - _rfc3261_18_1_2_ sent-by mismatch * - _rfc3261_18_3_response_ Body Content-Length * * All: * - Minimum headers in a SIP message */ // Sanity Check functions for requests function rfc3261_8_2_2_1(message, ua, transport) { if(!message.ruri || message.ruri.scheme !== 'sip') { reply(416, message, transport); return false; } } function rfc3261_16_3_4(message, ua, transport) { if(!message.to_tag) { if(message.call_id.substr(0, 5) === ua.configuration.sipjsId) { reply(482, message, transport); return false; } } } function rfc3261_18_3_request(message, ua, transport) { var len = SIP.Utils.str_utf8_length(message.body), contentLength = message.getHeader('content-length'); if(len < contentLength) { reply(400, message, transport); return false; } } function rfc3261_8_2_2_2(message, ua, transport) { var tr, idx, fromTag = message.from_tag, call_id = message.call_id, cseq = message.cseq; if(!message.to_tag) { if(message.method === SIP.C.INVITE) { tr = ua.transactions.ist[message.via_branch]; if(tr) { return; } else { for(idx in ua.transactions.ist) { tr = ua.transactions.ist[idx]; if(tr.request.from_tag === fromTag && tr.request.call_id === call_id && tr.request.cseq === cseq) { reply(482, message, transport); return false; } } } } else { tr = ua.transactions.nist[message.via_branch]; if(tr) { return; } else { for(idx in ua.transactions.nist) { tr = ua.transactions.nist[idx]; if(tr.request.from_tag === fromTag && tr.request.call_id === call_id && tr.request.cseq === cseq) { reply(482, message, transport); return false; } } } } } } // Sanity Check functions for responses function rfc3261_8_1_3_3(message, ua) { if(message.getHeaders('via').length > 1) { ua.getLogger('sip.sanitycheck').warn('More than one Via header field present in the response. Dropping the response'); return false; } } function rfc3261_18_1_2(message, ua) { var viaHost = ua.configuration.viaHost; if(message.via.host !== viaHost || message.via.port !== undefined) { ua.getLogger('sip.sanitycheck').warn('Via sent-by in the response does not match UA Via host value. Dropping the response'); return false; } } function rfc3261_18_3_response(message, ua) { var len = SIP.Utils.str_utf8_length(message.body), contentLength = message.getHeader('content-length'); if(len < contentLength) { ua.getLogger('sip.sanitycheck').warn('Message body length is lower than the value in Content-Length header field. Dropping the response'); return false; } } // Sanity Check functions for requests and responses function minimumHeaders(message, ua) { var mandatoryHeaders = ['from', 'to', 'call_id', 'cseq', 'via'], idx = mandatoryHeaders.length; while(idx--) { if(!message.hasHeader(mandatoryHeaders[idx])) { ua.getLogger('sip.sanitycheck').warn('Missing mandatory header field : '+ mandatoryHeaders[idx] +'. Dropping the response'); return false; } } } requests.push(rfc3261_8_2_2_1); requests.push(rfc3261_16_3_4); requests.push(rfc3261_18_3_request); requests.push(rfc3261_8_2_2_2); responses.push(rfc3261_8_1_3_3); responses.push(rfc3261_18_1_2); responses.push(rfc3261_18_3_response); all.push(minimumHeaders); sanityCheck = function(message, ua, transport) { var len, pass; len = all.length; while(len--) { pass = all[len](message, ua, transport); if(pass === false) { return false; } } if(message instanceof SIP.IncomingRequest) { len = requests.length; while(len--) { pass = requests[len](message, ua, transport); if(pass === false) { return false; } } } else if(message instanceof SIP.IncomingResponse) { len = responses.length; while(len--) { pass = responses[len](message, ua, transport); if(pass === false) { return false; } } } //Everything is OK return true; }; SIP.sanityCheck = sanityCheck; }; },{}],22:[function(require,module,exports){ "use strict"; module.exports = function (SIP) { var ServerContext; ServerContext = function (ua, request) { this.ua = ua; this.logger = ua.getLogger('sip.servercontext'); this.request = request; if (request.method === SIP.C.INVITE) { this.transaction = new SIP.Transactions.InviteServerTransaction(request, ua); } else { this.transaction = new SIP.Transactions.NonInviteServerTransaction(request, ua); } if (request.body) { this.body = request.body; } if (request.hasHeader('Content-Type')) { this.contentType = request.getHeader('Content-Type'); } this.method = request.method; this.data = {}; this.localIdentity = request.to; this.remoteIdentity = request.from; }; ServerContext.prototype = Object.create(SIP.EventEmitter.prototype); ServerContext.prototype.progress = function (options) { options = Object.create(options || Object.prototype); options.statusCode || (options.statusCode = 180); options.minCode = 100; options.maxCode = 199; options.events = ['progress']; return this.reply(options); }; ServerContext.prototype.accept = function (options) { options = Object.create(options || Object.prototype); options.statusCode || (options.statusCode = 200); options.minCode = 200; options.maxCode = 299; options.events = ['accepted']; return this.reply(options); }; ServerContext.prototype.reject = function (options) { options = Object.create(options || Object.prototype); options.statusCode || (options.statusCode = 480); options.minCode = 300; options.maxCode = 699; options.events = ['rejected', 'failed']; return this.reply(options); }; ServerContext.prototype.reply = function (options) { options = options || {}; // This is okay, so long as we treat options as read-only in this method var statusCode = options.statusCode || 100, minCode = options.minCode || 100, maxCode = options.maxCode || 699, reasonPhrase = SIP.Utils.getReasonPhrase(statusCode, options.reasonPhrase), extraHeaders = options.extraHeaders || [], body = options.body, events = options.events || [], response; if (statusCode < minCode || statusCode > maxCode) { throw new TypeError('Invalid statusCode: ' + statusCode); } response = this.request.reply(statusCode, reasonPhrase, extraHeaders, body); events.forEach(function (event) { this.emit(event, response, reasonPhrase); }, this); return this; }; ServerContext.prototype.onRequestTimeout = function () { this.emit('failed', null, SIP.C.causes.REQUEST_TIMEOUT); }; ServerContext.prototype.onTransportError = function () { this.emit('failed', null, SIP.C.causes.CONNECTION_ERROR); }; SIP.ServerContext = ServerContext; }; },{}],23:[function(require,module,exports){ "use strict"; module.exports = function (SIP, environment) { var DTMF = require('./Session/DTMF')(SIP); var Session, InviteServerContext, InviteClientContext, C = { //Session states STATUS_NULL: 0, STATUS_INVITE_SENT: 1, STATUS_1XX_RECEIVED: 2, STATUS_INVITE_RECEIVED: 3, STATUS_WAITING_FOR_ANSWER: 4, STATUS_ANSWERED: 5, STATUS_WAITING_FOR_PRACK: 6, STATUS_WAITING_FOR_ACK: 7, STATUS_CANCELED: 8, STATUS_TERMINATED: 9, STATUS_ANSWERED_WAITING_FOR_PRACK: 10, STATUS_EARLY_MEDIA: 11, STATUS_CONFIRMED: 12 }; /* * @param {function returning SIP.MediaHandler} [mediaHandlerFactory] * (See the documentation for the mediaHandlerFactory argument of the UA constructor.) */ Session = function (mediaHandlerFactory) { this.status = C.STATUS_NULL; this.dialog = null; this.earlyDialogs = {}; this.mediaHandlerFactory = mediaHandlerFactory || SIP.WebRTC.MediaHandler.defaultFactory; // this.mediaHandler gets set by ICC/ISC constructors this.hasOffer = false; this.hasAnswer = false; // Session Timers this.timers = { ackTimer: null, expiresTimer: null, invite2xxTimer: null, userNoAnswerTimer: null, rel1xxTimer: null, prackTimer: null }; // Session info this.startTime = null; this.endTime = null; this.tones = null; // Mute/Hold state this.local_hold = false; this.remote_hold = false; this.pending_actions = { actions: [], length: function() { return this.actions.length; }, isPending: function(name){ var idx = 0, length = this.actions.length; for (idx; idx 0) { dtmfs.push(new DTMF(this, tones.shift(), options)); } if (this.tones) { // Tones are already queued, just add to the queue this.tones = this.tones.concat(dtmfs); return this; } var sendDTMF = function () { var dtmf, timeout; if (self.status === C.STATUS_TERMINATED || !self.tones || self.tones.length === 0) { // Stop sending DTMF self.tones = null; return this; } dtmf = self.tones.shift(); if (tone === ',') { timeout = 2000; } else { dtmf.on('failed', function(){self.tones = null;}); dtmf.send(options); timeout = dtmf.duration + dtmf.interToneGap; } // Set timeout for the next tone SIP.Timers.setTimeout(sendDTMF, timeout); }; this.tones = dtmfs; sendDTMF(); return this; }, bye: function(options) { options = Object.create(options || Object.prototype); var statusCode = options.statusCode; // Check Session Status if (this.status === C.STATUS_TERMINATED) { this.logger.error('Error: Attempted to send BYE in a terminated session.'); return this; } this.logger.log('terminating Session'); if (statusCode && (statusCode < 200 || statusCode >= 700)) { throw new TypeError('Invalid statusCode: '+ statusCode); } options.receiveResponse = function () {}; return this. sendRequest(SIP.C.BYE, options). terminated(); }, refer: function(target, options) { options = options || {}; var extraHeaders = (options.extraHeaders || []).slice(), withReplaces = target instanceof SIP.InviteServerContext || target instanceof SIP.InviteClientContext, originalTarget = target; if (target === undefined) { throw new TypeError('Not enough arguments'); } // Check Session Status if (this.status !== C.STATUS_CONFIRMED) { throw new SIP.Exceptions.InvalidStateError(this.status); } // transform `target` so that it can be a Refer-To header value if (withReplaces) { //Attended Transfer // B.transfer(C) target = '"' + target.remoteIdentity.friendlyName + '" ' + '<' + target.dialog.remote_target.toString() + '?Replaces=' + target.dialog.id.call_id + '%3Bto-tag%3D' + target.dialog.id.remote_tag + '%3Bfrom-tag%3D' + target.dialog.id.local_tag + '>'; } else { //Blind Transfer // normalizeTarget allows instances of SIP.URI to pass through unaltered, // so try to make one ahead of time try { target = SIP.Grammar.parse(target, 'Refer_To').uri || target; } catch (e) { this.logger.debug(".refer() cannot parse Refer_To from", target); this.logger.debug("...falling through to normalizeTarget()"); } // Check target validity target = this.ua.normalizeTarget(target); if (!target) { throw new TypeError('Invalid target: ' + originalTarget); } } extraHeaders.push('Contact: '+ this.contact); extraHeaders.push('Allow: '+ SIP.UA.C.ALLOWED_METHODS.toString()); extraHeaders.push('Refer-To: '+ target); // Send the request this.sendRequest(SIP.C.REFER, { extraHeaders: extraHeaders, body: options.body, receiveResponse: function (response) { if ( ! /^2[0-9]{2}$/.test(response.status_code) ) { return; } // hang up only if we transferred to a SIP address if (withReplaces || (target.scheme && target.scheme.match("^sips?$"))) { this.terminate(); } }.bind(this) }); return this; }, followRefer: function followRefer (callback) { return function referListener (callback, request) { // open non-SIP URIs if possible and keep session open var referTo = request.parseHeader('refer-to'); var target = referTo.uri; if (!target.scheme.match("^sips?$")) { var targetString = target.toString(); if (typeof environment.open === "function") { environment.open(targetString); } else { this.logger.warn("referred to non-SIP URI but `open` isn't in the environment: " + targetString); } return; } var extraHeaders = []; /* Copy the Replaces query into a Replaces header */ /* TODO - make sure we don't copy a poorly formatted header? */ var replaces = target.getHeader('Replaces'); if (replaces !== undefined) { extraHeaders.push('Replaces: ' + decodeURIComponent(replaces)); } // don't embed headers into Request-URI of INVITE target.clearHeaders(); /* Harmless race condition. Both sides of REFER may send a BYE, but in the end the dialogs are destroyed. */ var getReferMedia = this.mediaHandler.getReferMedia; var mediaHint = getReferMedia ? getReferMedia.call(this.mediaHandler) : this.mediaHint; SIP.Hacks.Chrome.getsConfusedAboutGUM(this); var referSession = this.ua.invite(target, { media: mediaHint, params: { to_displayName: referTo.friendlyName }, extraHeaders: extraHeaders }); callback.call(this, request, referSession); this.terminate(); }.bind(this, callback); }, sendRequest: function(method,options) { options = options || {}; var self = this; var request = new SIP.OutgoingRequest( method, this.dialog.remote_target, this.ua, { cseq: options.cseq || (this.dialog.local_seqnum += 1), call_id: this.dialog.id.call_id, from_uri: this.dialog.local_uri, from_tag: this.dialog.id.local_tag, to_uri: this.dialog.remote_uri, to_tag: this.dialog.id.remote_tag, route_set: this.dialog.route_set, statusCode: options.statusCode, reasonPhrase: options.reasonPhrase }, options.extraHeaders || [], options.body ); new SIP.RequestSender({ request: request, onRequestTimeout: function() { self.onRequestTimeout(); }, onTransportError: function() { self.onTransportError(); }, receiveResponse: options.receiveResponse || function(response) { self.receiveNonInviteResponse(response); } }, this.ua).send(); // Emit the request event this.emit(method.toLowerCase(), request); return this; }, close: function() { var idx; if(this.status === C.STATUS_TERMINATED) { return this; } this.logger.log('closing INVITE session ' + this.id); // 1st Step. Terminate media. if (this.mediaHandler){ this.mediaHandler.close(); } // 2nd Step. Terminate signaling. // Clear session timers for(idx in this.timers) { SIP.Timers.clearTimeout(this.timers[idx]); } // Terminate dialogs // Terminate confirmed dialog if(this.dialog) { this.dialog.terminate(); delete this.dialog; } // Terminate early dialogs for(idx in this.earlyDialogs) { this.earlyDialogs[idx].terminate(); delete this.earlyDialogs[idx]; } this.status = C.STATUS_TERMINATED; delete this.ua.sessions[this.id]; return this; }, createDialog: function(message, type, early) { var dialog, early_dialog, local_tag = message[(type === 'UAS') ? 'to_tag' : 'from_tag'], remote_tag = message[(type === 'UAS') ? 'from_tag' : 'to_tag'], id = message.call_id + local_tag + remote_tag; early_dialog = this.earlyDialogs[id]; // Early Dialog if (early) { if (early_dialog) { return true; } else { early_dialog = new SIP.Dialog(this, message, type, SIP.Dialog.C.STATUS_EARLY); // Dialog has been successfully created. if(early_dialog.error) { this.logger.error(early_dialog.error); this.failed(message, SIP.C.causes.INTERNAL_ERROR); return false; } else { this.earlyDialogs[id] = early_dialog; return true; } } } // Confirmed Dialog else { // In case the dialog is in _early_ state, update it if (early_dialog) { early_dialog.update(message, type); this.dialog = early_dialog; delete this.earlyDialogs[id]; for (var dia in this.earlyDialogs) { this.earlyDialogs[dia].terminate(); delete this.earlyDialogs[dia]; } return true; } // Otherwise, create a _confirmed_ dialog dialog = new SIP.Dialog(this, message, type); if(dialog.error) { this.logger.error(dialog.error); this.failed(message, SIP.C.causes.INTERNAL_ERROR); return false; } else { this.to_tag = message.to_tag; this.dialog = dialog; return true; } } }, /** * Check if Session is ready for a re-INVITE * * @returns {Boolean} */ isReadyToReinvite: function() { return this.mediaHandler.isReady() && !this.dialog.uac_pending_reply && !this.dialog.uas_pending_reply; }, /** * Mute */ mute: function(options) { var ret = this.mediaHandler.mute(options); if (ret) { this.onmute(ret); } }, /** * Unmute */ unmute: function(options) { var ret = this.mediaHandler.unmute(options); if (ret) { this.onunmute(ret); } }, /** * Hold */ hold: function(options) { if (this.status !== C.STATUS_WAITING_FOR_ACK && this.status !== C.STATUS_CONFIRMED) { throw new SIP.Exceptions.InvalidStateError(this.status); } this.mediaHandler.hold(); // Check if RTCSession is ready to send a reINVITE if (!this.isReadyToReinvite()) { /* If there is a pending 'unhold' action, cancel it and don't queue this one * Else, if there isn't any 'hold' action, add this one to the queue * Else, if there is already a 'hold' action, skip */ if (this.pending_actions.isPending('unhold')) { this.pending_actions.pop('unhold'); } else if (!this.pending_actions.isPending('hold')) { this.pending_actions.push('hold'); } return; } else if (this.local_hold === true) { return; } this.onhold('local'); this.sendReinvite(options); }, /** * Unhold */ unhold: function(options) { if (this.status !== C.STATUS_WAITING_FOR_ACK && this.status !== C.STATUS_CONFIRMED) { throw new SIP.Exceptions.InvalidStateError(this.status); } this.mediaHandler.unhold(); if (!this.isReadyToReinvite()) { /* If there is a pending 'hold' action, cancel it and don't queue this one * Else, if there isn't any 'unhold' action, add this one to the queue * Else, if there is already a 'unhold' action, skip */ if (this.pending_actions.isPending('hold')) { this.pending_actions.pop('hold'); } else if (!this.pending_actions.isPending('unhold')) { this.pending_actions.push('unhold'); } return; } else if (this.local_hold === false) { return; } this.onunhold('local'); this.sendReinvite(options); }, /** * isOnHold */ isOnHold: function() { return { local: this.local_hold, remote: this.remote_hold }; }, /** * In dialog INVITE Reception * @private */ receiveReinvite: function(request) { var self = this; if (!this.mediaHandler.hasDescription(request)) { this.logger.warn('invalid Content-Type'); request.reply(415); return; } this.mediaHandler.setDescription(request) .then(this.mediaHandler.getDescription.bind(this.mediaHandler, this.mediaHint)) .then(function(description) { var extraHeaders = ['Contact: ' + self.contact]; request.reply(200, null, extraHeaders, description, function() { self.status = C.STATUS_WAITING_FOR_ACK; self.setInvite2xxTimer(request, description); self.setACKTimer(); if (self.remote_hold && !self.mediaHandler.remote_hold) { self.onunhold('remote'); } else if (!self.remote_hold && self.mediaHandler.remote_hold) { self.onhold('remote'); } }); }) .catch(function onFailure (e) { var statusCode; if (e instanceof SIP.Exceptions.GetDescriptionError) { statusCode = 500; } else { self.logger.error(e); statusCode = 488; } request.reply(statusCode); }); }, sendReinvite: function(options) { options = options || {}; var self = this, extraHeaders = (options.extraHeaders || []).slice(), eventHandlers = options.eventHandlers || {}, succeeded; if (eventHandlers.succeeded) { succeeded = eventHandlers.succeeded; } this.reinviteSucceeded = function(){ SIP.Timers.clearTimeout(self.timers.ackTimer); SIP.Timers.clearTimeout(self.timers.invite2xxTimer); self.status = C.STATUS_CONFIRMED; succeeded && succeeded.apply(this, arguments); }; if (eventHandlers.failed) { this.reinviteFailed = eventHandlers.failed; } else { this.reinviteFailed = function(){}; } extraHeaders.push('Contact: ' + this.contact); extraHeaders.push('Allow: '+ SIP.UA.C.ALLOWED_METHODS.toString()); this.receiveResponse = this.receiveReinviteResponse; //REVISIT this.mediaHandler.getDescription(self.mediaHint) .then( function(description){ self.dialog.sendRequest(self, SIP.C.INVITE, { extraHeaders: extraHeaders, body: description }); }, function() { if (self.isReadyToReinvite()) { self.onReadyToReinvite(); } self.reinviteFailed(); } ); }, receiveRequest: function (request) { switch (request.method) { case SIP.C.BYE: request.reply(200); if(this.status === C.STATUS_CONFIRMED) { this.emit('bye', request); this.terminated(request, SIP.C.causes.BYE); } break; case SIP.C.INVITE: if(this.status === C.STATUS_CONFIRMED) { this.logger.log('re-INVITE received'); this.receiveReinvite(request); } break; case SIP.C.INFO: if(this.status === C.STATUS_CONFIRMED || this.status === C.STATUS_WAITING_FOR_ACK) { if (this.onInfo) { return this.onInfo(request); } var body, tone, duration, contentType = request.getHeader('content-type'), reg_tone = /^(Signal\s*?=\s*?)([0-9A-D#*]{1})(\s)?.*/, reg_duration = /^(Duration\s?=\s?)([0-9]{1,4})(\s)?.*/; if (contentType) { if (contentType.match(/^application\/dtmf-relay/i)) { if (request.body) { body = request.body.split('\r\n', 2); if (body.length === 2) { if (reg_tone.test(body[0])) { tone = body[0].replace(reg_tone,"$2"); } if (reg_duration.test(body[1])) { duration = parseInt(body[1].replace(reg_duration,"$2"), 10); } } } new DTMF(this, tone, {duration: duration}).init_incoming(request); } else { request.reply(415, null, ["Accept: application/dtmf-relay"]); } } } break; case SIP.C.REFER: if(this.status === C.STATUS_CONFIRMED) { this.logger.log('REFER received'); var hasReferListener = this.listeners('refer').length, notifyBody; if (hasReferListener) { request.reply(202, 'Accepted'); notifyBody = 'SIP/2.0 100 Trying'; this.sendRequest(SIP.C.NOTIFY, { extraHeaders:[ 'Event: refer', 'Subscription-State: terminated', 'Content-Type: message/sipfrag' ], body: notifyBody, receiveResponse: function() {} }); this.emit('refer', request); } else { // RFC 3515.2.4.2: 'the UA MAY decline the request.' request.reply(603, 'Declined'); } } break; case SIP.C.NOTIFY: request.reply(200, 'OK'); this.emit('notify', request); break; } }, /** * Reception of Response for in-dialog INVITE * @private */ receiveReinviteResponse: function(response) { var self = this; if (this.status === C.STATUS_TERMINATED) { return; } switch(true) { case /^1[0-9]{2}$/.test(response.status_code): break; case /^2[0-9]{2}$/.test(response.status_code): this.status = C.STATUS_CONFIRMED; this.sendRequest(SIP.C.ACK,{cseq:response.cseq}); if (!this.mediaHandler.hasDescription(response)) { this.reinviteFailed(); break; } //REVISIT this.mediaHandler.setDescription(response) .then( function onSuccess () { self.reinviteSucceeded(); }, function onFailure () { self.reinviteFailed(); } ); break; default: this.reinviteFailed(); } }, acceptAndTerminate: function(response, status_code, reason_phrase) { var extraHeaders = []; if (status_code) { extraHeaders.push('Reason: ' + SIP.Utils.getReasonHeaderValue(status_code, reason_phrase)); } // An error on dialog creation will fire 'failed' event if (this.dialog || this.createDialog(response, 'UAC')) { this.sendRequest(SIP.C.ACK,{cseq: response.cseq}); this.sendRequest(SIP.C.BYE, { extraHeaders: extraHeaders }); } return this; }, /** * RFC3261 13.3.1.4 * Response retransmissions cannot be accomplished by transaction layer * since it is destroyed when receiving the first 2xx answer */ setInvite2xxTimer: function(request, description) { var self = this, timeout = SIP.Timers.T1; this.timers.invite2xxTimer = SIP.Timers.setTimeout(function invite2xxRetransmission() { if (self.status !== C.STATUS_WAITING_FOR_ACK) { return; } self.logger.log('no ACK received, attempting to retransmit OK'); var extraHeaders = ['Contact: ' + self.contact]; request.reply(200, null, extraHeaders, description); timeout = Math.min(timeout * 2, SIP.Timers.T2); self.timers.invite2xxTimer = SIP.Timers.setTimeout(invite2xxRetransmission, timeout); }, timeout); }, /** * RFC3261 14.2 * If a UAS generates a 2xx response and never receives an ACK, * it SHOULD generate a BYE to terminate the dialog. */ setACKTimer: function() { var self = this; this.timers.ackTimer = SIP.Timers.setTimeout(function() { if(self.status === C.STATUS_WAITING_FOR_ACK) { self.logger.log('no ACK received for an extended period of time, terminating the call'); SIP.Timers.clearTimeout(self.timers.invite2xxTimer); self.sendRequest(SIP.C.BYE); self.terminated(null, SIP.C.causes.NO_ACK); } }, SIP.Timers.TIMER_H); }, /* * @private */ onReadyToReinvite: function() { var action = this.pending_actions.shift(); if (!action || !this[action.name]) { return; } this[action.name](); }, onTransportError: function() { if (this.status !== C.STATUS_CONFIRMED && this.status !== C.STATUS_TERMINATED) { this.failed(null, SIP.C.causes.CONNECTION_ERROR); } }, onRequestTimeout: function() { if (this.status === C.STATUS_CONFIRMED) { this.terminated(null, SIP.C.causes.REQUEST_TIMEOUT); } else if (this.status !== C.STATUS_TERMINATED) { this.failed(null, SIP.C.causes.REQUEST_TIMEOUT); this.terminated(null, SIP.C.causes.REQUEST_TIMEOUT); } }, onDialogError: function(response) { if (this.status === C.STATUS_CONFIRMED) { this.terminated(response, SIP.C.causes.DIALOG_ERROR); } else if (this.status !== C.STATUS_TERMINATED) { this.failed(response, SIP.C.causes.DIALOG_ERROR); this.terminated(response, SIP.C.causes.DIALOG_ERROR); } }, /** * @private */ onhold: function(originator) { this[originator === 'local' ? 'local_hold' : 'remote_hold'] = true; this.emit('hold', { originator: originator }); }, /** * @private */ onunhold: function(originator) { this[originator === 'local' ? 'local_hold' : 'remote_hold'] = false; this.emit('unhold', { originator: originator }); }, /* * @private */ onmute: function(options) { this.emit('muted', { audio: options.audio, video: options.video }); }, /* * @private */ onunmute: function(options) { this.emit('unmuted', { audio: options.audio, video: options.video }); }, failed: function(response, cause) { if (this.status === C.STATUS_TERMINATED) { return this; } this.emit('failed', response || null, cause || null); return this; }, rejected: function(response, cause) { this.emit('rejected', response || null, cause || null ); return this; }, canceled: function() { this.emit('cancel'); return this; }, accepted: function(response, cause) { cause = SIP.Utils.getReasonPhrase(response && response.status_code, cause); this.startTime = new Date(); if (this.replacee) { this.replacee.emit('replaced', this); this.replacee.terminate(); } this.emit('accepted', response, cause); return this; }, terminated: function(message, cause) { if (this.status === C.STATUS_TERMINATED) { return this; } this.endTime = new Date(); this.close(); this.emit('terminated', message || null, cause || null ); return this; }, connecting: function(request) { this.emit('connecting', { request: request }); return this; } }; Session.desugar = function desugar(options) { if (environment.HTMLMediaElement && options instanceof environment.HTMLMediaElement) { options = { media: { constraints: { audio: true, video: options.tagName === 'VIDEO' }, render: { remote: options } } }; } return options || {}; }; Session.C = C; SIP.Session = Session; InviteServerContext = function(ua, request) { var expires, self = this, contentType = request.getHeader('Content-Type'), contentDisp = request.parseHeader('Content-Disposition'); SIP.Utils.augment(this, SIP.ServerContext, [ua, request]); SIP.Utils.augment(this, SIP.Session, [ua.configuration.mediaHandlerFactory]); //Initialize Media Session this.mediaHandler = this.mediaHandlerFactory(this, { RTCConstraints: {"optional": [{'DtlsSrtpKeyAgreement': 'true'}]} }); // Check body and content type if ((!contentDisp && !this.mediaHandler.hasDescription(request)) || (contentDisp && contentDisp.type === 'render')) { this.renderbody = request.body; this.rendertype = contentType; } else if (!this.mediaHandler.hasDescription(request) && (contentDisp && contentDisp.type === 'session')) { request.reply(415); //TODO: instead of 415, pass off to the media handler, who can then decide if we can use it return; } this.status = C.STATUS_INVITE_RECEIVED; this.from_tag = request.from_tag; this.id = request.call_id + this.from_tag; this.request = request; this.contact = this.ua.contact.toString(); this.receiveNonInviteResponse = function () {}; // intentional no-op this.logger = ua.getLogger('sip.inviteservercontext', this.id); //Save the session into the ua sessions collection. this.ua.sessions[this.id] = this; //Get the Expires header value if exists if(request.hasHeader('expires')) { expires = request.getHeader('expires') * 1000; } //Set 100rel if necessary function set100rel(h,c) { if (request.hasHeader(h) && request.getHeader(h).toLowerCase().indexOf('100rel') >= 0) { self.rel100 = c; } } set100rel('require', SIP.C.supported.REQUIRED); set100rel('supported', SIP.C.supported.SUPPORTED); /* Set the to_tag before * replying a response code that will create a dialog. */ request.to_tag = SIP.Utils.newTag(); // An error on dialog creation will fire 'failed' event if(!this.createDialog(request, 'UAS', true)) { request.reply(500, 'Missing Contact header field'); return; } if (this.mediaHandler && this.mediaHandler.getRemoteStreams) { this.getRemoteStreams = this.mediaHandler.getRemoteStreams.bind(this.mediaHandler); this.getLocalStreams = this.mediaHandler.getLocalStreams.bind(this.mediaHandler); } function fireNewSession() { var options = {extraHeaders: ['Contact: ' + self.contact]}; if (self.rel100 !== SIP.C.supported.REQUIRED) { self.progress(options); } self.status = C.STATUS_WAITING_FOR_ANSWER; // Set userNoAnswerTimer self.timers.userNoAnswerTimer = SIP.Timers.setTimeout(function() { request.reply(408); self.failed(request, SIP.C.causes.NO_ANSWER); self.terminated(request, SIP.C.causes.NO_ANSWER); }, self.ua.configuration.noAnswerTimeout); /* Set expiresTimer * RFC3261 13.3.1 */ if (expires) { self.timers.expiresTimer = SIP.Timers.setTimeout(function() { if(self.status === C.STATUS_WAITING_FOR_ANSWER) { request.reply(487); self.failed(request, SIP.C.causes.EXPIRES); self.terminated(request, SIP.C.causes.EXPIRES); } }, expires); } self.emit('invite',request); } if (!this.mediaHandler.hasDescription(request) || this.renderbody) { SIP.Timers.setTimeout(fireNewSession, 0); } else { this.hasOffer = true; this.mediaHandler.setDescription(request) .then( fireNewSession, function onFailure (e) { self.logger.warn('invalid description'); self.logger.warn(e); request.reply(488); } ); } }; InviteServerContext.prototype = { reject: function(options) { // Check Session Status if (this.status === C.STATUS_TERMINATED) { throw new SIP.Exceptions.InvalidStateError(this.status); } this.logger.log('rejecting RTCSession'); SIP.ServerContext.prototype.reject.call(this, options); return this.terminated(); }, terminate: function(options) { options = options || {}; var extraHeaders = (options.extraHeaders || []).slice(), body = options.body, dialog, self = this; if (this.status === C.STATUS_WAITING_FOR_ACK && this.request.server_transaction.state !== SIP.Transactions.C.STATUS_TERMINATED) { dialog = this.dialog; this.receiveRequest = function(request) { if (request.method === SIP.C.ACK) { this.sendRequest(SIP.C.BYE, { extraHeaders: extraHeaders, body: body }); dialog.terminate(); } }; this.request.server_transaction.on('stateChanged', function(){ if (this.state === SIP.Transactions.C.STATUS_TERMINATED && this.dialog) { this.request = new SIP.OutgoingRequest( SIP.C.BYE, this.dialog.remote_target, this.ua, { 'cseq': this.dialog.local_seqnum+=1, 'call_id': this.dialog.id.call_id, 'from_uri': this.dialog.local_uri, 'from_tag': this.dialog.id.local_tag, 'to_uri': this.dialog.remote_uri, 'to_tag': this.dialog.id.remote_tag, 'route_set': this.dialog.route_set }, extraHeaders, body ); new SIP.RequestSender( { request: this.request, onRequestTimeout: function() { self.onRequestTimeout(); }, onTransportError: function() { self.onTransportError(); }, receiveResponse: function() { return; } }, this.ua ).send(); dialog.terminate(); } }); this.emit('bye', this.request); this.terminated(); // Restore the dialog into 'this' in order to be able to send the in-dialog BYE :-) this.dialog = dialog; // Restore the dialog into 'ua' so the ACK can reach 'this' session this.ua.dialogs[dialog.id.toString()] = dialog; } else if (this.status === C.STATUS_CONFIRMED) { this.bye(options); } else { this.reject(options); } return this; }, /* * @param {Object} [options.media] gets passed to SIP.MediaHandler.getDescription as mediaHint */ progress: function (options) { options = options || {}; var statusCode = options.statusCode || 180, reasonPhrase = options.reasonPhrase, extraHeaders = (options.extraHeaders || []).slice(), iceServers, stunServers = options.stunServers || null, turnServers = options.turnServers || null, body = options.body, response; if (statusCode < 100 || statusCode > 199) { throw new TypeError('Invalid statusCode: ' + statusCode); } if (this.isCanceled || this.status === C.STATUS_TERMINATED) { return this; } if (stunServers || turnServers) { if (stunServers) { iceServers = this.ua.getConfigurationCheck().optional['stunServers'](stunServers); if (!iceServers) { throw new TypeError('Invalid stunServers: '+ stunServers); } else { this.stunServers = iceServers; } } if (turnServers) { iceServers = this.ua.getConfigurationCheck().optional['turnServers'](turnServers); if (!iceServers) { throw new TypeError('Invalid turnServers: '+ turnServers); } else { this.turnServers = iceServers; } } this.mediaHandler.updateIceServers({ stunServers: this.stunServers, turnServers: this.turnServers }); } function do100rel() { /* jshint validthis: true */ statusCode = options.statusCode || 183; // Set status and add extra headers this.status = C.STATUS_WAITING_FOR_PRACK; extraHeaders.push('Contact: '+ this.contact); extraHeaders.push('Require: 100rel'); extraHeaders.push('RSeq: ' + Math.floor(Math.random() * 10000)); // Save media hint for later (referred sessions) this.mediaHint = options.media; // Get the session description to add to preaccept with this.mediaHandler.getDescription(options.media) .then( function onSuccess (description) { if (this.isCanceled || this.status === C.STATUS_TERMINATED) { return; } this.early_sdp = description.body; this[this.hasOffer ? 'hasAnswer' : 'hasOffer'] = true; // Retransmit until we get a response or we time out (see prackTimer below) var timeout = SIP.Timers.T1; this.timers.rel1xxTimer = SIP.Timers.setTimeout(function rel1xxRetransmission() { this.request.reply(statusCode, null, extraHeaders, description); timeout *= 2; this.timers.rel1xxTimer = SIP.Timers.setTimeout(rel1xxRetransmission.bind(this), timeout); }.bind(this), timeout); // Timeout and reject INVITE if no response this.timers.prackTimer = SIP.Timers.setTimeout(function () { if (this.status !== C.STATUS_WAITING_FOR_PRACK) { return; } this.logger.log('no PRACK received, rejecting the call'); SIP.Timers.clearTimeout(this.timers.rel1xxTimer); this.request.reply(504); this.terminated(null, SIP.C.causes.NO_PRACK); }.bind(this), SIP.Timers.T1 * 64); // Send the initial response response = this.request.reply(statusCode, reasonPhrase, extraHeaders, description); this.emit('progress', response, reasonPhrase); }.bind(this), function onFailure () { this.request.reply(480); this.failed(null, SIP.C.causes.WEBRTC_ERROR); this.terminated(null, SIP.C.causes.WEBRTC_ERROR); }.bind(this) ); } // end do100rel function normalReply() { /* jshint validthis:true */ response = this.request.reply(statusCode, reasonPhrase, extraHeaders, body); this.emit('progress', response, reasonPhrase); } if (options.statusCode !== 100 && (this.rel100 === SIP.C.supported.REQUIRED || (this.rel100 === SIP.C.supported.SUPPORTED && options.rel100) || (this.rel100 === SIP.C.supported.SUPPORTED && (this.ua.configuration.rel100 === SIP.C.supported.REQUIRED)))) { do100rel.apply(this); } else { normalReply.apply(this); } return this; }, /* * @param {Object} [options.media] gets passed to SIP.MediaHandler.getDescription as mediaHint */ accept: function(options) { options = Object.create(Session.desugar(options)); SIP.Utils.optionsOverride(options, 'media', 'mediaConstraints', true, this.logger, this.ua.configuration.media); this.mediaHint = options.media; this.onInfo = options.onInfo; // commented out now-unused hold-related variables for jshint. See below. JMF 2014-1-21 var //idx, length, hasAudio, hasVideo, self = this, request = this.request, extraHeaders = (options.extraHeaders || []).slice(), //mediaStream = options.mediaStream || null, iceServers, stunServers = options.stunServers || null, turnServers = options.turnServers || null, descriptionCreationSucceeded = function(description) { var response, // run for reply success callback replySucceeded = function() { self.status = C.STATUS_WAITING_FOR_ACK; self.setInvite2xxTimer(request, description); self.setACKTimer(); }, // run for reply failure callback replyFailed = function() { self.failed(null, SIP.C.causes.CONNECTION_ERROR); self.terminated(null, SIP.C.causes.CONNECTION_ERROR); }; // Chrome might call onaddstream before accept() is called, which means // mediaHandler.render() was called without a renderHint, so we need to // re-render now that mediaHint.render has been set. // // Chrome seems to be in the right regarding this, see // http://dev.w3.org/2011/webrtc/editor/webrtc.html#widl-RTCPeerConnection-onaddstream self.mediaHandler.render(); extraHeaders.push('Contact: ' + self.contact); extraHeaders.push('Allow: ' + SIP.UA.C.ALLOWED_METHODS.toString()); if(!self.hasOffer) { self.hasOffer = true; } else { self.hasAnswer = true; } response = request.reply(200, null, extraHeaders, description, replySucceeded, replyFailed ); if (self.status !== C.STATUS_TERMINATED) { // Didn't fail self.accepted(response, SIP.Utils.getReasonPhrase(200)); } }, descriptionCreationFailed = function() { if (self.status === C.STATUS_TERMINATED) { return; } // TODO - fail out on error self.request.reply(480); //self.failed(response, SIP.C.causes.USER_DENIED_MEDIA_ACCESS); self.failed(null, SIP.C.causes.WEBRTC_ERROR); self.terminated(null, SIP.C.causes.WEBRTC_ERROR); }; // Check Session Status if (this.status === C.STATUS_WAITING_FOR_PRACK) { this.status = C.STATUS_ANSWERED_WAITING_FOR_PRACK; return this; } else if (this.status === C.STATUS_WAITING_FOR_ANSWER) { this.status = C.STATUS_ANSWERED; } else if (this.status !== C.STATUS_EARLY_MEDIA) { throw new SIP.Exceptions.InvalidStateError(this.status); } if ((stunServers || turnServers) && (this.status !== C.STATUS_EARLY_MEDIA && this.status !== C.STATUS_ANSWERED_WAITING_FOR_PRACK)) { if (stunServers) { iceServers = this.ua.getConfigurationCheck().optional['stunServers'](stunServers); if (!iceServers) { throw new TypeError('Invalid stunServers: '+ stunServers); } else { this.stunServers = iceServers; } } if (turnServers) { iceServers = this.ua.getConfigurationCheck().optional['turnServers'](turnServers); if (!iceServers) { throw new TypeError('Invalid turnServers: '+ turnServers); } else { this.turnServers = iceServers; } } this.mediaHandler.updateIceServers({ stunServers: this.stunServers, turnServers: this.turnServers }); } // An error on dialog creation will fire 'failed' event if(!this.createDialog(request, 'UAS')) { request.reply(500, 'Missing Contact header field'); return this; } SIP.Timers.clearTimeout(this.timers.userNoAnswerTimer); // this hold-related code breaks FF accepting new calls - JMF 2014-1-21 /* length = this.getRemoteStreams().length; for (idx = 0; idx < length; idx++) { if (this.mediaHandler.getRemoteStreams()[idx].getVideoTracks().length > 0) { hasVideo = true; } if (this.mediaHandler.getRemoteStreams()[idx].getAudioTracks().length > 0) { hasAudio = true; } } if (!hasAudio && this.mediaConstraints.audio === true) { this.mediaConstraints.audio = false; if (mediaStream) { length = mediaStream.getAudioTracks().length; for (idx = 0; idx < length; idx++) { mediaStream.removeTrack(mediaStream.getAudioTracks()[idx]); } } } if (!hasVideo && this.mediaConstraints.video === true) { this.mediaConstraints.video = false; if (mediaStream) { length = mediaStream.getVideoTracks().length; for (idx = 0; idx < length; idx++) { mediaStream.removeTrack(mediaStream.getVideoTracks()[idx]); } } } */ if (this.status === C.STATUS_EARLY_MEDIA) { descriptionCreationSucceeded({}); } else { this.mediaHandler.getDescription(self.mediaHint) .then( descriptionCreationSucceeded, descriptionCreationFailed ); } return this; }, receiveRequest: function(request) { // ISC RECEIVE REQUEST function confirmSession() { /* jshint validthis:true */ var contentType; SIP.Timers.clearTimeout(this.timers.ackTimer); SIP.Timers.clearTimeout(this.timers.invite2xxTimer); this.status = C.STATUS_CONFIRMED; this.unmute(); // TODO - this logic assumes Content-Disposition defaults contentType = request.getHeader('Content-Type'); if (!this.mediaHandler.hasDescription(request)) { this.renderbody = request.body; this.rendertype = contentType; } this.emit('confirmed', request); } switch(request.method) { case SIP.C.CANCEL: /* RFC3261 15 States that a UAS may have accepted an invitation while a CANCEL * was in progress and that the UAC MAY continue with the session established by * any 2xx response, or MAY terminate with BYE. SIP does continue with the * established session. So the CANCEL is processed only if the session is not yet * established. */ /* * Terminate the whole session in case the user didn't accept (or yet to send the answer) nor reject the *request opening the session. */ if(this.status === C.STATUS_WAITING_FOR_ANSWER || this.status === C.STATUS_WAITING_FOR_PRACK || this.status === C.STATUS_ANSWERED_WAITING_FOR_PRACK || this.status === C.STATUS_EARLY_MEDIA || this.status === C.STATUS_ANSWERED) { this.status = C.STATUS_CANCELED; this.request.reply(487); this.canceled(request); this.rejected(request, SIP.C.causes.CANCELED); this.failed(request, SIP.C.causes.CANCELED); this.terminated(request, SIP.C.causes.CANCELED); } break; case SIP.C.ACK: if(this.status === C.STATUS_WAITING_FOR_ACK) { if (!this.hasAnswer) { if(this.mediaHandler.hasDescription(request)) { // ACK contains answer to an INVITE w/o SDP negotiation this.hasAnswer = true; this.mediaHandler.setDescription(request) .then( confirmSession.bind(this), function onFailure (e) { this.logger.warn(e); this.terminate({ statusCode: '488', reasonPhrase: 'Bad Media Description' }); this.failed(request, SIP.C.causes.BAD_MEDIA_DESCRIPTION); this.terminated(request, SIP.C.causes.BAD_MEDIA_DESCRIPTION); }.bind(this) ); } else if (this.early_sdp) { confirmSession.apply(this); } else { //TODO: Pass to mediahandler this.failed(request, SIP.C.causes.BAD_MEDIA_DESCRIPTION); this.terminated(request, SIP.C.causes.BAD_MEDIA_DESCRIPTION); } } else { confirmSession.apply(this); } } break; case SIP.C.PRACK: if (this.status === C.STATUS_WAITING_FOR_PRACK || this.status === C.STATUS_ANSWERED_WAITING_FOR_PRACK) { //localMedia = session.mediaHandler.localMedia; if(!this.hasAnswer) { if(this.mediaHandler.hasDescription(request)) { this.hasAnswer = true; this.mediaHandler.setDescription(request) .then( function onSuccess () { SIP.Timers.clearTimeout(this.timers.rel1xxTimer); SIP.Timers.clearTimeout(this.timers.prackTimer); request.reply(200); if (this.status === C.STATUS_ANSWERED_WAITING_FOR_PRACK) { this.status = C.STATUS_EARLY_MEDIA; this.accept(); } this.status = C.STATUS_EARLY_MEDIA; //REVISIT this.mute(); }.bind(this), function onFailure (e) { //TODO: Send to media handler this.logger.warn(e); this.terminate({ statusCode: '488', reasonPhrase: 'Bad Media Description' }); this.failed(request, SIP.C.causes.BAD_MEDIA_DESCRIPTION); this.terminated(request, SIP.C.causes.BAD_MEDIA_DESCRIPTION); }.bind(this) ); } else { this.terminate({ statusCode: '488', reasonPhrase: 'Bad Media Description' }); this.failed(request, SIP.C.causes.BAD_MEDIA_DESCRIPTION); this.terminated(request, SIP.C.causes.BAD_MEDIA_DESCRIPTION); } } else { SIP.Timers.clearTimeout(this.timers.rel1xxTimer); SIP.Timers.clearTimeout(this.timers.prackTimer); request.reply(200); if (this.status === C.STATUS_ANSWERED_WAITING_FOR_PRACK) { this.status = C.STATUS_EARLY_MEDIA; this.accept(); } this.status = C.STATUS_EARLY_MEDIA; //REVISIT this.mute(); } } else if(this.status === C.STATUS_EARLY_MEDIA) { request.reply(200); } break; default: Session.prototype.receiveRequest.apply(this, [request]); break; } }, onTransportError: function() { if (this.status !== C.STATUS_CONFIRMED && this.status !== C.STATUS_TERMINATED) { this.failed(null, SIP.C.causes.CONNECTION_ERROR); } }, onRequestTimeout: function() { if (this.status === C.STATUS_CONFIRMED) { this.terminated(null, SIP.C.causes.REQUEST_TIMEOUT); } else if (this.status !== C.STATUS_TERMINATED) { this.failed(null, SIP.C.causes.REQUEST_TIMEOUT); this.terminated(null, SIP.C.causes.REQUEST_TIMEOUT); } } }; SIP.InviteServerContext = InviteServerContext; InviteClientContext = function(ua, target, options) { options = Object.create(Session.desugar(options)); options.params = Object.create(options.params || Object.prototype); var iceServers, extraHeaders = (options.extraHeaders || []).slice(), stunServers = options.stunServers || null, turnServers = options.turnServers || null, mediaHandlerFactory = options.mediaHandlerFactory || ua.configuration.mediaHandlerFactory, isMediaSupported = mediaHandlerFactory.isSupported; // Check WebRTC support if (isMediaSupported && !isMediaSupported()) { throw new SIP.Exceptions.NotSupportedError('Media not supported'); } this.RTCConstraints = options.RTCConstraints || {}; this.inviteWithoutSdp = options.inviteWithoutSdp || false; // Set anonymous property this.anonymous = options.anonymous || false; // Custom data to be sent either in INVITE or in ACK this.renderbody = options.renderbody || null; this.rendertype = options.rendertype || 'text/plain'; options.params.from_tag = this.from_tag; /* Do not add ;ob in initial forming dialog requests if the registration over * the current connection got a GRUU URI. */ this.contact = ua.contact.toString({ anonymous: this.anonymous, outbound: this.anonymous ? !ua.contact.temp_gruu : !ua.contact.pub_gruu }); if (this.anonymous) { options.params.from_displayName = 'Anonymous'; options.params.from_uri = 'sip:anonymous@anonymous.invalid'; extraHeaders.push('P-Preferred-Identity: '+ ua.configuration.uri.toString()); extraHeaders.push('Privacy: id'); } extraHeaders.push('Contact: '+ this.contact); extraHeaders.push('Allow: '+ SIP.UA.C.ALLOWED_METHODS.toString()); if (this.inviteWithoutSdp && this.renderbody) { extraHeaders.push('Content-Type: ' + this.rendertype); extraHeaders.push('Content-Disposition: render;handling=optional'); } if (ua.configuration.rel100 === SIP.C.supported.REQUIRED) { extraHeaders.push('Require: 100rel'); } if (ua.configuration.replaces === SIP.C.supported.REQUIRED) { extraHeaders.push('Require: replaces'); } options.extraHeaders = extraHeaders; SIP.Utils.augment(this, SIP.ClientContext, [ua, SIP.C.INVITE, target, options]); SIP.Utils.augment(this, SIP.Session, [mediaHandlerFactory]); // Check Session Status if (this.status !== C.STATUS_NULL) { throw new SIP.Exceptions.InvalidStateError(this.status); } // Session parameter initialization this.from_tag = SIP.Utils.newTag(); // OutgoingSession specific parameters this.isCanceled = false; this.received_100 = false; this.method = SIP.C.INVITE; this.receiveNonInviteResponse = this.receiveResponse; this.receiveResponse = this.receiveInviteResponse; this.logger = ua.getLogger('sip.inviteclientcontext'); if (stunServers) { iceServers = this.ua.getConfigurationCheck().optional['stunServers'](stunServers); if (!iceServers) { throw new TypeError('Invalid stunServers: '+ stunServers); } else { this.stunServers = iceServers; } } if (turnServers) { iceServers = this.ua.getConfigurationCheck().optional['turnServers'](turnServers); if (!iceServers) { throw new TypeError('Invalid turnServers: '+ turnServers); } else { this.turnServers = iceServers; } } ua.applicants[this] = this; this.id = this.request.call_id + this.from_tag; //Initialize Media Session this.mediaHandler = this.mediaHandlerFactory(this, { RTCConstraints: this.RTCConstraints, stunServers: this.stunServers, turnServers: this.turnServers }); if (this.mediaHandler && this.mediaHandler.getRemoteStreams) { this.getRemoteStreams = this.mediaHandler.getRemoteStreams.bind(this.mediaHandler); this.getLocalStreams = this.mediaHandler.getLocalStreams.bind(this.mediaHandler); } SIP.Utils.optionsOverride(options, 'media', 'mediaConstraints', true, this.logger, this.ua.configuration.media); this.mediaHint = options.media; this.onInfo = options.onInfo; }; InviteClientContext.prototype = { invite: function () { var self = this; //Save the session into the ua sessions collection. //Note: placing in constructor breaks call to request.cancel on close... User does not need this anyway this.ua.sessions[this.id] = this; //Note: due to the way Firefox handles gUM calls, it is recommended to make the gUM call at the app level // and hand sip.js a stream as the mediaHint if (this.inviteWithoutSdp) { //just send an invite with no sdp... this.request.body = self.renderbody; this.status = C.STATUS_INVITE_SENT; this.send(); } else { this.mediaHandler.getDescription(self.mediaHint) .then( function onSuccess(description) { if (self.isCanceled || self.status === C.STATUS_TERMINATED) { return; } self.hasOffer = true; self.request.body = description; self.status = C.STATUS_INVITE_SENT; self.send(); }, function onFailure() { if (self.status === C.STATUS_TERMINATED) { return; } // TODO...fail out //self.failed(null, SIP.C.causes.USER_DENIED_MEDIA_ACCESS); //self.failed(null, SIP.C.causes.WEBRTC_ERROR); self.failed(null, SIP.C.causes.WEBRTC_ERROR); self.terminated(null, SIP.C.causes.WEBRTC_ERROR); } ); } return this; }, receiveInviteResponse: function(response) { var cause, //localMedia, session = this, id = response.call_id + response.from_tag + response.to_tag, extraHeaders = [], options = {}; if (this.status === C.STATUS_TERMINATED || response.method !== SIP.C.INVITE) { return; } if (this.dialog && (response.status_code >= 200 && response.status_code <= 299)) { if (id !== this.dialog.id.toString() ) { if (!this.createDialog(response, 'UAC', true)) { return; } this.earlyDialogs[id].sendRequest(this, SIP.C.ACK, { body: SIP.Utils.generateFakeSDP(response.body) }); this.earlyDialogs[id].sendRequest(this, SIP.C.BYE); /* NOTE: This fails because the forking proxy does not recognize that an unanswerable * leg (due to peerConnection limitations) has been answered first. If your forking * proxy does not hang up all unanswered branches on the first branch answered, remove this. */ if(this.status !== C.STATUS_CONFIRMED) { this.failed(response, SIP.C.causes.WEBRTC_ERROR); this.terminated(response, SIP.C.causes.WEBRTC_ERROR); } return; } else if (this.status === C.STATUS_CONFIRMED) { this.sendRequest(SIP.C.ACK,{cseq: response.cseq}); return; } else if (!this.hasAnswer) { // invite w/o sdp is waiting for callback //an invite with sdp must go on, and hasAnswer is true return; } } if (this.dialog && response.status_code < 200) { /* Early media has been set up with at least one other different branch, but a final 2xx response hasn't been received */ if (this.dialog.pracked.indexOf(response.getHeader('rseq')) !== -1 || (this.dialog.pracked[this.dialog.pracked.length-1] >= response.getHeader('rseq') && this.dialog.pracked.length > 0)) { return; } if (!this.earlyDialogs[id] && !this.createDialog(response, 'UAC', true)) { return; } if (this.earlyDialogs[id].pracked.indexOf(response.getHeader('rseq')) !== -1 || (this.earlyDialogs[id].pracked[this.earlyDialogs[id].pracked.length-1] >= response.getHeader('rseq') && this.earlyDialogs[id].pracked.length > 0)) { return; } extraHeaders.push('RAck: ' + response.getHeader('rseq') + ' ' + response.getHeader('cseq')); this.earlyDialogs[id].pracked.push(response.getHeader('rseq')); this.earlyDialogs[id].sendRequest(this, SIP.C.PRACK, { extraHeaders: extraHeaders, body: SIP.Utils.generateFakeSDP(response.body) }); return; } // Proceed to cancellation if the user requested. if(this.isCanceled) { if(response.status_code >= 100 && response.status_code < 200) { this.request.cancel(this.cancelReason, extraHeaders); this.canceled(null); } else if(response.status_code >= 200 && response.status_code < 299) { this.acceptAndTerminate(response); this.emit('bye', this.request); } else if (response.status_code >= 300) { cause = SIP.C.REASON_PHRASE[response.status_code] || SIP.C.causes.CANCELED; this.rejected(response, cause); this.failed(response, cause); this.terminated(response, cause); } return; } switch(true) { case /^100$/.test(response.status_code): this.received_100 = true; this.emit('progress', response); break; case (/^1[0-9]{2}$/.test(response.status_code)): // Do nothing with 1xx responses without To tag. if(!response.to_tag) { this.logger.warn('1xx response received without to tag'); break; } // Create Early Dialog if 1XX comes with contact if(response.hasHeader('contact')) { // An error on dialog creation will fire 'failed' event if (!this.createDialog(response, 'UAC', true)) { break; } } this.status = C.STATUS_1XX_RECEIVED; if(response.hasHeader('require') && response.getHeader('require').indexOf('100rel') !== -1) { // Do nothing if this.dialog is already confirmed if (this.dialog || !this.earlyDialogs[id]) { break; } if (this.earlyDialogs[id].pracked.indexOf(response.getHeader('rseq')) !== -1 || (this.earlyDialogs[id].pracked[this.earlyDialogs[id].pracked.length-1] >= response.getHeader('rseq') && this.earlyDialogs[id].pracked.length > 0)) { return; } if (!this.mediaHandler.hasDescription(response)) { extraHeaders.push('RAck: ' + response.getHeader('rseq') + ' ' + response.getHeader('cseq')); this.earlyDialogs[id].pracked.push(response.getHeader('rseq')); this.earlyDialogs[id].sendRequest(this, SIP.C.PRACK, { extraHeaders: extraHeaders }); this.emit('progress', response); } else if (this.hasOffer) { if (!this.createDialog(response, 'UAC')) { break; } this.hasAnswer = true; this.dialog.pracked.push(response.getHeader('rseq')); this.mediaHandler.setDescription(response) .then( function onSuccess () { extraHeaders.push('RAck: ' + response.getHeader('rseq') + ' ' + response.getHeader('cseq')); session.sendRequest(SIP.C.PRACK, { extraHeaders: extraHeaders, receiveResponse: function() {} }); session.status = C.STATUS_EARLY_MEDIA; session.mute(); session.emit('progress', response); /* if (session.status === C.STATUS_EARLY_MEDIA) { localMedia = session.mediaHandler.localMedia; if (localMedia.getAudioTracks().length > 0) { localMedia.getAudioTracks()[0].enabled = false; } if (localMedia.getVideoTracks().length > 0) { localMedia.getVideoTracks()[0].enabled = false; } }*/ }, function onFailure (e) { session.logger.warn(e); session.acceptAndTerminate(response, 488, 'Not Acceptable Here'); session.failed(response, SIP.C.causes.BAD_MEDIA_DESCRIPTION); } ); } else { var earlyDialog = this.earlyDialogs[id]; var earlyMedia = earlyDialog.mediaHandler; earlyDialog.pracked.push(response.getHeader('rseq')); earlyMedia.setDescription(response) .then(earlyMedia.getDescription.bind(earlyMedia, session.mediaHint)) .then(function onSuccess(description) { extraHeaders.push('RAck: ' + response.getHeader('rseq') + ' ' + response.getHeader('cseq')); earlyDialog.sendRequest(session, SIP.C.PRACK, { extraHeaders: extraHeaders, body: description }); session.status = C.STATUS_EARLY_MEDIA; session.emit('progress', response); }) .catch(function onFailure(e) { if (e instanceof SIP.Exceptions.GetDescriptionError) { earlyDialog.pracked.push(response.getHeader('rseq')); if (session.status === C.STATUS_TERMINATED) { return; } // TODO - fail out on error // session.failed(gum error); session.failed(null, SIP.C.causes.WEBRTC_ERROR); session.terminated(null, SIP.C.causes.WEBRTC_ERROR); } else { earlyDialog.pracked.splice(earlyDialog.pracked.indexOf(response.getHeader('rseq')), 1); // Could not set remote description session.logger.warn('invalid description'); session.logger.warn(e); } }); } } else { this.emit('progress', response); } break; case /^2[0-9]{2}$/.test(response.status_code): var cseq = this.request.cseq + ' ' + this.request.method; if (cseq !== response.getHeader('cseq')) { break; } if (this.status === C.STATUS_EARLY_MEDIA && this.dialog) { this.status = C.STATUS_CONFIRMED; this.unmute(); /*localMedia = this.mediaHandler.localMedia; if (localMedia.getAudioTracks().length > 0) { localMedia.getAudioTracks()[0].enabled = true; } if (localMedia.getVideoTracks().length > 0) { localMedia.getVideoTracks()[0].enabled = true; }*/ options = {}; if (this.renderbody) { extraHeaders.push('Content-Type: ' + this.rendertype); options.extraHeaders = extraHeaders; options.body = this.renderbody; } options.cseq = response.cseq; this.sendRequest(SIP.C.ACK, options); this.accepted(response); break; } // Do nothing if this.dialog is already confirmed if (this.dialog) { break; } // This is an invite without sdp if (!this.hasOffer) { if (this.earlyDialogs[id] && this.earlyDialogs[id].mediaHandler.localMedia) { //REVISIT this.hasOffer = true; this.hasAnswer = true; this.mediaHandler = this.earlyDialogs[id].mediaHandler; if (!this.createDialog(response, 'UAC')) { break; } this.status = C.STATUS_CONFIRMED; this.sendRequest(SIP.C.ACK, {cseq:response.cseq}); this.unmute(); /* localMedia = session.mediaHandler.localMedia; if (localMedia.getAudioTracks().length > 0) { localMedia.getAudioTracks()[0].enabled = true; } if (localMedia.getVideoTracks().length > 0) { localMedia.getVideoTracks()[0].enabled = true; }*/ this.accepted(response); } else { if(!this.mediaHandler.hasDescription(response)) { this.acceptAndTerminate(response, 400, 'Missing session description'); this.failed(response, SIP.C.causes.BAD_MEDIA_DESCRIPTION); break; } if (!this.createDialog(response, 'UAC')) { break; } this.hasOffer = true; this.mediaHandler.setDescription(response) .then(this.mediaHandler.getDescription.bind(this.mediaHandler, this.mediaHint)) .then(function onSuccess(description) { //var localMedia; if(session.isCanceled || session.status === C.STATUS_TERMINATED) { return; } session.status = C.STATUS_CONFIRMED; session.hasAnswer = true; session.unmute(); /*localMedia = session.mediaHandler.localMedia; if (localMedia.getAudioTracks().length > 0) { localMedia.getAudioTracks()[0].enabled = true; } if (localMedia.getVideoTracks().length > 0) { localMedia.getVideoTracks()[0].enabled = true; }*/ session.sendRequest(SIP.C.ACK,{ body: description, cseq:response.cseq }); session.accepted(response); }) .catch(function onFailure(e) { if (e instanceof SIP.Exceptions.GetDescriptionError) { // TODO do something here session.logger.warn("there was a problem"); } else { session.logger.warn('invalid description'); session.logger.warn(e); session.acceptAndTerminate(response, 488, 'Invalid session description'); session.failed(response, SIP.C.causes.BAD_MEDIA_DESCRIPTION); } }); } } else if (this.hasAnswer){ if (this.renderbody) { extraHeaders.push('Content-Type: ' + session.rendertype); options.extraHeaders = extraHeaders; options.body = this.renderbody; } this.sendRequest(SIP.C.ACK, options); } else { if(!this.mediaHandler.hasDescription(response)) { this.acceptAndTerminate(response, 400, 'Missing session description'); this.failed(response, SIP.C.causes.BAD_MEDIA_DESCRIPTION); break; } if (!this.createDialog(response, 'UAC')) { break; } this.hasAnswer = true; this.mediaHandler.setDescription(response) .then( function onSuccess () { var options = {};//,localMedia; session.status = C.STATUS_CONFIRMED; session.unmute(); /*localMedia = session.mediaHandler.localMedia; if (localMedia.getAudioTracks().length > 0) { localMedia.getAudioTracks()[0].enabled = true; } if (localMedia.getVideoTracks().length > 0) { localMedia.getVideoTracks()[0].enabled = true; }*/ if (session.renderbody) { extraHeaders.push('Content-Type: ' + session.rendertype); options.extraHeaders = extraHeaders; options.body = session.renderbody; } options.cseq = response.cseq; session.sendRequest(SIP.C.ACK, options); session.accepted(response); }, function onFailure (e) { session.logger.warn(e); session.acceptAndTerminate(response, 488, 'Not Acceptable Here'); session.failed(response, SIP.C.causes.BAD_MEDIA_DESCRIPTION); } ); } break; default: cause = SIP.Utils.sipErrorCause(response.status_code); this.rejected(response, cause); this.failed(response, cause); this.terminated(response, cause); } }, cancel: function(options) { options = options || {}; options.extraHeaders = (options.extraHeaders || []).slice(); // Check Session Status if (this.status === C.STATUS_TERMINATED || this.status === C.STATUS_CONFIRMED) { throw new SIP.Exceptions.InvalidStateError(this.status); } this.logger.log('canceling RTCSession'); var cancel_reason = SIP.Utils.getCancelReason(options.status_code, options.reason_phrase); // Check Session Status if (this.status === C.STATUS_NULL || (this.status === C.STATUS_INVITE_SENT && !this.received_100)) { this.isCanceled = true; this.cancelReason = cancel_reason; } else if (this.status === C.STATUS_INVITE_SENT || this.status === C.STATUS_1XX_RECEIVED || this.status === C.STATUS_EARLY_MEDIA) { this.request.cancel(cancel_reason, options.extraHeaders); } return this.canceled(); }, terminate: function(options) { if (this.status === C.STATUS_TERMINATED) { return this; } if (this.status === C.STATUS_WAITING_FOR_ACK || this.status === C.STATUS_CONFIRMED) { this.bye(options); } else { this.cancel(options); } return this; }, receiveRequest: function(request) { // ICC RECEIVE REQUEST // Reject CANCELs if (request.method === SIP.C.CANCEL) { // TODO; make this a switch when it gets added } if (request.method === SIP.C.ACK && this.status === C.STATUS_WAITING_FOR_ACK) { SIP.Timers.clearTimeout(this.timers.ackTimer); SIP.Timers.clearTimeout(this.timers.invite2xxTimer); this.status = C.STATUS_CONFIRMED; this.unmute(); this.accepted(); } return Session.prototype.receiveRequest.apply(this, [request]); }, onTransportError: function() { if (this.status !== C.STATUS_CONFIRMED && this.status !== C.STATUS_TERMINATED) { this.failed(null, SIP.C.causes.CONNECTION_ERROR); } }, onRequestTimeout: function() { if (this.status === C.STATUS_CONFIRMED) { this.terminated(null, SIP.C.causes.REQUEST_TIMEOUT); } else if (this.status !== C.STATUS_TERMINATED) { this.failed(null, SIP.C.causes.REQUEST_TIMEOUT); this.terminated(null, SIP.C.causes.REQUEST_TIMEOUT); } } }; SIP.InviteClientContext = InviteClientContext; }; },{"./Session/DTMF":24}],24:[function(require,module,exports){ "use strict"; /** * @fileoverview DTMF */ /** * @class DTMF * @param {SIP.Session} session */ module.exports = function (SIP) { var DTMF, C = { MIN_DURATION: 70, MAX_DURATION: 6000, DEFAULT_DURATION: 100, MIN_INTER_TONE_GAP: 50, DEFAULT_INTER_TONE_GAP: 500 }; DTMF = function(session, tone, options) { var duration, interToneGap; if (tone === undefined) { throw new TypeError('Not enough arguments'); } this.logger = session.ua.getLogger('sip.invitecontext.dtmf', session.id); this.owner = session; this.direction = null; options = options || {}; duration = options.duration || null; interToneGap = options.interToneGap || null; // Check tone type if (typeof tone === 'string' ) { tone = tone.toUpperCase(); } else if (typeof tone === 'number') { tone = tone.toString(); } else { throw new TypeError('Invalid tone: '+ tone); } // Check tone value if (!tone.match(/^[0-9A-D#*]$/)) { throw new TypeError('Invalid tone: '+ tone); } else { this.tone = tone; } // Check duration if (duration && !SIP.Utils.isDecimal(duration)) { throw new TypeError('Invalid tone duration: '+ duration); } else if (!duration) { duration = DTMF.C.DEFAULT_DURATION; } else if (duration < DTMF.C.MIN_DURATION) { this.logger.warn('"duration" value is lower than the minimum allowed, setting it to '+ DTMF.C.MIN_DURATION+ ' milliseconds'); duration = DTMF.C.MIN_DURATION; } else if (duration > DTMF.C.MAX_DURATION) { this.logger.warn('"duration" value is greater than the maximum allowed, setting it to '+ DTMF.C.MAX_DURATION +' milliseconds'); duration = DTMF.C.MAX_DURATION; } else { duration = Math.abs(duration); } this.duration = duration; // Check interToneGap if (interToneGap && !SIP.Utils.isDecimal(interToneGap)) { throw new TypeError('Invalid interToneGap: '+ interToneGap); } else if (!interToneGap) { interToneGap = DTMF.C.DEFAULT_INTER_TONE_GAP; } else if (interToneGap < DTMF.C.MIN_INTER_TONE_GAP) { this.logger.warn('"interToneGap" value is lower than the minimum allowed, setting it to '+ DTMF.C.MIN_INTER_TONE_GAP +' milliseconds'); interToneGap = DTMF.C.MIN_INTER_TONE_GAP; } else { interToneGap = Math.abs(interToneGap); } this.interToneGap = interToneGap; }; DTMF.prototype = Object.create(SIP.EventEmitter.prototype); DTMF.prototype.send = function(options) { var extraHeaders, body = {}; this.direction = 'outgoing'; // Check RTCSession Status if (this.owner.status !== SIP.Session.C.STATUS_CONFIRMED && this.owner.status !== SIP.Session.C.STATUS_WAITING_FOR_ACK) { throw new SIP.Exceptions.InvalidStateError(this.owner.status); } // Get DTMF options options = options || {}; extraHeaders = options.extraHeaders ? options.extraHeaders.slice() : []; body.contentType = 'application/dtmf-relay'; body.body = "Signal= " + this.tone + "\r\n"; body.body += "Duration= " + this.duration; this.request = this.owner.dialog.sendRequest(this, SIP.C.INFO, { extraHeaders: extraHeaders, body: body }); this.owner.emit('dtmf', this.request, this); }; /** * @private */ DTMF.prototype.receiveResponse = function(response) { var cause; switch(true) { case /^1[0-9]{2}$/.test(response.status_code): // Ignore provisional responses. break; case /^2[0-9]{2}$/.test(response.status_code): this.emit('succeeded', { originator: 'remote', response: response }); break; default: cause = SIP.Utils.sipErrorCause(response.status_code); this.emit('failed', response, cause); break; } }; /** * @private */ DTMF.prototype.onRequestTimeout = function() { this.emit('failed', null, SIP.C.causes.REQUEST_TIMEOUT); this.owner.onRequestTimeout(); }; /** * @private */ DTMF.prototype.onTransportError = function() { this.emit('failed', null, SIP.C.causes.CONNECTION_ERROR); this.owner.onTransportError(); }; /** * @private */ DTMF.prototype.onDialogError = function(response) { this.emit('failed', response, SIP.C.causes.DIALOG_ERROR); this.owner.onDialogError(response); }; /** * @private */ DTMF.prototype.init_incoming = function(request) { this.direction = 'incoming'; this.request = request; request.reply(200); if (!this.tone || !this.duration) { this.logger.warn('invalid INFO DTMF received, discarded'); } else { this.owner.emit('dtmf', request, this); } }; DTMF.C = C; return DTMF; }; },{}],25:[function(require,module,exports){ "use strict"; /** * @fileoverview SIP Subscriber (SIP-Specific Event Notifications RFC6665) */ /** * @augments SIP * @class Class creating a SIP Subscription. */ module.exports = function (SIP) { SIP.Subscription = function (ua, target, event, options) { options = Object.create(options || Object.prototype); this.extraHeaders = options.extraHeaders = (options.extraHeaders || []).slice(); this.id = null; this.state = 'init'; if (!event) { throw new TypeError('Event necessary to create a subscription.'); } else { //TODO: check for valid events here probably make a list in SIP.C; or leave it up to app to check? //The check may need to/should probably occur on the other side, this.event = event; } if(typeof options.expires !== 'number'){ ua.logger.warn('expires must be a number. Using default of 3600.'); this.expires = 3600; } else { this.expires = options.expires; } options.extraHeaders.push('Event: ' + this.event); options.extraHeaders.push('Expires: ' + this.expires); if (options.body) { this.body = options.body; } this.contact = ua.contact.toString(); options.extraHeaders.push('Contact: '+ this.contact); options.extraHeaders.push('Allow: '+ SIP.UA.C.ALLOWED_METHODS.toString()); SIP.Utils.augment(this, SIP.ClientContext, [ua, SIP.C.SUBSCRIBE, target, options]); this.logger = ua.getLogger('sip.subscription'); this.dialog = null; this.timers = {N: null, sub_duration: null}; this.errorCodes = [404,405,410,416,480,481,482,483,484,485,489,501,604]; }; SIP.Subscription.prototype = { subscribe: function() { var sub = this; //these states point to an existing subscription, no subscribe is necessary if (this.state === 'active') { this.refresh(); return this; } else if (this.state === 'notify_wait') { return this; } SIP.Timers.clearTimeout(this.timers.sub_duration); SIP.Timers.clearTimeout(this.timers.N); this.timers.N = SIP.Timers.setTimeout(sub.timer_fire.bind(sub), SIP.Timers.TIMER_N); this.ua.earlySubscriptions[this.request.call_id + this.request.from.parameters.tag + this.event] = this; this.send(); this.state = 'notify_wait'; return this; }, refresh: function () { if (this.state === 'terminated' || this.state === 'pending' || this.state === 'notify_wait') { return; } this.dialog.sendRequest(this, SIP.C.SUBSCRIBE, { extraHeaders: this.extraHeaders, body: this.body }); }, receiveResponse: function(response) { var expires, sub = this, cause = SIP.Utils.getReasonPhrase(response.status_code); if ((this.state === 'notify_wait' && response.status_code >= 300) || (this.state !== 'notify_wait' && this.errorCodes.indexOf(response.status_code) !== -1)) { this.failed(response, null); } else if (/^2[0-9]{2}$/.test(response.status_code)){ this.emit('accepted', response, cause); //As we don't support RFC 5839 or other extensions where the NOTIFY is optional, timer N will not be cleared //SIP.Timers.clearTimeout(this.timers.N); expires = response.getHeader('Expires'); if (expires && expires <= this.expires) { // Preserve new expires value for subsequent requests this.expires = expires; this.timers.sub_duration = SIP.Timers.setTimeout(sub.refresh.bind(sub), expires * 900); } else { if (!expires) { this.logger.warn('Expires header missing in a 200-class response to SUBSCRIBE'); this.failed(response, SIP.C.EXPIRES_HEADER_MISSING); } else { this.logger.warn('Expires header in a 200-class response to SUBSCRIBE with a higher value than the one in the request'); this.failed(response, SIP.C.INVALID_EXPIRES_HEADER); } } } else if (response.statusCode > 300) { this.emit('failed', response, cause); this.emit('rejected', response, cause); } }, unsubscribe: function() { var extraHeaders = [], sub = this; this.state = 'terminated'; extraHeaders.push('Event: ' + this.event); extraHeaders.push('Expires: 0'); extraHeaders.push('Contact: '+ this.contact); extraHeaders.push('Allow: '+ SIP.UA.C.ALLOWED_METHODS.toString()); //makes sure expires isn't set, and other typical resubscribe behavior this.receiveResponse = function(){}; this.dialog.sendRequest(this, this.method, { extraHeaders: extraHeaders, body: this.body }); SIP.Timers.clearTimeout(this.timers.sub_duration); SIP.Timers.clearTimeout(this.timers.N); this.timers.N = SIP.Timers.setTimeout(sub.timer_fire.bind(sub), SIP.Timers.TIMER_N); }, /** * @private */ timer_fire: function(){ if (this.state === 'terminated') { this.terminateDialog(); SIP.Timers.clearTimeout(this.timers.N); SIP.Timers.clearTimeout(this.timers.sub_duration); delete this.ua.subscriptions[this.id]; } else if (this.state === 'notify_wait' || this.state === 'pending') { this.close(); } else { this.refresh(); } }, /** * @private */ close: function() { if (this.state === 'notify_wait') { this.state = 'terminated'; SIP.Timers.clearTimeout(this.timers.N); SIP.Timers.clearTimeout(this.timers.sub_duration); this.receiveResponse = function(){}; delete this.ua.earlySubscriptions[this.request.call_id + this.request.from.parameters.tag + this.event]; } else if (this.state !== 'terminated') { this.unsubscribe(); } }, /** * @private */ createConfirmedDialog: function(message, type) { var dialog; this.terminateDialog(); dialog = new SIP.Dialog(this, message, type); dialog.invite_seqnum = this.request.cseq; dialog.local_seqnum = this.request.cseq; if(!dialog.error) { this.dialog = dialog; return true; } // Dialog not created due to an error else { return false; } }, /** * @private */ terminateDialog: function() { if(this.dialog) { delete this.ua.subscriptions[this.id]; this.dialog.terminate(); delete this.dialog; } }, /** * @private */ receiveRequest: function(request) { var sub_state, sub = this; function setExpiresTimeout() { if (sub_state.expires) { SIP.Timers.clearTimeout(sub.timers.sub_duration); sub_state.expires = Math.min(sub.expires, Math.max(sub_state.expires, 0)); sub.timers.sub_duration = SIP.Timers.setTimeout(sub.refresh.bind(sub), sub_state.expires * 900); } } if (!this.matchEvent(request)) { //checks event and subscription_state headers request.reply(489); return; } if (!this.dialog) { if (this.createConfirmedDialog(request,'UAS')) { this.id = this.dialog.id.toString(); delete this.ua.earlySubscriptions[this.request.call_id + this.request.from.parameters.tag + this.event]; this.ua.subscriptions[this.id] = this; // UPDATE ROUTE SET TO BE BACKWARDS COMPATIBLE? } } sub_state = request.parseHeader('Subscription-State'); request.reply(200, SIP.C.REASON_200); SIP.Timers.clearTimeout(this.timers.N); this.emit('notify', {request: request}); // if we've set state to terminated, no further processing should take place // and we are only interested in cleaning up after the appropriate NOTIFY if (this.state === 'terminated') { if (sub_state.state === 'terminated') { this.terminateDialog(); SIP.Timers.clearTimeout(this.timers.N); SIP.Timers.clearTimeout(this.timers.sub_duration); delete this.ua.subscriptions[this.id]; } return; } switch (sub_state.state) { case 'active': this.state = 'active'; setExpiresTimeout(); break; case 'pending': if (this.state === 'notify_wait') { setExpiresTimeout(); } this.state = 'pending'; break; case 'terminated': SIP.Timers.clearTimeout(this.timers.sub_duration); if (sub_state.reason) { this.logger.log('terminating subscription with reason '+ sub_state.reason); switch (sub_state.reason) { case 'deactivated': case 'timeout': this.subscribe(); return; case 'probation': case 'giveup': if(sub_state.params && sub_state.params['retry-after']) { this.timers.sub_duration = SIP.Timers.setTimeout(sub.subscribe.bind(sub), sub_state.params['retry-after']); } else { this.subscribe(); } return; case 'rejected': case 'noresource': case 'invariant': break; } } this.close(); break; } }, failed: function(response, cause) { this.close(); this.emit('failed', response, cause); this.emit('rejected', response, cause); return this; }, onDialogError: function(response) { this.failed(response, SIP.C.causes.DIALOG_ERROR); }, /** * @private */ matchEvent: function(request) { var event; // Check mandatory header Event if (!request.hasHeader('Event')) { this.logger.warn('missing Event header'); return false; } // Check mandatory header Subscription-State if (!request.hasHeader('Subscription-State')) { this.logger.warn('missing Subscription-State header'); return false; } // Check whether the event in NOTIFY matches the event in SUBSCRIBE event = request.parseHeader('event').event; if (this.event !== event) { this.logger.warn('event match failed'); request.reply(481, 'Event Match Failed'); return false; } else { return true; } } }; }; },{}],26:[function(require,module,exports){ "use strict"; /** * @fileoverview SIP TIMERS */ /** * @augments SIP */ var T1 = 500, T2 = 4000, T4 = 5000; module.exports = function (timers) { var Timers = { T1: T1, T2: T2, T4: T4, TIMER_B: 64 * T1, TIMER_D: 0 * T1, TIMER_F: 64 * T1, TIMER_H: 64 * T1, TIMER_I: 0 * T1, TIMER_J: 0 * T1, TIMER_K: 0 * T4, TIMER_L: 64 * T1, TIMER_M: 64 * T1, TIMER_N: 64 * T1, PROVISIONAL_RESPONSE_INTERVAL: 60000 // See RFC 3261 Section 13.3.1.1 }; ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval'] .forEach(function (name) { // can't just use timers[name].bind(timers) since it bypasses jasmine's // clock-mocking Timers[name] = function () { return timers[name].apply(timers, arguments); }; }); return Timers; }; },{}],27:[function(require,module,exports){ "use strict"; /** * @fileoverview SIP Transactions */ /** * SIP Transactions module. * @augments SIP */ module.exports = function (SIP) { var C = { // Transaction states STATUS_TRYING: 1, STATUS_PROCEEDING: 2, STATUS_CALLING: 3, STATUS_ACCEPTED: 4, STATUS_COMPLETED: 5, STATUS_TERMINATED: 6, STATUS_CONFIRMED: 7, // Transaction types NON_INVITE_CLIENT: 'nict', NON_INVITE_SERVER: 'nist', INVITE_CLIENT: 'ict', INVITE_SERVER: 'ist' }; function buildViaHeader (request_sender, transport, id) { var via; via = 'SIP/2.0/' + (request_sender.ua.configuration.hackViaTcp ? 'TCP' : transport.server.scheme); via += ' ' + request_sender.ua.configuration.viaHost + ';branch=' + id; if (request_sender.ua.configuration.forceRport) { via += ';rport'; } return via; } /** * @augments SIP.Transactions * @class Non Invite Client Transaction * @param {SIP.RequestSender} request_sender * @param {SIP.OutgoingRequest} request * @param {SIP.Transport} transport */ var NonInviteClientTransaction = function(request_sender, request, transport) { var via; this.type = C.NON_INVITE_CLIENT; this.transport = transport; this.id = 'z9hG4bK' + Math.floor(Math.random() * 10000000); this.request_sender = request_sender; this.request = request; this.logger = request_sender.ua.getLogger('sip.transaction.nict', this.id); via = buildViaHeader(request_sender, transport, this.id); this.request.setHeader('via', via); this.request_sender.ua.newTransaction(this); }; NonInviteClientTransaction.prototype = Object.create(SIP.EventEmitter.prototype); NonInviteClientTransaction.prototype.stateChanged = function(state) { this.state = state; this.emit('stateChanged'); }; NonInviteClientTransaction.prototype.send = function() { var tr = this; this.stateChanged(C.STATUS_TRYING); this.F = SIP.Timers.setTimeout(tr.timer_F.bind(tr), SIP.Timers.TIMER_F); if(!this.transport.send(this.request)) { this.onTransportError(); } }; NonInviteClientTransaction.prototype.onTransportError = function() { this.logger.log('transport error occurred, deleting non-INVITE client transaction ' + this.id); SIP.Timers.clearTimeout(this.F); SIP.Timers.clearTimeout(this.K); this.stateChanged(C.STATUS_TERMINATED); this.request_sender.ua.destroyTransaction(this); this.request_sender.onTransportError(); }; NonInviteClientTransaction.prototype.timer_F = function() { this.logger.log('Timer F expired for non-INVITE client transaction ' + this.id); this.stateChanged(C.STATUS_TERMINATED); this.request_sender.ua.destroyTransaction(this); this.request_sender.onRequestTimeout(); }; NonInviteClientTransaction.prototype.timer_K = function() { this.stateChanged(C.STATUS_TERMINATED); this.request_sender.ua.destroyTransaction(this); }; NonInviteClientTransaction.prototype.receiveResponse = function(response) { var tr = this, status_code = response.status_code; if(status_code < 200) { switch(this.state) { case C.STATUS_TRYING: case C.STATUS_PROCEEDING: this.stateChanged(C.STATUS_PROCEEDING); this.request_sender.receiveResponse(response); break; } } else { switch(this.state) { case C.STATUS_TRYING: case C.STATUS_PROCEEDING: this.stateChanged(C.STATUS_COMPLETED); SIP.Timers.clearTimeout(this.F); if(status_code === 408) { this.request_sender.onRequestTimeout(); } else { this.request_sender.receiveResponse(response); } this.K = SIP.Timers.setTimeout(tr.timer_K.bind(tr), SIP.Timers.TIMER_K); break; case C.STATUS_COMPLETED: break; } } }; /** * @augments SIP.Transactions * @class Invite Client Transaction * @param {SIP.RequestSender} request_sender * @param {SIP.OutgoingRequest} request * @param {SIP.Transport} transport */ var InviteClientTransaction = function(request_sender, request, transport) { var via, tr = this; this.type = C.INVITE_CLIENT; this.transport = transport; this.id = 'z9hG4bK' + Math.floor(Math.random() * 10000000); this.request_sender = request_sender; this.request = request; this.logger = request_sender.ua.getLogger('sip.transaction.ict', this.id); via = buildViaHeader(request_sender, transport, this.id); this.request.setHeader('via', via); this.request_sender.ua.newTransaction(this); // Add the cancel property to the request. //Will be called from the request instance, not the transaction itself. this.request.cancel = function(reason, extraHeaders) { extraHeaders = (extraHeaders || []).slice(); var length = extraHeaders.length; var extraHeadersString = null; for (var idx = 0; idx < length; idx++) { extraHeadersString = (extraHeadersString || '') + extraHeaders[idx].trim() + '\r\n'; } tr.cancel_request(tr, reason, extraHeadersString); }; }; InviteClientTransaction.prototype = Object.create(SIP.EventEmitter.prototype); InviteClientTransaction.prototype.stateChanged = function(state) { this.state = state; this.emit('stateChanged'); }; InviteClientTransaction.prototype.send = function() { var tr = this; this.stateChanged(C.STATUS_CALLING); this.B = SIP.Timers.setTimeout(tr.timer_B.bind(tr), SIP.Timers.TIMER_B); if(!this.transport.send(this.request)) { this.onTransportError(); } }; InviteClientTransaction.prototype.onTransportError = function() { this.logger.log('transport error occurred, deleting INVITE client transaction ' + this.id); SIP.Timers.clearTimeout(this.B); SIP.Timers.clearTimeout(this.D); SIP.Timers.clearTimeout(this.M); this.stateChanged(C.STATUS_TERMINATED); this.request_sender.ua.destroyTransaction(this); if (this.state !== C.STATUS_ACCEPTED) { this.request_sender.onTransportError(); } }; // RFC 6026 7.2 InviteClientTransaction.prototype.timer_M = function() { this.logger.log('Timer M expired for INVITE client transaction ' + this.id); if(this.state === C.STATUS_ACCEPTED) { SIP.Timers.clearTimeout(this.B); this.stateChanged(C.STATUS_TERMINATED); this.request_sender.ua.destroyTransaction(this); } }; // RFC 3261 17.1.1 InviteClientTransaction.prototype.timer_B = function() { this.logger.log('Timer B expired for INVITE client transaction ' + this.id); if(this.state === C.STATUS_CALLING) { this.stateChanged(C.STATUS_TERMINATED); this.request_sender.ua.destroyTransaction(this); this.request_sender.onRequestTimeout(); } }; InviteClientTransaction.prototype.timer_D = function() { this.logger.log('Timer D expired for INVITE client transaction ' + this.id); SIP.Timers.clearTimeout(this.B); this.stateChanged(C.STATUS_TERMINATED); this.request_sender.ua.destroyTransaction(this); }; InviteClientTransaction.prototype.sendACK = function(response) { var tr = this; this.ack = 'ACK ' + this.request.ruri + ' SIP/2.0\r\n'; this.ack += 'Via: ' + this.request.headers['Via'].toString() + '\r\n'; if(this.request.headers['Route']) { this.ack += 'Route: ' + this.request.headers['Route'].toString() + '\r\n'; } this.ack += 'To: ' + response.getHeader('to') + '\r\n'; this.ack += 'From: ' + this.request.headers['From'].toString() + '\r\n'; this.ack += 'Call-ID: ' + this.request.headers['Call-ID'].toString() + '\r\n'; this.ack += 'Content-Length: 0\r\n'; this.ack += 'CSeq: ' + this.request.headers['CSeq'].toString().split(' ')[0]; this.ack += ' ACK\r\n\r\n'; this.D = SIP.Timers.setTimeout(tr.timer_D.bind(tr), SIP.Timers.TIMER_D); this.transport.send(this.ack); }; InviteClientTransaction.prototype.cancel_request = function(tr, reason, extraHeaders) { var request = tr.request; this.cancel = SIP.C.CANCEL + ' ' + request.ruri + ' SIP/2.0\r\n'; this.cancel += 'Via: ' + request.headers['Via'].toString() + '\r\n'; if(this.request.headers['Route']) { this.cancel += 'Route: ' + request.headers['Route'].toString() + '\r\n'; } this.cancel += 'To: ' + request.headers['To'].toString() + '\r\n'; this.cancel += 'From: ' + request.headers['From'].toString() + '\r\n'; this.cancel += 'Call-ID: ' + request.headers['Call-ID'].toString() + '\r\n'; this.cancel += 'CSeq: ' + request.headers['CSeq'].toString().split(' ')[0] + ' CANCEL\r\n'; if(reason) { this.cancel += 'Reason: ' + reason + '\r\n'; } if (extraHeaders) { this.cancel += extraHeaders; } this.cancel += 'Content-Length: 0\r\n\r\n'; // Send only if a provisional response (>100) has been received. if(this.state === C.STATUS_PROCEEDING) { this.transport.send(this.cancel); } }; InviteClientTransaction.prototype.receiveResponse = function(response) { var tr = this, status_code = response.status_code; if(status_code >= 100 && status_code <= 199) { switch(this.state) { case C.STATUS_CALLING: this.stateChanged(C.STATUS_PROCEEDING); this.request_sender.receiveResponse(response); if(this.cancel) { this.transport.send(this.cancel); } break; case C.STATUS_PROCEEDING: this.request_sender.receiveResponse(response); break; } } else if(status_code >= 200 && status_code <= 299) { switch(this.state) { case C.STATUS_CALLING: case C.STATUS_PROCEEDING: this.stateChanged(C.STATUS_ACCEPTED); this.M = SIP.Timers.setTimeout(tr.timer_M.bind(tr), SIP.Timers.TIMER_M); this.request_sender.receiveResponse(response); break; case C.STATUS_ACCEPTED: this.request_sender.receiveResponse(response); break; } } else if(status_code >= 300 && status_code <= 699) { switch(this.state) { case C.STATUS_CALLING: case C.STATUS_PROCEEDING: this.stateChanged(C.STATUS_COMPLETED); this.sendACK(response); this.request_sender.receiveResponse(response); break; case C.STATUS_COMPLETED: this.sendACK(response); break; } } }; /** * @augments SIP.Transactions * @class ACK Client Transaction * @param {SIP.RequestSender} request_sender * @param {SIP.OutgoingRequest} request * @param {SIP.Transport} transport */ var AckClientTransaction = function(request_sender, request, transport) { var via; this.transport = transport; this.id = 'z9hG4bK' + Math.floor(Math.random() * 10000000); this.request_sender = request_sender; this.request = request; this.logger = request_sender.ua.getLogger('sip.transaction.nict', this.id); via = buildViaHeader(request_sender, transport, this.id); this.request.setHeader('via', via); }; AckClientTransaction.prototype = Object.create(SIP.EventEmitter.prototype); AckClientTransaction.prototype.send = function() { if(!this.transport.send(this.request)) { this.onTransportError(); } }; AckClientTransaction.prototype.onTransportError = function() { this.logger.log('transport error occurred, for an ACK client transaction ' + this.id); this.request_sender.onTransportError(); }; /** * @augments SIP.Transactions * @class Non Invite Server Transaction * @param {SIP.IncomingRequest} request * @param {SIP.UA} ua */ var NonInviteServerTransaction = function(request, ua) { this.type = C.NON_INVITE_SERVER; this.id = request.via_branch; this.request = request; this.transport = request.transport; this.ua = ua; this.last_response = ''; request.server_transaction = this; this.logger = ua.getLogger('sip.transaction.nist', this.id); this.state = C.STATUS_TRYING; ua.newTransaction(this); }; NonInviteServerTransaction.prototype = Object.create(SIP.EventEmitter.prototype); NonInviteServerTransaction.prototype.stateChanged = function(state) { this.state = state; this.emit('stateChanged'); }; NonInviteServerTransaction.prototype.timer_J = function() { this.logger.log('Timer J expired for non-INVITE server transaction ' + this.id); this.stateChanged(C.STATUS_TERMINATED); this.ua.destroyTransaction(this); }; NonInviteServerTransaction.prototype.onTransportError = function() { if (!this.transportError) { this.transportError = true; this.logger.log('transport error occurred, deleting non-INVITE server transaction ' + this.id); SIP.Timers.clearTimeout(this.J); this.stateChanged(C.STATUS_TERMINATED); this.ua.destroyTransaction(this); } }; NonInviteServerTransaction.prototype.receiveResponse = function(status_code, response) { var tr = this; var deferred = SIP.Utils.defer(); if(status_code === 100) { /* RFC 4320 4.1 * 'A SIP element MUST NOT * send any provisional response with a * Status-Code other than 100 to a non-INVITE request.' */ switch(this.state) { case C.STATUS_TRYING: this.stateChanged(C.STATUS_PROCEEDING); if(!this.transport.send(response)) { this.onTransportError(); } break; case C.STATUS_PROCEEDING: this.last_response = response; if(!this.transport.send(response)) { this.onTransportError(); deferred.reject(); } else { deferred.resolve(); } break; } } else if(status_code >= 200 && status_code <= 699) { switch(this.state) { case C.STATUS_TRYING: case C.STATUS_PROCEEDING: this.stateChanged(C.STATUS_COMPLETED); this.last_response = response; this.J = SIP.Timers.setTimeout(tr.timer_J.bind(tr), SIP.Timers.TIMER_J); if(!this.transport.send(response)) { this.onTransportError(); deferred.reject(); } else { deferred.resolve(); } break; case C.STATUS_COMPLETED: break; } } return deferred.promise; }; /** * @augments SIP.Transactions * @class Invite Server Transaction * @param {SIP.IncomingRequest} request * @param {SIP.UA} ua */ var InviteServerTransaction = function(request, ua) { this.type = C.INVITE_SERVER; this.id = request.via_branch; this.request = request; this.transport = request.transport; this.ua = ua; this.last_response = ''; request.server_transaction = this; this.logger = ua.getLogger('sip.transaction.ist', this.id); this.state = C.STATUS_PROCEEDING; ua.newTransaction(this); this.resendProvisionalTimer = null; request.reply(100); }; InviteServerTransaction.prototype = Object.create(SIP.EventEmitter.prototype); InviteServerTransaction.prototype.stateChanged = function(state) { this.state = state; this.emit('stateChanged'); }; InviteServerTransaction.prototype.timer_H = function() { this.logger.log('Timer H expired for INVITE server transaction ' + this.id); if(this.state === C.STATUS_COMPLETED) { this.logger.warn('transactions', 'ACK for INVITE server transaction was never received, call will be terminated'); } this.stateChanged(C.STATUS_TERMINATED); this.ua.destroyTransaction(this); }; InviteServerTransaction.prototype.timer_I = function() { this.stateChanged(C.STATUS_TERMINATED); this.ua.destroyTransaction(this); }; // RFC 6026 7.1 InviteServerTransaction.prototype.timer_L = function() { this.logger.log('Timer L expired for INVITE server transaction ' + this.id); if(this.state === C.STATUS_ACCEPTED) { this.stateChanged(C.STATUS_TERMINATED); this.ua.destroyTransaction(this); } }; InviteServerTransaction.prototype.onTransportError = function() { if (!this.transportError) { this.transportError = true; this.logger.log('transport error occurred, deleting INVITE server transaction ' + this.id); if (this.resendProvisionalTimer !== null) { SIP.Timers.clearInterval(this.resendProvisionalTimer); this.resendProvisionalTimer = null; } SIP.Timers.clearTimeout(this.L); SIP.Timers.clearTimeout(this.H); SIP.Timers.clearTimeout(this.I); this.stateChanged(C.STATUS_TERMINATED); this.ua.destroyTransaction(this); } }; InviteServerTransaction.prototype.resend_provisional = function() { if(!this.transport.send(this.last_response)) { this.onTransportError(); } }; // INVITE Server Transaction RFC 3261 17.2.1 InviteServerTransaction.prototype.receiveResponse = function(status_code, response) { var tr = this; var deferred = SIP.Utils.defer(); if(status_code >= 100 && status_code <= 199) { switch(this.state) { case C.STATUS_PROCEEDING: if(!this.transport.send(response)) { this.onTransportError(); } this.last_response = response; break; } } if(status_code > 100 && status_code <= 199 && this.state === C.STATUS_PROCEEDING) { // Trigger the resendProvisionalTimer only for the first non 100 provisional response. if(this.resendProvisionalTimer === null) { this.resendProvisionalTimer = SIP.Timers.setInterval(tr.resend_provisional.bind(tr), SIP.Timers.PROVISIONAL_RESPONSE_INTERVAL); } } else if(status_code >= 200 && status_code <= 299) { switch(this.state) { case C.STATUS_PROCEEDING: this.stateChanged(C.STATUS_ACCEPTED); this.last_response = response; this.L = SIP.Timers.setTimeout(tr.timer_L.bind(tr), SIP.Timers.TIMER_L); if (this.resendProvisionalTimer !== null) { SIP.Timers.clearInterval(this.resendProvisionalTimer); this.resendProvisionalTimer = null; } /* falls through */ case C.STATUS_ACCEPTED: // Note that this point will be reached for proceeding tr.state also. if(!this.transport.send(response)) { this.onTransportError(); deferred.reject(); } else { deferred.resolve(); } break; } } else if(status_code >= 300 && status_code <= 699) { switch(this.state) { case C.STATUS_PROCEEDING: if (this.resendProvisionalTimer !== null) { SIP.Timers.clearInterval(this.resendProvisionalTimer); this.resendProvisionalTimer = null; } if(!this.transport.send(response)) { this.onTransportError(); deferred.reject(); } else { this.stateChanged(C.STATUS_COMPLETED); this.H = SIP.Timers.setTimeout(tr.timer_H.bind(tr), SIP.Timers.TIMER_H); deferred.resolve(); } break; } } return deferred.promise; }; /** * @function * @param {SIP.UA} ua * @param {SIP.IncomingRequest} request * * @return {boolean} * INVITE: * _true_ if retransmission * _false_ new request * * ACK: * _true_ ACK to non2xx response * _false_ ACK must be passed to TU (accepted state) * ACK to 2xx response * * CANCEL: * _true_ no matching invite transaction * _false_ matching invite transaction and no final response sent * * OTHER: * _true_ retransmission * _false_ new request */ var checkTransaction = function(ua, request) { var tr; switch(request.method) { case SIP.C.INVITE: tr = ua.transactions.ist[request.via_branch]; if(tr) { switch(tr.state) { case C.STATUS_PROCEEDING: tr.transport.send(tr.last_response); break; // RFC 6026 7.1 Invite retransmission //received while in C.STATUS_ACCEPTED state. Absorb it. case C.STATUS_ACCEPTED: break; } return true; } break; case SIP.C.ACK: tr = ua.transactions.ist[request.via_branch]; // RFC 6026 7.1 if(tr) { if(tr.state === C.STATUS_ACCEPTED) { return false; } else if(tr.state === C.STATUS_COMPLETED) { tr.stateChanged(C.STATUS_CONFIRMED); tr.I = SIP.Timers.setTimeout(tr.timer_I.bind(tr), SIP.Timers.TIMER_I); return true; } } // ACK to 2XX Response. else { return false; } break; case SIP.C.CANCEL: tr = ua.transactions.ist[request.via_branch]; if(tr) { request.reply_sl(200); if(tr.state === C.STATUS_PROCEEDING) { return false; } else { return true; } } else { request.reply_sl(481); return true; } break; default: // Non-INVITE Server Transaction RFC 3261 17.2.2 tr = ua.transactions.nist[request.via_branch]; if(tr) { switch(tr.state) { case C.STATUS_TRYING: break; case C.STATUS_PROCEEDING: case C.STATUS_COMPLETED: tr.transport.send(tr.last_response); break; } return true; } break; } }; SIP.Transactions = { C: C, checkTransaction: checkTransaction, NonInviteClientTransaction: NonInviteClientTransaction, InviteClientTransaction: InviteClientTransaction, AckClientTransaction: AckClientTransaction, NonInviteServerTransaction: NonInviteServerTransaction, InviteServerTransaction: InviteServerTransaction }; }; },{}],28:[function(require,module,exports){ "use strict"; /** * @fileoverview Transport */ /** * @augments SIP * @class Transport * @param {SIP.UA} ua * @param {Object} server ws_server Object */ module.exports = function (SIP, WebSocket) { var Transport, C = { // Transport status codes STATUS_READY: 0, STATUS_DISCONNECTED: 1, STATUS_ERROR: 2 }; /** * Compute an amount of time in seconds to wait before sending another * keep-alive. * @returns {Number} */ function computeKeepAliveTimeout(upperBound) { var lowerBound = upperBound * 0.8; return 1000 * (Math.random() * (upperBound - lowerBound) + lowerBound); } Transport = function(ua, server) { this.logger = ua.getLogger('sip.transport'); this.ua = ua; this.ws = null; this.server = server; this.reconnection_attempts = 0; this.closed = false; this.connected = false; this.reconnectTimer = null; this.lastTransportError = {}; this.keepAliveInterval = ua.configuration.keepAliveInterval; this.keepAliveTimeout = null; this.keepAliveTimer = null; this.ua.transport = this; // Connect this.connect(); }; Transport.prototype = { /** * Send a message. * @param {SIP.OutgoingRequest|String} msg * @returns {Boolean} */ send: function(msg) { var message = msg.toString(); if(this.ws && this.ws.readyState === WebSocket.OPEN) { if (this.ua.configuration.traceSip === true) { this.logger.log('sending WebSocket message:\n\n' + message + '\n'); } this.ws.send(message); return true; } else { this.logger.warn('unable to send message, WebSocket is not open'); return false; } }, /** * Send a keep-alive (a double-CRLF sequence). * @private * @returns {Boolean} */ sendKeepAlive: function() { if(this.keepAliveTimeout) { return; } this.keepAliveTimeout = SIP.Timers.setTimeout(function() { this.ua.emit('keepAliveTimeout'); }.bind(this), 10000); return this.send('\r\n\r\n'); }, /** * Start sending keep-alives. * @private */ startSendingKeepAlives: function() { if (this.keepAliveInterval && !this.keepAliveTimer) { this.keepAliveTimer = SIP.Timers.setTimeout(function() { this.sendKeepAlive(); this.keepAliveTimer = null; this.startSendingKeepAlives(); }.bind(this), computeKeepAliveTimeout(this.keepAliveInterval)); } }, /** * Stop sending keep-alives. * @private */ stopSendingKeepAlives: function() { SIP.Timers.clearTimeout(this.keepAliveTimer); SIP.Timers.clearTimeout(this.keepAliveTimeout); this.keepAliveTimer = null; this.keepAliveTimeout = null; }, /** * Disconnect socket. */ disconnect: function() { if(this.ws) { // Clear reconnectTimer SIP.Timers.clearTimeout(this.reconnectTimer); this.stopSendingKeepAlives(); this.closed = true; this.logger.log('closing WebSocket ' + this.server.ws_uri); this.ws.close(); this.ws = null; } if (this.reconnectTimer !== null) { SIP.Timers.clearTimeout(this.reconnectTimer); this.reconnectTimer = null; this.ua.emit('disconnected', { transport: this, code: this.lastTransportError.code, reason: this.lastTransportError.reason }); } }, /** * Connect socket. */ connect: function() { var transport = this; if(this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) { this.logger.log('WebSocket ' + this.server.ws_uri + ' is already connected'); return false; } if(this.ws) { this.ws.close(); this.ws = null; } this.logger.log('connecting to WebSocket ' + this.server.ws_uri); this.ua.onTransportConnecting(this, (this.reconnection_attempts === 0)?1:this.reconnection_attempts); try { this.ws = new WebSocket(this.server.ws_uri, 'sip'); } catch(e) { this.logger.warn('error connecting to WebSocket ' + this.server.ws_uri + ': ' + e); } this.ws.binaryType = 'arraybuffer'; this.ws.onopen = function() { transport.onOpen(); }; this.ws.onclose = function(e) { transport.onClose(e); // Always cleanup. Eases GC, prevents potential memory leaks. this.onopen = null; this.onclose = null; this.onmessage = null; this.onerror = null; }; this.ws.onmessage = function(e) { transport.onMessage(e); }; this.ws.onerror = function(e) { transport.onError(e); }; }, // Transport Event Handlers /** * @event * @param {event} e */ onOpen: function() { this.connected = true; this.logger.log('WebSocket ' + this.server.ws_uri + ' connected'); // Clear reconnectTimer since we are not disconnected if (this.reconnectTimer !== null) { SIP.Timers.clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } // Reset reconnection_attempts this.reconnection_attempts = 0; // Disable closed this.closed = false; // Trigger onTransportConnected callback this.ua.onTransportConnected(this); // Start sending keep-alives this.startSendingKeepAlives(); }, /** * @event * @param {event} e */ onClose: function(e) { var connected_before = this.connected; this.lastTransportError.code = e.code; this.lastTransportError.reason = e.reason; this.stopSendingKeepAlives(); if (this.reconnection_attempts > 0) { this.logger.log('Reconnection attempt ' + this.reconnection_attempts + ' failed (code: ' + e.code + (e.reason? '| reason: ' + e.reason : '') +')'); this.reconnect(); } else { this.connected = false; this.logger.log('WebSocket disconnected (code: ' + e.code + (e.reason? '| reason: ' + e.reason : '') +')'); if(e.wasClean === false) { this.logger.warn('WebSocket abrupt disconnection'); } // Transport was connected if(connected_before === true) { this.ua.onTransportClosed(this); // Check whether the user requested to close. if(!this.closed) { this.reconnect(); } else { this.ua.emit('disconnected', { transport: this, code: this.lastTransportError.code, reason: this.lastTransportError.reason }); } } else { // This is the first connection attempt //Network error this.ua.onTransportError(this); } } }, /** * @event * @param {event} e */ onMessage: function(e) { var message, transaction, data = e.data; // CRLF Keep Alive response from server. Ignore it. if(data === '\r\n') { SIP.Timers.clearTimeout(this.keepAliveTimeout); this.keepAliveTimeout = null; if (this.ua.configuration.traceSip === true) { this.logger.log('received WebSocket message with CRLF Keep Alive response'); } return; } // WebSocket binary message. else if (typeof data !== 'string') { try { data = String.fromCharCode.apply(null, new Uint8Array(data)); } catch(evt) { this.logger.warn('received WebSocket binary message failed to be converted into string, message discarded'); return; } if (this.ua.configuration.traceSip === true) { this.logger.log('received WebSocket binary message:\n\n' + data + '\n'); } } // WebSocket text message. else { if (this.ua.configuration.traceSip === true) { this.logger.log('received WebSocket text message:\n\n' + data + '\n'); } } message = SIP.Parser.parseMessage(data, this.ua); if (!message) { return; } if(this.ua.status === SIP.UA.C.STATUS_USER_CLOSED && message instanceof SIP.IncomingRequest) { return; } // Do some sanity check if(SIP.sanityCheck(message, this.ua, this)) { if(message instanceof SIP.IncomingRequest) { message.transport = this; this.ua.receiveRequest(message); } else if(message instanceof SIP.IncomingResponse) { /* Unike stated in 18.1.2, if a response does not match * any transaction, it is discarded here and no passed to the core * in order to be discarded there. */ switch(message.method) { case SIP.C.INVITE: transaction = this.ua.transactions.ict[message.via_branch]; if(transaction) { transaction.receiveResponse(message); } break; case SIP.C.ACK: // Just in case ;-) break; default: transaction = this.ua.transactions.nict[message.via_branch]; if(transaction) { transaction.receiveResponse(message); } break; } } } }, /** * @event * @param {event} e */ onError: function(e) { this.logger.warn('WebSocket connection error: ' + JSON.stringify(e)); }, /** * Reconnection attempt logic. * @private */ reconnect: function() { var transport = this; this.reconnection_attempts += 1; if(this.reconnection_attempts > this.ua.configuration.wsServerMaxReconnection) { this.logger.warn('maximum reconnection attempts for WebSocket ' + this.server.ws_uri); this.ua.onTransportError(this); } else if (this.reconnection_attempts === 1) { this.logger.log('Connection to WebSocket ' + this.server.ws_uri + ' severed, attempting first reconnect'); transport.connect(); } else { this.logger.log('trying to reconnect to WebSocket ' + this.server.ws_uri + ' (reconnection attempt ' + this.reconnection_attempts + ')'); this.reconnectTimer = SIP.Timers.setTimeout(function() { transport.connect(); transport.reconnectTimer = null; }, this.ua.configuration.wsServerReconnectionTimeout * 1000); } } }; Transport.C = C; return Transport; }; },{}],29:[function(require,module,exports){ (function (global){ "use strict"; /** * @augments SIP * @class Class creating a SIP User Agent. * @param {function returning SIP.MediaHandler} [configuration.mediaHandlerFactory] * A function will be invoked by each of the UA's Sessions to build the MediaHandler for that Session. * If no (or a falsy) value is provided, each Session will use a default (WebRTC) MediaHandler. * * @param {Object} [configuration.media] gets passed to SIP.MediaHandler.getDescription as mediaHint */ module.exports = function (SIP, environment) { var UA, C = { // UA status codes STATUS_INIT: 0, STATUS_STARTING: 1, STATUS_READY: 2, STATUS_USER_CLOSED: 3, STATUS_NOT_READY: 4, // UA error codes CONFIGURATION_ERROR: 1, NETWORK_ERROR: 2, ALLOWED_METHODS: [ 'ACK', 'CANCEL', 'INVITE', 'MESSAGE', 'BYE', 'OPTIONS', 'INFO', 'NOTIFY', 'REFER' ], ACCEPTED_BODY_TYPES: [ 'application/sdp', 'application/dtmf-relay' ], MAX_FORWARDS: 70, TAG_LENGTH: 10 }; UA = function(configuration) { var self = this; // Helper function for forwarding events function selfEmit(type) { //registrationFailed handler is invoked with two arguments. Allow event handlers to be invoked with a variable no. of arguments return self.emit.bind(self, type); } // Set Accepted Body Types C.ACCEPTED_BODY_TYPES = C.ACCEPTED_BODY_TYPES.toString(); this.log = new SIP.LoggerFactory(); this.logger = this.getLogger('sip.ua'); this.cache = { credentials: {} }; this.configuration = {}; this.dialogs = {}; //User actions outside any session/dialog (MESSAGE) this.applicants = {}; this.data = {}; this.sessions = {}; this.subscriptions = {}; this.earlySubscriptions = {}; this.transport = null; this.contact = null; this.status = C.STATUS_INIT; this.error = null; this.transactions = { nist: {}, nict: {}, ist: {}, ict: {} }; this.transportRecoverAttempts = 0; this.transportRecoveryTimer = null; Object.defineProperties(this, { transactionsCount: { get: function() { var type, transactions = ['nist','nict','ist','ict'], count = 0; for (type in transactions) { count += Object.keys(this.transactions[transactions[type]]).length; } return count; } }, nictTransactionsCount: { get: function() { return Object.keys(this.transactions['nict']).length; } }, nistTransactionsCount: { get: function() { return Object.keys(this.transactions['nist']).length; } }, ictTransactionsCount: { get: function() { return Object.keys(this.transactions['ict']).length; } }, istTransactionsCount: { get: function() { return Object.keys(this.transactions['ist']).length; } } }); /** * Load configuration * * @throws {SIP.Exceptions.ConfigurationError} * @throws {TypeError} */ if(configuration === undefined) { configuration = {}; } else if (typeof configuration === 'string' || configuration instanceof String) { configuration = { uri: configuration }; } // Apply log configuration if present if (configuration.log) { if (configuration.log.hasOwnProperty('builtinEnabled')) { this.log.builtinEnabled = configuration.log.builtinEnabled; } if (configuration.log.hasOwnProperty('level')) { this.log.level = configuration.log.level; } if (configuration.log.hasOwnProperty('connector')) { this.log.connector = configuration.log.connector; } } try { this.loadConfig(configuration); } catch(e) { this.status = C.STATUS_NOT_READY; this.error = C.CONFIGURATION_ERROR; throw e; } // Initialize registerContext this.registerContext = new SIP.RegisterContext(this); this.registerContext.on('failed', selfEmit('registrationFailed')); this.registerContext.on('registered', selfEmit('registered')); this.registerContext.on('unregistered', selfEmit('unregistered')); if(this.configuration.autostart) { this.start(); } }; UA.prototype = Object.create(SIP.EventEmitter.prototype); //================= // High Level API //================= UA.prototype.register = function(options) { this.configuration.register = true; this.registerContext.register(options); return this; }; /** * Unregister. * * @param {Boolean} [all] unregister all user bindings. * */ UA.prototype.unregister = function(options) { this.configuration.register = false; var context = this.registerContext; this.afterConnected(context.unregister.bind(context, options)); return this; }; UA.prototype.isRegistered = function() { return this.registerContext.registered; }; /** * Connection state. * @param {Boolean} */ UA.prototype.isConnected = function() { return this.transport ? this.transport.connected : false; }; UA.prototype.afterConnected = function afterConnected (callback) { if (this.isConnected()) { callback(); } else { this.once('connected', callback); } }; /** * Make an outgoing call. * * @param {String} target * @param {Object} views * @param {Object} [options.media] gets passed to SIP.MediaHandler.getDescription as mediaHint * * @throws {TypeError} * */ UA.prototype.invite = function(target, options) { var context = new SIP.InviteClientContext(this, target, options); this.afterConnected(context.invite.bind(context)); this.emit('inviteSent', context); return context; }; UA.prototype.subscribe = function(target, event, options) { var sub = new SIP.Subscription(this, target, event, options); this.afterConnected(sub.subscribe.bind(sub)); return sub; }; /** * Send a message. * * @param {String} target * @param {String} body * @param {Object} [options] * * @throws {TypeError} * */ UA.prototype.message = function(target, body, options) { if (body === undefined) { throw new TypeError('Not enough arguments'); } // There is no Message module, so it is okay that the UA handles defaults here. options = Object.create(options || Object.prototype); options.contentType || (options.contentType = 'text/plain'); options.body = body; return this.request(SIP.C.MESSAGE, target, options); }; UA.prototype.request = function (method, target, options) { var req = new SIP.ClientContext(this, method, target, options); this.afterConnected(req.send.bind(req)); return req; }; /** * Gracefully close. * */ UA.prototype.stop = function() { var session, subscription, applicant, ua = this; function transactionsListener() { if (ua.nistTransactionsCount === 0 && ua.nictTransactionsCount === 0) { ua.removeListener('transactionDestroyed', transactionsListener); ua.transport.disconnect(); } } this.logger.log('user requested closure...'); if(this.status === C.STATUS_USER_CLOSED) { this.logger.warn('UA already closed'); return this; } // Clear transportRecoveryTimer SIP.Timers.clearTimeout(this.transportRecoveryTimer); // Close registerContext this.logger.log('closing registerContext'); this.registerContext.close(); // Run _terminate_ on every Session for(session in this.sessions) { this.logger.log('closing session ' + session); this.sessions[session].terminate(); } //Run _close_ on every confirmed Subscription for(subscription in this.subscriptions) { this.logger.log('unsubscribing from subscription ' + subscription); this.subscriptions[subscription].close(); } //Run _close_ on every early Subscription for(subscription in this.earlySubscriptions) { this.logger.log('unsubscribing from early subscription ' + subscription); this.earlySubscriptions[subscription].close(); } // Run _close_ on every applicant for(applicant in this.applicants) { this.applicants[applicant].close(); } this.status = C.STATUS_USER_CLOSED; /* * If the remaining transactions are all INVITE transactions, there is no need to * wait anymore because every session has already been closed by this method. * - locally originated sessions where terminated (CANCEL or BYE) * - remotely originated sessions where rejected (4XX) or terminated (BYE) * Remaining INVITE transactions belong tho sessions that where answered. This are in * 'accepted' state due to timers 'L' and 'M' defined in [RFC 6026] */ if (this.nistTransactionsCount === 0 && this.nictTransactionsCount === 0) { this.transport.disconnect(); } else { this.on('transactionDestroyed', transactionsListener); } if (typeof environment.removeEventListener === 'function') { // Google Chrome Packaged Apps don't allow 'unload' listeners: // unload is not available in packaged apps if (!(global.chrome && global.chrome.app && global.chrome.app.runtime)) { environment.removeEventListener('unload', this.environListener); } } return this; }; /** * Connect to the WS server if status = STATUS_INIT. * Resume UA after being closed. * */ UA.prototype.start = function() { var server; this.logger.log('user requested startup...'); if (this.status === C.STATUS_INIT) { server = this.getNextWsServer(); this.status = C.STATUS_STARTING; new SIP.Transport(this, server); } else if(this.status === C.STATUS_USER_CLOSED) { this.logger.log('resuming'); this.status = C.STATUS_READY; this.transport.connect(); } else if (this.status === C.STATUS_STARTING) { this.logger.log('UA is in STARTING status, not opening new connection'); } else if (this.status === C.STATUS_READY) { this.logger.log('UA is in READY status, not resuming'); } else { this.logger.error('Connection is down. Auto-Recovery system is trying to connect'); } if (this.configuration.autostop && typeof environment.addEventListener === 'function') { // Google Chrome Packaged Apps don't allow 'unload' listeners: // unload is not available in packaged apps if (!(global.chrome && global.chrome.app && global.chrome.app.runtime)) { this.environListener = this.stop.bind(this); environment.addEventListener('unload', this.environListener); } } return this; }; /** * Normalize a string into a valid SIP request URI * * @param {String} target * * @returns {SIP.URI|undefined} */ UA.prototype.normalizeTarget = function(target) { return SIP.Utils.normalizeTarget(target, this.configuration.hostportParams); }; //=============================== // Private (For internal use) //=============================== UA.prototype.saveCredentials = function(credentials) { this.cache.credentials[credentials.realm] = this.cache.credentials[credentials.realm] || {}; this.cache.credentials[credentials.realm][credentials.uri] = credentials; return this; }; UA.prototype.getCredentials = function(request) { var realm, credentials; realm = request.ruri.host; if (this.cache.credentials[realm] && this.cache.credentials[realm][request.ruri]) { credentials = this.cache.credentials[realm][request.ruri]; credentials.method = request.method; } return credentials; }; UA.prototype.getLogger = function(category, label) { return this.log.getLogger(category, label); }; //============================== // Event Handlers //============================== /** * Transport Close event * @private * @event * @param {SIP.Transport} transport. */ UA.prototype.onTransportClosed = function(transport) { // Run _onTransportError_ callback on every client transaction using _transport_ var type, idx, length, client_transactions = ['nict', 'ict', 'nist', 'ist']; transport.server.status = SIP.Transport.C.STATUS_DISCONNECTED; this.logger.log('connection state set to '+ SIP.Transport.C.STATUS_DISCONNECTED); length = client_transactions.length; for (type = 0; type < length; type++) { for(idx in this.transactions[client_transactions[type]]) { this.transactions[client_transactions[type]][idx].onTransportError(); } } // Close sessions if GRUU is not being used if (!this.contact.pub_gruu) { this.closeSessionsOnTransportError(); } }; /** * Unrecoverable transport event. * Connection reattempt logic has been done and didn't success. * @private * @event * @param {SIP.Transport} transport. */ UA.prototype.onTransportError = function(transport) { var server; this.logger.log('transport ' + transport.server.ws_uri + ' failed | connection state set to '+ SIP.Transport.C.STATUS_ERROR); // Close sessions. //Mark this transport as 'down' transport.server.status = SIP.Transport.C.STATUS_ERROR; this.emit('disconnected', { transport: transport }); // try the next transport if the UA isn't closed if(this.status === C.STATUS_USER_CLOSED) { return; } server = this.getNextWsServer(); if(server) { new SIP.Transport(this, server); }else { this.closeSessionsOnTransportError(); if (!this.error || this.error !== C.NETWORK_ERROR) { this.status = C.STATUS_NOT_READY; this.error = C.NETWORK_ERROR; } // Transport Recovery process this.recoverTransport(); } }; /** * Transport connection event. * @private * @event * @param {SIP.Transport} transport. */ UA.prototype.onTransportConnected = function(transport) { this.transport = transport; // Reset transport recovery counter this.transportRecoverAttempts = 0; transport.server.status = SIP.Transport.C.STATUS_READY; this.logger.log('connection state set to '+ SIP.Transport.C.STATUS_READY); if(this.status === C.STATUS_USER_CLOSED) { return; } this.status = C.STATUS_READY; this.error = null; if(this.configuration.register) { this.configuration.authenticationFactory.initialize().then(function () { this.registerContext.onTransportConnected(); }.bind(this)); } this.emit('connected', { transport: transport }); }; /** * Transport connecting event * @private * @param {SIP.Transport} transport. * #param {Integer} attempts. */ UA.prototype.onTransportConnecting = function(transport, attempts) { this.emit('connecting', { transport: transport, attempts: attempts }); }; /** * new Transaction * @private * @param {SIP.Transaction} transaction. */ UA.prototype.newTransaction = function(transaction) { this.transactions[transaction.type][transaction.id] = transaction; this.emit('newTransaction', {transaction: transaction}); }; /** * destroy Transaction * @private * @param {SIP.Transaction} transaction. */ UA.prototype.destroyTransaction = function(transaction) { delete this.transactions[transaction.type][transaction.id]; this.emit('transactionDestroyed', { transaction: transaction }); }; //========================= // receiveRequest //========================= /** * Request reception * @private * @param {SIP.IncomingRequest} request. */ UA.prototype.receiveRequest = function(request) { var dialog, session, message, earlySubscription, method = request.method, transaction, replaces, replacedDialog, self = this; function ruriMatches (uri) { return uri && uri.user === request.ruri.user; } // Check that request URI points to us if(!(ruriMatches(this.configuration.uri) || ruriMatches(this.contact.uri) || ruriMatches(this.contact.pub_gruu) || ruriMatches(this.contact.temp_gruu))) { this.logger.warn('Request-URI does not point to us'); if (request.method !== SIP.C.ACK) { request.reply_sl(404); } return; } // Check request URI scheme if(request.ruri.scheme === SIP.C.SIPS) { request.reply_sl(416); return; } // Check transaction if(SIP.Transactions.checkTransaction(this, request)) { return; } /* RFC3261 12.2.2 * Requests that do not change in any way the state of a dialog may be * received within a dialog (for example, an OPTIONS request). * They are processed as if they had been received outside the dialog. */ if(method === SIP.C.OPTIONS) { new SIP.Transactions.NonInviteServerTransaction(request, this); request.reply(200, null, [ 'Allow: '+ SIP.UA.C.ALLOWED_METHODS.toString(), 'Accept: '+ C.ACCEPTED_BODY_TYPES ]); } else if (method === SIP.C.MESSAGE) { message = new SIP.ServerContext(this, request); message.body = request.body; message.content_type = request.getHeader('Content-Type') || 'text/plain'; request.reply(200, null); this.emit('message', message); } else if (method !== SIP.C.INVITE && method !== SIP.C.ACK) { // Let those methods pass through to normal processing for now. transaction = new SIP.ServerContext(this, request); } // Initial Request if(!request.to_tag) { switch(method) { case SIP.C.INVITE: replaces = this.configuration.replaces !== SIP.C.supported.UNSUPPORTED && request.parseHeader('replaces'); if (replaces) { replacedDialog = this.dialogs[replaces.call_id + replaces.replaces_to_tag + replaces.replaces_from_tag]; if (!replacedDialog) { //Replaced header without a matching dialog, reject request.reply_sl(481, null); return; } else if (replacedDialog.owner.status === SIP.Session.C.STATUS_TERMINATED) { request.reply_sl(603, null); return; } else if (replacedDialog.state === SIP.Dialog.C.STATUS_CONFIRMED && replaces.early_only) { request.reply_sl(486, null); return; } } var isMediaSupported = this.configuration.mediaHandlerFactory.isSupported; if(!isMediaSupported || isMediaSupported()) { session = new SIP.InviteServerContext(this, request); session.replacee = replacedDialog && replacedDialog.owner; session.on('invite', function() { self.emit('invite', this); }); } else { this.logger.warn('INVITE received but WebRTC is not supported'); request.reply(488); } break; case SIP.C.BYE: // Out of dialog BYE received request.reply(481); break; case SIP.C.CANCEL: session = this.findSession(request); if(session) { session.receiveRequest(request); } else { this.logger.warn('received CANCEL request for a non existent session'); } break; case SIP.C.ACK: /* Absorb it. * ACK request without a corresponding Invite Transaction * and without To tag. */ break; case SIP.C.NOTIFY: if (this.configuration.allowLegacyNotifications && this.listeners('notify').length > 0) { request.reply(200, null); self.emit('notify', {request: request}); } else { request.reply(481, 'Subscription does not exist'); } break; default: request.reply(405); break; } } // In-dialog request else { dialog = this.findDialog(request); if(dialog) { if (method === SIP.C.INVITE) { new SIP.Transactions.InviteServerTransaction(request, this); } dialog.receiveRequest(request); } else if (method === SIP.C.NOTIFY) { session = this.findSession(request); earlySubscription = this.findEarlySubscription(request); if(session) { session.receiveRequest(request); } else if(earlySubscription) { earlySubscription.receiveRequest(request); } else { this.logger.warn('received NOTIFY request for a non existent session or subscription'); request.reply(481, 'Subscription does not exist'); } } /* RFC3261 12.2.2 * Request with to tag, but no matching dialog found. * Exception: ACK for an Invite request for which a dialog has not * been created. */ else { if(method !== SIP.C.ACK) { request.reply(481); } } } }; //================= // Utils //================= /** * Get the session to which the request belongs to, if any. * @private * @param {SIP.IncomingRequest} request. * @returns {SIP.OutgoingSession|SIP.IncomingSession|null} */ UA.prototype.findSession = function(request) { return this.sessions[request.call_id + request.from_tag] || this.sessions[request.call_id + request.to_tag] || null; }; /** * Get the dialog to which the request belongs to, if any. * @private * @param {SIP.IncomingRequest} * @returns {SIP.Dialog|null} */ UA.prototype.findDialog = function(request) { return this.dialogs[request.call_id + request.from_tag + request.to_tag] || this.dialogs[request.call_id + request.to_tag + request.from_tag] || null; }; /** * Get the subscription which has not been confirmed to which the request belongs to, if any * @private * @param {SIP.IncomingRequest} * @returns {SIP.Subscription|null} */ UA.prototype.findEarlySubscription = function(request) { return this.earlySubscriptions[request.call_id + request.to_tag + request.getHeader('event')] || null; }; /** * Retrieve the next server to which connect. * @private * @returns {Object} ws_server */ UA.prototype.getNextWsServer = function() { // Order servers by weight var idx, length, ws_server, candidates = []; length = this.configuration.wsServers.length; for (idx = 0; idx < length; idx++) { ws_server = this.configuration.wsServers[idx]; if (ws_server.status === SIP.Transport.C.STATUS_ERROR) { continue; } else if (candidates.length === 0) { candidates.push(ws_server); } else if (ws_server.weight > candidates[0].weight) { candidates = [ws_server]; } else if (ws_server.weight === candidates[0].weight) { candidates.push(ws_server); } } idx = Math.floor(Math.random() * candidates.length); return candidates[idx]; }; /** * Close all sessions on transport error. * @private */ UA.prototype.closeSessionsOnTransportError = function() { var idx; // Run _transportError_ for every Session for(idx in this.sessions) { this.sessions[idx].onTransportError(); } // Call registerContext _onTransportClosed_ this.registerContext.onTransportClosed(); }; UA.prototype.recoverTransport = function(ua) { var idx, length, k, nextRetry, count, server; ua = ua || this; count = ua.transportRecoverAttempts; length = ua.configuration.wsServers.length; for (idx = 0; idx < length; idx++) { ua.configuration.wsServers[idx].status = 0; } server = ua.getNextWsServer(); k = Math.floor((Math.random() * Math.pow(2,count)) +1); nextRetry = k * ua.configuration.connectionRecoveryMinInterval; if (nextRetry > ua.configuration.connectionRecoveryMaxInterval) { this.logger.log('time for next connection attempt exceeds connectionRecoveryMaxInterval, resetting counter'); nextRetry = ua.configuration.connectionRecoveryMinInterval; count = 0; } this.logger.log('next connection attempt in '+ nextRetry +' seconds'); this.transportRecoveryTimer = SIP.Timers.setTimeout( function(){ ua.transportRecoverAttempts = count + 1; new SIP.Transport(ua, server); }, nextRetry * 1000); }; function checkAuthenticationFactory (authenticationFactory) { if (!(authenticationFactory instanceof Function)) { return; } if (!authenticationFactory.initialize) { authenticationFactory.initialize = function initialize () { return SIP.Utils.Promise.resolve(); }; } return authenticationFactory; } /** * Configuration load. * @private * returns {Boolean} */ UA.prototype.loadConfig = function(configuration) { // Settings and default values var parameter, value, checked_value, hostportParams, registrarServer, settings = { /* Host address * Value to be set in Via sent_by and host part of Contact FQDN */ viaHost: SIP.Utils.createRandomToken(12) + '.invalid', uri: new SIP.URI('sip', 'anonymous.' + SIP.Utils.createRandomToken(6), 'anonymous.invalid', null, null), wsServers: [{ scheme: 'WSS', sip_uri: '', status: 0, weight: 0, ws_uri: 'wss://edge.sip.onsip.com' }], //Custom Configuration Settings custom: {}, //Display name displayName: '', // Password password: null, // Registration parameters registerExpires: 600, register: true, registrarServer: null, // Transport related parameters wsServerMaxReconnection: 3, wsServerReconnectionTimeout: 4, connectionRecoveryMinInterval: 2, connectionRecoveryMaxInterval: 30, keepAliveInterval: 0, extraSupported: [], usePreloadedRoute: false, //string to be inserted into User-Agent request header userAgentString: SIP.C.USER_AGENT, // Session parameters iceCheckingTimeout: 5000, noAnswerTimeout: 60, stunServers: ['stun:stun.l.google.com:19302'], turnServers: [], // Logging parameters traceSip: false, // Hacks hackViaTcp: false, hackIpInContact: false, hackWssInTransport: false, hackAllowUnregisteredOptionTags: false, hackCleanJitsiSdpImageattr: false, hackStripTcp: false, contactTransport: 'ws', forceRport: false, //autostarting autostart: true, autostop: true, //Reliable Provisional Responses rel100: SIP.C.supported.UNSUPPORTED, // Replaces header (RFC 3891) // http://tools.ietf.org/html/rfc3891 replaces: SIP.C.supported.UNSUPPORTED, mediaHandlerFactory: SIP.WebRTC.MediaHandler.defaultFactory, authenticationFactory: checkAuthenticationFactory(function authenticationFactory (ua) { return new SIP.DigestAuthentication(ua); }), allowLegacyNotifications: false }; // Pre-Configuration function aliasUnderscored (parameter, logger) { var underscored = parameter.replace(/([a-z][A-Z])/g, function (m) { return m[0] + '_' + m[1].toLowerCase(); }); if (parameter === underscored) { return; } var hasParameter = configuration.hasOwnProperty(parameter); if (configuration.hasOwnProperty(underscored)) { logger.warn(underscored + ' is deprecated, please use ' + parameter); if (hasParameter) { logger.warn(parameter + ' overriding ' + underscored); } } configuration[parameter] = hasParameter ? configuration[parameter] : configuration[underscored]; } var configCheck = this.getConfigurationCheck(); // Check Mandatory parameters for(parameter in configCheck.mandatory) { aliasUnderscored(parameter, this.logger); if(!configuration.hasOwnProperty(parameter)) { throw new SIP.Exceptions.ConfigurationError(parameter); } else { value = configuration[parameter]; checked_value = configCheck.mandatory[parameter](value); if (checked_value !== undefined) { settings[parameter] = checked_value; } else { throw new SIP.Exceptions.ConfigurationError(parameter, value); } } } SIP.Utils.optionsOverride(configuration, 'rel100', 'reliable', true, this.logger, SIP.C.supported.UNSUPPORTED); var emptyArraysAllowed = ['stunServers', 'turnServers']; // Check Optional parameters for(parameter in configCheck.optional) { aliasUnderscored(parameter, this.logger); if(configuration.hasOwnProperty(parameter)) { value = configuration[parameter]; // If the parameter value is an empty array, but shouldn't be, apply its default value. if (value instanceof Array && value.length === 0 && emptyArraysAllowed.indexOf(parameter) < 0) { continue; } // If the parameter value is null, empty string, or undefined then apply its default value. if(value === null || value === "" || value === undefined) { continue; } // If it's a number with NaN value then also apply its default value. // NOTE: JS does not allow "value === NaN", the following does the work: else if(typeof(value) === 'number' && isNaN(value)) { continue; } checked_value = configCheck.optional[parameter](value); if (checked_value !== undefined) { settings[parameter] = checked_value; } else { throw new SIP.Exceptions.ConfigurationError(parameter, value); } } } // Sanity Checks // Connection recovery intervals if(settings.connectionRecoveryMaxInterval < settings.connectionRecoveryMinInterval) { throw new SIP.Exceptions.ConfigurationError('connectionRecoveryMaxInterval', settings.connectionRecoveryMaxInterval); } // Post Configuration Process // Allow passing 0 number as displayName. if (settings.displayName === 0) { settings.displayName = '0'; } // Instance-id for GRUU if (!settings.instanceId) { settings.instanceId = SIP.Utils.newUUID(); } // sipjsId instance parameter. Static random tag of length 5 settings.sipjsId = SIP.Utils.createRandomToken(5); // String containing settings.uri without scheme and user. hostportParams = settings.uri.clone(); hostportParams.user = null; settings.hostportParams = hostportParams.toRaw().replace(/^sip:/i, ''); /* Check whether authorizationUser is explicitly defined. * Take 'settings.uri.user' value if not. */ if (!settings.authorizationUser) { settings.authorizationUser = settings.uri.user; } /* If no 'registrarServer' is set use the 'uri' value without user portion. */ if (!settings.registrarServer) { registrarServer = settings.uri.clone(); registrarServer.user = null; settings.registrarServer = registrarServer; } // User noAnswerTimeout settings.noAnswerTimeout = settings.noAnswerTimeout * 1000; // Via Host if (settings.hackIpInContact) { if (typeof settings.hackIpInContact === 'boolean') { settings.viaHost = SIP.Utils.getRandomTestNetIP(); } else if (typeof settings.hackIpInContact === 'string') { settings.viaHost = settings.hackIpInContact; } } // Contact transport parameter if (settings.hackWssInTransport) { settings.contactTransport = 'wss'; } this.contact = { pub_gruu: null, temp_gruu: null, uri: new SIP.URI('sip', SIP.Utils.createRandomToken(8), settings.viaHost, null, {transport: settings.contactTransport}), toString: function(options){ options = options || {}; var anonymous = options.anonymous || null, outbound = options.outbound || null, contact = '<'; if (anonymous) { contact += (this.temp_gruu || ('sip:anonymous@anonymous.invalid;transport='+settings.contactTransport)).toString(); } else { contact += (this.pub_gruu || this.uri).toString(); } if (outbound) { contact += ';ob'; } contact += '>'; return contact; } }; // media overrides mediaConstraints SIP.Utils.optionsOverride(settings, 'media', 'mediaConstraints', true, this.logger); var skeleton = {}; // Fill the value of the configuration_skeleton for(parameter in settings) { skeleton[parameter] = { value: settings[parameter], writable: (parameter === 'register' || parameter === 'custom'), configurable: false }; } Object.defineProperties(this.configuration, skeleton); this.logger.log('configuration parameters after validation:'); for(parameter in settings) { switch(parameter) { case 'uri': case 'registrarServer': case 'mediaHandlerFactory': this.logger.log('· ' + parameter + ': ' + settings[parameter]); break; case 'password': this.logger.log('· ' + parameter + ': ' + 'NOT SHOWN'); break; default: this.logger.log('· ' + parameter + ': ' + JSON.stringify(settings[parameter])); } } return; }; /** * Configuration checker. * @private * @return {Boolean} */ UA.prototype.getConfigurationCheck = function () { return { mandatory: { }, optional: { uri: function(uri) { var parsed; if (!(/^sip:/i).test(uri)) { uri = SIP.C.SIP + ':' + uri; } parsed = SIP.URI.parse(uri); if(!parsed) { return; } else if(!parsed.user) { return; } else { return parsed; } }, //Note: this function used to call 'this.logger.error' but calling 'this' with anything here is invalid wsServers: function(wsServers) { var idx, length, url; /* Allow defining wsServers parameter as: * String: "host" * Array of Strings: ["host1", "host2"] * Array of Objects: [{ws_uri:"host1", weight:1}, {ws_uri:"host2", weight:0}] * Array of Objects and Strings: [{ws_uri:"host1"}, "host2"] */ if (typeof wsServers === 'string') { wsServers = [{ws_uri: wsServers}]; } else if (wsServers instanceof Array) { length = wsServers.length; for (idx = 0; idx < length; idx++) { if (typeof wsServers[idx] === 'string'){ wsServers[idx] = {ws_uri: wsServers[idx]}; } } } else { return; } if (wsServers.length === 0) { return false; } length = wsServers.length; for (idx = 0; idx < length; idx++) { if (!wsServers[idx].ws_uri) { return; } if (wsServers[idx].weight && !Number(wsServers[idx].weight)) { return; } url = SIP.Grammar.parse(wsServers[idx].ws_uri, 'absoluteURI'); if(url === -1) { return; } else if(['wss', 'ws', 'udp'].indexOf(url.scheme) < 0) { return; } else { wsServers[idx].sip_uri = ''; if (!wsServers[idx].weight) { wsServers[idx].weight = 0; } wsServers[idx].status = 0; wsServers[idx].scheme = url.scheme.toUpperCase(); } } return wsServers; }, authorizationUser: function(authorizationUser) { if(SIP.Grammar.parse('"'+ authorizationUser +'"', 'quoted_string') === -1) { return; } else { return authorizationUser; } }, connectionRecoveryMaxInterval: function(connectionRecoveryMaxInterval) { var value; if(SIP.Utils.isDecimal(connectionRecoveryMaxInterval)) { value = Number(connectionRecoveryMaxInterval); if(value > 0) { return value; } } }, connectionRecoveryMinInterval: function(connectionRecoveryMinInterval) { var value; if(SIP.Utils.isDecimal(connectionRecoveryMinInterval)) { value = Number(connectionRecoveryMinInterval); if(value > 0) { return value; } } }, displayName: function(displayName) { if(SIP.Grammar.parse('"' + displayName + '"', 'displayName') === -1) { return; } else { return displayName; } }, hackViaTcp: function(hackViaTcp) { if (typeof hackViaTcp === 'boolean') { return hackViaTcp; } }, hackIpInContact: function(hackIpInContact) { if (typeof hackIpInContact === 'boolean') { return hackIpInContact; } else if (typeof hackIpInContact === 'string' && SIP.Grammar.parse(hackIpInContact, 'host') !== -1) { return hackIpInContact; } }, iceCheckingTimeout: function(iceCheckingTimeout) { if(SIP.Utils.isDecimal(iceCheckingTimeout)) { return Math.max(500, iceCheckingTimeout); } }, hackWssInTransport: function(hackWssInTransport) { if (typeof hackWssInTransport === 'boolean') { return hackWssInTransport; } }, hackAllowUnregisteredOptionTags: function(hackAllowUnregisteredOptionTags) { if (typeof hackAllowUnregisteredOptionTags === 'boolean') { return hackAllowUnregisteredOptionTags; } }, hackCleanJitsiSdpImageattr: function(hackCleanJitsiSdpImageattr) { if (typeof hackCleanJitsiSdpImageattr === 'boolean') { return hackCleanJitsiSdpImageattr; } }, hackStripTcp: function(hackStripTcp) { if (typeof hackStripTcp === 'boolean') { return hackStripTcp; } }, contactTransport: function(contactTransport) { if (typeof contactTransport === 'string') { return contactTransport; } }, forceRport: function(forceRport) { if (typeof forceRport === 'boolean') { return forceRport; } }, instanceId: function(instanceId) { if(typeof instanceId !== 'string') { return; } if ((/^uuid:/i.test(instanceId))) { instanceId = instanceId.substr(5); } if(SIP.Grammar.parse(instanceId, 'uuid') === -1) { return; } else { return instanceId; } }, keepAliveInterval: function(keepAliveInterval) { var value; if (SIP.Utils.isDecimal(keepAliveInterval)) { value = Number(keepAliveInterval); if (value > 0) { return value; } } }, extraSupported: function(optionTags) { var idx, length; if (!(optionTags instanceof Array)) { return; } length = optionTags.length; for (idx = 0; idx < length; idx++) { if (typeof optionTags[idx] !== 'string') { return; } } return optionTags; }, noAnswerTimeout: function(noAnswerTimeout) { var value; if (SIP.Utils.isDecimal(noAnswerTimeout)) { value = Number(noAnswerTimeout); if (value > 0) { return value; } } }, password: function(password) { return String(password); }, rel100: function(rel100) { if(rel100 === SIP.C.supported.REQUIRED) { return SIP.C.supported.REQUIRED; } else if (rel100 === SIP.C.supported.SUPPORTED) { return SIP.C.supported.SUPPORTED; } else { return SIP.C.supported.UNSUPPORTED; } }, replaces: function(replaces) { if(replaces === SIP.C.supported.REQUIRED) { return SIP.C.supported.REQUIRED; } else if (replaces === SIP.C.supported.SUPPORTED) { return SIP.C.supported.SUPPORTED; } else { return SIP.C.supported.UNSUPPORTED; } }, register: function(register) { if (typeof register === 'boolean') { return register; } }, registerExpires: function(registerExpires) { var value; if (SIP.Utils.isDecimal(registerExpires)) { value = Number(registerExpires); if (value > 0) { return value; } } }, registrarServer: function(registrarServer) { var parsed; if(typeof registrarServer !== 'string') { return; } if (!/^sip:/i.test(registrarServer)) { registrarServer = SIP.C.SIP + ':' + registrarServer; } parsed = SIP.URI.parse(registrarServer); if(!parsed) { return; } else if(parsed.user) { return; } else { return parsed; } }, stunServers: function(stunServers) { var idx, length, stun_server; if (typeof stunServers === 'string') { stunServers = [stunServers]; } else if (!(stunServers instanceof Array)) { return; } length = stunServers.length; for (idx = 0; idx < length; idx++) { stun_server = stunServers[idx]; if (!(/^stuns?:/.test(stun_server))) { stun_server = 'stun:' + stun_server; } if(SIP.Grammar.parse(stun_server, 'stun_URI') === -1) { return; } else { stunServers[idx] = stun_server; } } return stunServers; }, traceSip: function(traceSip) { if (typeof traceSip === 'boolean') { return traceSip; } }, turnServers: function(turnServers) { var idx, jdx, length, turn_server, num_turn_server_urls, url; if (turnServers instanceof Array) { // Do nothing } else { turnServers = [turnServers]; } length = turnServers.length; for (idx = 0; idx < length; idx++) { turn_server = turnServers[idx]; //Backwards compatibility: Allow defining the turn_server url with the 'server' property. if (turn_server.server) { turn_server.urls = [turn_server.server]; } if (!turn_server.urls) { return; } if (turn_server.urls instanceof Array) { num_turn_server_urls = turn_server.urls.length; } else { turn_server.urls = [turn_server.urls]; num_turn_server_urls = 1; } for (jdx = 0; jdx < num_turn_server_urls; jdx++) { url = turn_server.urls[jdx]; if (!(/^turns?:/.test(url))) { url = 'turn:' + url; } if(SIP.Grammar.parse(url, 'turn_URI') === -1) { return; } } } return turnServers; }, rtcpMuxPolicy: function(rtcpMuxPolicy) { if (typeof rtcpMuxPolicy === 'string') { return rtcpMuxPolicy; } }, userAgentString: function(userAgentString) { if (typeof userAgentString === 'string') { return userAgentString; } }, usePreloadedRoute: function(usePreloadedRoute) { if (typeof usePreloadedRoute === 'boolean') { return usePreloadedRoute; } }, wsServerMaxReconnection: function(wsServerMaxReconnection) { var value; if (SIP.Utils.isDecimal(wsServerMaxReconnection)) { value = Number(wsServerMaxReconnection); if (value > 0) { return value; } } }, wsServerReconnectionTimeout: function(wsServerReconnectionTimeout) { var value; if (SIP.Utils.isDecimal(wsServerReconnectionTimeout)) { value = Number(wsServerReconnectionTimeout); if (value > 0) { return value; } } }, autostart: function(autostart) { if (typeof autostart === 'boolean') { return autostart; } }, autostop: function(autostop) { if (typeof autostop === 'boolean') { return autostop; } }, mediaHandlerFactory: function(mediaHandlerFactory) { if (mediaHandlerFactory instanceof Function) { var promisifiedFactory = function promisifiedFactory () { var mediaHandler = mediaHandlerFactory.apply(this, arguments); function patchMethod (methodName) { var method = mediaHandler[methodName]; if (method.length > 1) { var callbacksFirst = methodName === 'getDescription'; mediaHandler[methodName] = SIP.Utils.promisify(mediaHandler, methodName, callbacksFirst); } } patchMethod('getDescription'); patchMethod('setDescription'); return mediaHandler; }; promisifiedFactory.isSupported = mediaHandlerFactory.isSupported; return promisifiedFactory; } }, authenticationFactory: checkAuthenticationFactory, allowLegacyNotifications: function(allowLegacyNotifications) { if (typeof allowLegacyNotifications === 'boolean') { return allowLegacyNotifications; } }, custom: function(custom) { if (typeof custom === 'object') { return custom; } } } }; }; UA.C = C; SIP.UA = UA; }; }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{}],30:[function(require,module,exports){ "use strict"; /** * @fileoverview SIP URI */ /** * @augments SIP * @class Class creating a SIP URI. * * @param {String} [scheme] * @param {String} [user] * @param {String} host * @param {String} [port] * @param {Object} [parameters] * @param {Object} [headers] * */ module.exports = function (SIP) { var URI; URI = function(scheme, user, host, port, parameters, headers) { var param, header, raw, normal; // Checks if(!host) { throw new TypeError('missing or invalid "host" parameter'); } // Initialize parameters scheme = scheme || SIP.C.SIP; this.parameters = {}; this.headers = {}; for (param in parameters) { this.setParam(param, parameters[param]); } for (header in headers) { this.setHeader(header, headers[header]); } // Raw URI raw = { scheme: scheme, user: user, host: host, port: port }; // Normalized URI normal = { scheme: scheme.toLowerCase(), user: user, host: host.toLowerCase(), port: port }; Object.defineProperties(this, { _normal: { get: function() { return normal; } }, _raw: { get: function() { return raw; } }, scheme: { get: function() { return normal.scheme; }, set: function(value) { raw.scheme = value; normal.scheme = value.toLowerCase(); } }, user: { get: function() { return normal.user; }, set: function(value) { normal.user = raw.user = value; } }, host: { get: function() { return normal.host; }, set: function(value) { raw.host = value; normal.host = value.toLowerCase(); } }, aor: { get: function() { return normal.user + '@' + normal.host; } }, port: { get: function() { return normal.port; }, set: function(value) { normal.port = raw.port = value === 0 ? value : (parseInt(value,10) || null); } } }); }; URI.prototype = { setParam: function(key, value) { if(key) { this.parameters[key.toLowerCase()] = (typeof value === 'undefined' || value === null) ? null : value.toString().toLowerCase(); } }, getParam: function(key) { if(key) { return this.parameters[key.toLowerCase()]; } }, hasParam: function(key) { if(key) { return (this.parameters.hasOwnProperty(key.toLowerCase()) && true) || false; } }, deleteParam: function(parameter) { var value; parameter = parameter.toLowerCase(); if (this.parameters.hasOwnProperty(parameter)) { value = this.parameters[parameter]; delete this.parameters[parameter]; return value; } }, clearParams: function() { this.parameters = {}; }, setHeader: function(name, value) { this.headers[SIP.Utils.headerize(name)] = (value instanceof Array) ? value : [value]; }, getHeader: function(name) { if(name) { return this.headers[SIP.Utils.headerize(name)]; } }, hasHeader: function(name) { if(name) { return (this.headers.hasOwnProperty(SIP.Utils.headerize(name)) && true) || false; } }, deleteHeader: function(header) { var value; header = SIP.Utils.headerize(header); if(this.headers.hasOwnProperty(header)) { value = this.headers[header]; delete this.headers[header]; return value; } }, clearHeaders: function() { this.headers = {}; }, clone: function() { return new URI( this._raw.scheme, this._raw.user, this._raw.host, this._raw.port, JSON.parse(JSON.stringify(this.parameters)), JSON.parse(JSON.stringify(this.headers))); }, toRaw: function() { return this._toString(this._raw); }, toString: function() { return this._toString(this._normal); }, _toString: function(uri) { var header, parameter, idx, uriString, headers = []; uriString = uri.scheme + ':'; // add slashes if it's not a sip(s) URI if (!uri.scheme.toLowerCase().match("^sips?$")) { uriString += "//"; } if (uri.user) { uriString += SIP.Utils.escapeUser(uri.user) + '@'; } uriString += uri.host; if (uri.port || uri.port === 0) { uriString += ':' + uri.port; } for (parameter in this.parameters) { uriString += ';' + parameter; if (this.parameters[parameter] !== null) { uriString += '='+ this.parameters[parameter]; } } for(header in this.headers) { for(idx in this.headers[header]) { headers.push(header + '=' + this.headers[header][idx]); } } if (headers.length > 0) { uriString += '?' + headers.join('&'); } return uriString; } }; /** * Parse the given string and returns a SIP.URI instance or undefined if * it is an invalid URI. * @public * @param {String} uri */ URI.parse = function(uri) { uri = SIP.Grammar.parse(uri,'SIP_URI'); if (uri !== -1) { return uri; } else { return undefined; } }; SIP.URI = URI; }; },{}],31:[function(require,module,exports){ "use strict"; /** * @fileoverview Utils */ module.exports = function (SIP, environment) { var Utils; Utils= { Promise: environment.Promise, defer: function defer () { var deferred = {}; deferred.promise = new Utils.Promise(function (resolve, reject) { deferred.resolve = resolve; deferred.reject = reject; }); return deferred; }, promisify: function promisify (object, methodName, callbacksFirst) { var oldMethod = object[methodName]; return function promisifiedMethod (arg, onSuccess, onFailure) { return new Utils.Promise(function (resolve, reject) { var oldArgs = [arg, resolve, reject]; if (callbacksFirst) { oldArgs = [resolve, reject, arg]; } oldMethod.apply(object, oldArgs); }).then(onSuccess, onFailure); }; }, augment: function (object, constructor, args, override) { var idx, proto; // Add public properties from constructor's prototype onto object proto = constructor.prototype; for (idx in proto) { if (override || object[idx] === undefined) { object[idx] = proto[idx]; } } // Construct the object as though it were just created by constructor constructor.apply(object, args); }, optionsOverride: function (options, winner, loser, isDeprecated, logger, defaultValue) { if (isDeprecated && options[loser]) { logger.warn(loser + ' is deprecated, please use ' + winner + ' instead'); } if (options[winner] && options[loser]) { logger.warn(winner + ' overriding ' + loser); } options[winner] = options[winner] || options[loser] || defaultValue; }, str_utf8_length: function(string) { return encodeURIComponent(string).replace(/%[A-F\d]{2}/g, 'U').length; }, generateFakeSDP: function(body) { if (!body) { return; } var start = body.indexOf('o='); var end = body.indexOf('\r\n', start); return 'v=0\r\n' + body.slice(start, end) + '\r\ns=-\r\nt=0 0\r\nc=IN IP4 0.0.0.0'; }, isFunction: function(fn) { if (fn !== undefined) { return Object.prototype.toString.call(fn) === '[object Function]'; } else { return false; } }, isDecimal: function (num) { return !isNaN(num) && (parseFloat(num) === parseInt(num,10)); }, createRandomToken: function(size, base) { var i, r, token = ''; base = base || 32; for( i=0; i < size; i++ ) { r = Math.random() * base|0; token += r.toString(base); } return token; }, newTag: function() { return SIP.Utils.createRandomToken(SIP.UA.C.TAG_LENGTH); }, // http://stackoverflow.com/users/109538/broofa newUUID: function() { var UUID = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { var r = Math.random()*16|0, v = c === 'x' ? r : (r&0x3|0x8); return v.toString(16); }); return UUID; }, hostType: function(host) { if (!host) { return; } else { host = SIP.Grammar.parse(host,'host'); if (host !== -1) { return host.host_type; } } }, /** * Normalize SIP URI. * NOTE: It does not allow a SIP URI without username. * Accepts 'sip', 'sips' and 'tel' URIs and convert them into 'sip'. * Detects the domain part (if given) and properly hex-escapes the user portion. * If the user portion has only 'tel' number symbols the user portion is clean of 'tel' visual separators. * @private * @param {String} target * @param {String} [domain] */ normalizeTarget: function(target, domain) { var uri, target_array, target_user, target_domain; // If no target is given then raise an error. if (!target) { return; // If a SIP.URI instance is given then return it. } else if (target instanceof SIP.URI) { return target; // If a string is given split it by '@': // - Last fragment is the desired domain. // - Otherwise append the given domain argument. } else if (typeof target === 'string') { target_array = target.split('@'); switch(target_array.length) { case 1: if (!domain) { return; } target_user = target; target_domain = domain; break; case 2: target_user = target_array[0]; target_domain = target_array[1]; break; default: target_user = target_array.slice(0, target_array.length-1).join('@'); target_domain = target_array[target_array.length-1]; } // Remove the URI scheme (if present). target_user = target_user.replace(/^(sips?|tel):/i, ''); // Remove 'tel' visual separators if the user portion just contains 'tel' number symbols. if (/^[\-\.\(\)]*\+?[0-9\-\.\(\)]+$/.test(target_user)) { target_user = target_user.replace(/[\-\.\(\)]/g, ''); } // Build the complete SIP URI. target = SIP.C.SIP + ':' + SIP.Utils.escapeUser(target_user) + '@' + target_domain; // Finally parse the resulting URI. if (uri = SIP.URI.parse(target)) { return uri; } else { return; } } else { return; } }, /** * Hex-escape a SIP URI user. * @private * @param {String} user */ escapeUser: function(user) { // Don't hex-escape ':' (%3A), '+' (%2B), '?' (%3F"), '/' (%2F). return encodeURIComponent(decodeURIComponent(user)).replace(/%3A/ig, ':').replace(/%2B/ig, '+').replace(/%3F/ig, '?').replace(/%2F/ig, '/'); }, headerize: function(string) { var exceptions = { 'Call-Id': 'Call-ID', 'Cseq': 'CSeq', 'Min-Se': 'Min-SE', 'Rack': 'RAck', 'Rseq': 'RSeq', 'Www-Authenticate': 'WWW-Authenticate' }, name = string.toLowerCase().replace(/_/g,'-').split('-'), hname = '', parts = name.length, part; for (part = 0; part < parts; part++) { if (part !== 0) { hname +='-'; } hname += name[part].charAt(0).toUpperCase()+name[part].substring(1); } if (exceptions[hname]) { hname = exceptions[hname]; } return hname; }, sipErrorCause: function(status_code) { var cause; for (cause in SIP.C.SIP_ERROR_CAUSES) { if (SIP.C.SIP_ERROR_CAUSES[cause].indexOf(status_code) !== -1) { return SIP.C.causes[cause]; } } return SIP.C.causes.SIP_FAILURE_CODE; }, getReasonPhrase: function getReasonPhrase (code, specific) { return specific || SIP.C.REASON_PHRASE[code] || ''; }, getReasonHeaderValue: function getReasonHeaderValue (code, reason) { reason = SIP.Utils.getReasonPhrase(code, reason); return 'SIP ;cause=' + code + ' ;text="' + reason + '"'; }, getCancelReason: function getCancelReason (code, reason) { if (code && code < 200 || code > 699) { throw new TypeError('Invalid status_code: ' + code); } else if (code) { return SIP.Utils.getReasonHeaderValue(code, reason); } }, buildStatusLine: function buildStatusLine (code, reason) { code = code || null; reason = reason || null; // Validate code and reason values if (!code || (code < 100 || code > 699)) { throw new TypeError('Invalid status_code: '+ code); } else if (reason && typeof reason !== 'string' && !(reason instanceof String)) { throw new TypeError('Invalid reason_phrase: '+ reason); } reason = Utils.getReasonPhrase(code, reason); return 'SIP/2.0 ' + code + ' ' + reason + '\r\n'; }, /** * Generate a random Test-Net IP (http://tools.ietf.org/html/rfc5735) * @private */ getRandomTestNetIP: function() { function getOctet(from,to) { return Math.floor(Math.random()*(to-from+1)+from); } return '192.0.2.' + getOctet(1, 254); }, // MD5 (Message-Digest Algorithm) http://www.webtoolkit.info calculateMD5: function(string) { function RotateLeft(lValue, iShiftBits) { return (lValue<>>(32-iShiftBits)); } function AddUnsigned(lX,lY) { var lX4,lY4,lX8,lY8,lResult; lX8 = (lX & 0x80000000); lY8 = (lY & 0x80000000); lX4 = (lX & 0x40000000); lY4 = (lY & 0x40000000); lResult = (lX & 0x3FFFFFFF)+(lY & 0x3FFFFFFF); if (lX4 & lY4) { return (lResult ^ 0x80000000 ^ lX8 ^ lY8); } if (lX4 | lY4) { if (lResult & 0x40000000) { return (lResult ^ 0xC0000000 ^ lX8 ^ lY8); } else { return (lResult ^ 0x40000000 ^ lX8 ^ lY8); } } else { return (lResult ^ lX8 ^ lY8); } } function F(x,y,z) { return (x & y) | ((~x) & z); } function G(x,y,z) { return (x & z) | (y & (~z)); } function H(x,y,z) { return (x ^ y ^ z); } function I(x,y,z) { return (y ^ (x | (~z))); } function FF(a,b,c,d,x,s,ac) { a = AddUnsigned(a, AddUnsigned(AddUnsigned(F(b, c, d), x), ac)); return AddUnsigned(RotateLeft(a, s), b); } function GG(a,b,c,d,x,s,ac) { a = AddUnsigned(a, AddUnsigned(AddUnsigned(G(b, c, d), x), ac)); return AddUnsigned(RotateLeft(a, s), b); } function HH(a,b,c,d,x,s,ac) { a = AddUnsigned(a, AddUnsigned(AddUnsigned(H(b, c, d), x), ac)); return AddUnsigned(RotateLeft(a, s), b); } function II(a,b,c,d,x,s,ac) { a = AddUnsigned(a, AddUnsigned(AddUnsigned(I(b, c, d), x), ac)); return AddUnsigned(RotateLeft(a, s), b); } function ConvertToWordArray(string) { var lWordCount; var lMessageLength = string.length; var lNumberOfWords_temp1=lMessageLength + 8; var lNumberOfWords_temp2=(lNumberOfWords_temp1-(lNumberOfWords_temp1 % 64))/64; var lNumberOfWords = (lNumberOfWords_temp2+1)*16; var lWordArray=Array(lNumberOfWords-1); var lBytePosition = 0; var lByteCount = 0; while ( lByteCount < lMessageLength ) { lWordCount = (lByteCount-(lByteCount % 4))/4; lBytePosition = (lByteCount % 4)*8; lWordArray[lWordCount] = (lWordArray[lWordCount] | (string.charCodeAt(lByteCount)<>>29; return lWordArray; } function WordToHex(lValue) { var WordToHexValue="",WordToHexValue_temp="",lByte,lCount; for (lCount = 0;lCount<=3;lCount++) { lByte = (lValue>>>(lCount*8)) & 255; WordToHexValue_temp = "0" + lByte.toString(16); WordToHexValue = WordToHexValue + WordToHexValue_temp.substr(WordToHexValue_temp.length-2,2); } return WordToHexValue; } function Utf8Encode(string) { string = string.replace(/\r\n/g,"\n"); var utftext = ""; for (var n = 0; n < string.length; n++) { var c = string.charCodeAt(n); if (c < 128) { utftext += String.fromCharCode(c); } else if((c > 127) && (c < 2048)) { utftext += String.fromCharCode((c >> 6) | 192); utftext += String.fromCharCode((c & 63) | 128); } else { utftext += String.fromCharCode((c >> 12) | 224); utftext += String.fromCharCode(((c >> 6) & 63) | 128); utftext += String.fromCharCode((c & 63) | 128); } } return utftext; } var x=[]; var k,AA,BB,CC,DD,a,b,c,d; var S11=7, S12=12, S13=17, S14=22; var S21=5, S22=9 , S23=14, S24=20; var S31=4, S32=11, S33=16, S34=23; var S41=6, S42=10, S43=15, S44=21; string = Utf8Encode(string); x = ConvertToWordArray(string); a = 0x67452301; b = 0xEFCDAB89; c = 0x98BADCFE; d = 0x10325476; for (k=0;k 1) { acquire = SIP.Utils.promisify(this.mediaStreamManager, 'acquire', true); } mediaHint = mediaHint || {}; if (mediaHint.dataChannel === true) { mediaHint.dataChannel = {}; } this.mediaHint = mediaHint; /* * 1. acquire streams (skip if MediaStreams passed in) * 2. addStreams * 3. createOffer/createAnswer */ var streamPromise; if (self.localMedia) { self.logger.log('already have local media'); streamPromise = SIP.Utils.Promise.resolve(self.localMedia); } else { self.logger.log('acquiring local media'); streamPromise = acquire.call(self.mediaStreamManager, mediaHint) .then(function acquireSucceeded(streams) { self.logger.log('acquired local media streams'); self.localMedia = streams; self.session.connecting(); return streams; }, function acquireFailed(err) { self.logger.error('unable to acquire streams'); self.logger.error(err); self.session.connecting(); throw err; }) .then(this.addStreams.bind(this)) ; } return streamPromise .then(function streamAdditionSucceeded() { if (self.hasOffer('remote')) { self.peerConnection.ondatachannel = function (evt) { self.dataChannel = evt.channel; self.emit('dataChannel', self.dataChannel); }; } else if (mediaHint.dataChannel && self.peerConnection.createDataChannel) { self.dataChannel = self.peerConnection.createDataChannel( 'sipjs', mediaHint.dataChannel ); self.emit('dataChannel', self.dataChannel); } self.render(); return self.createOfferOrAnswer(self.RTCConstraints); }) .then(function(sdp) { sdp = SIP.Hacks.Firefox.hasMissingCLineInSDP(sdp); if (self.local_hold) { // Don't receive media // TODO - This will break for media streams with different directions. if (!(/a=(sendrecv|sendonly|recvonly|inactive)/).test(sdp)) { sdp = sdp.replace(/(m=[^\r]*\r\n)/g, '$1a=sendonly\r\n'); } else { sdp = sdp.replace(/a=sendrecv\r\n/g, 'a=sendonly\r\n'); sdp = sdp.replace(/a=recvonly\r\n/g, 'a=inactive\r\n'); } } return { body: sdp, contentType: 'application/sdp' }; }) ; }}, /** * Check if a SIP message contains a session description. * @param {SIP.SIPMessage} message * @returns {boolean} */ hasDescription: {writeable: true, value: function hasDescription (message) { return message.getHeader('Content-Type') === 'application/sdp' && !!message.body; }}, /** * Set the session description contained in a SIP message. * @param {SIP.SIPMessage} message * @returns {Promise} */ setDescription: {writable: true, value: function setDescription (message) { var self = this; var sdp = message.body; this.remote_hold = /a=(sendonly|inactive)/.test(sdp); sdp = SIP.Hacks.Firefox.cannotHandleExtraWhitespace(sdp); sdp = SIP.Hacks.AllBrowsers.maskDtls(sdp); var rawDescription = { type: this.hasOffer('local') ? 'answer' : 'offer', sdp: sdp }; this.emit('setDescription', rawDescription); var description = new SIP.WebRTC.RTCSessionDescription(rawDescription); return SIP.Utils.promisify(this.peerConnection, 'setRemoteDescription')(description) .catch(function setRemoteDescriptionError(e) { self.emit('peerConnection-setRemoteDescriptionFailed', e); throw e; }); }}, /** * If the Session associated with this MediaHandler were to be referred, * what mediaHint should be provided to the UA's invite method? */ getReferMedia: {writable: true, value: function getReferMedia () { function hasTracks (trackGetter, stream) { return stream[trackGetter]().length > 0; } function bothHaveTracks (trackGetter) { /* jshint validthis:true */ return this.getLocalStreams().some(hasTracks.bind(null, trackGetter)) && this.getRemoteStreams().some(hasTracks.bind(null, trackGetter)); } return { constraints: { audio: bothHaveTracks.call(this, 'getAudioTracks'), video: bothHaveTracks.call(this, 'getVideoTracks') } }; }}, updateIceServers: {writeable:true, value: function (options) { var servers = this.prepareIceServers(options.stunServers, options.turnServers); this.RTCConstraints = options.RTCConstraints || this.RTCConstraints; this.initPeerConnection(servers); /* once updateIce is implemented correctly, this is better than above //no op if browser does not support this if (!this.peerConnection.updateIce) { return; } this.peerConnection.updateIce({'iceServers': servers}, this.RTCConstraints); */ }}, // Functions the session can use, but only because it's convenient for the application isMuted: {writable: true, value: function isMuted () { return { audio: this.audioMuted, video: this.videoMuted }; }}, mute: {writable: true, value: function mute (options) { if (this.getLocalStreams().length === 0) { return; } options = options || { audio: this.getLocalStreams()[0].getAudioTracks().length > 0, video: this.getLocalStreams()[0].getVideoTracks().length > 0 }; var audioMuted = false, videoMuted = false; if (options.audio && !this.audioMuted) { audioMuted = true; this.audioMuted = true; this.toggleMuteAudio(true); } if (options.video && !this.videoMuted) { videoMuted = true; this.videoMuted = true; this.toggleMuteVideo(true); } //REVISIT if (audioMuted || videoMuted) { return { audio: audioMuted, video: videoMuted }; /*this.session.onmute({ audio: audioMuted, video: videoMuted });*/ } }}, unmute: {writable: true, value: function unmute (options) { if (this.getLocalStreams().length === 0) { return; } options = options || { audio: this.getLocalStreams()[0].getAudioTracks().length > 0, video: this.getLocalStreams()[0].getVideoTracks().length > 0 }; var audioUnMuted = false, videoUnMuted = false; if (options.audio && this.audioMuted) { audioUnMuted = true; this.audioMuted = false; this.toggleMuteAudio(false); } if (options.video && this.videoMuted) { videoUnMuted = true; this.videoMuted = false; this.toggleMuteVideo(false); } //REVISIT if (audioUnMuted || videoUnMuted) { return { audio: audioUnMuted, video: videoUnMuted }; /*this.session.onunmute({ audio: audioUnMuted, video: videoUnMuted });*/ } }}, hold: {writable: true, value: function hold () { this.local_hold = true; this.toggleMuteAudio(true); this.toggleMuteVideo(true); }}, unhold: {writable: true, value: function unhold () { this.local_hold = false; if (!this.audioMuted) { this.toggleMuteAudio(false); } if (!this.videoMuted) { this.toggleMuteVideo(false); } }}, // Functions the application can use, but not the session getLocalStreams: {writable: true, value: function getLocalStreams () { var pc = this.peerConnection; if (pc && pc.signalingState === 'closed') { this.logger.warn('peerConnection is closed, getLocalStreams returning []'); return []; } return (pc.getLocalStreams && pc.getLocalStreams()) || pc.localStreams || []; }}, getRemoteStreams: {writable: true, value: function getRemoteStreams () { var pc = this.peerConnection; if (pc && pc.signalingState === 'closed') { this.logger.warn('peerConnection is closed, getRemoteStreams returning this._remoteStreams'); return this._remoteStreams; } return(pc.getRemoteStreams && pc.getRemoteStreams()) || pc.remoteStreams || []; }}, render: {writable: true, value: function render (renderHint) { renderHint = renderHint || (this.mediaHint && this.mediaHint.render); if (!renderHint) { return false; } var streamGetters = { local: 'getLocalStreams', remote: 'getRemoteStreams' }; Object.keys(streamGetters).forEach(function (loc) { var streamGetter = streamGetters[loc]; var streams = this[streamGetter](); SIP.WebRTC.MediaStreamManager.render(streams, renderHint[loc]); }.bind(this)); }}, // Internal functions hasOffer: {writable: true, value: function hasOffer (where) { var offerState = 'have-' + where + '-offer'; return this.peerConnection.signalingState === offerState; // TODO consider signalingStates with 'pranswer'? }}, prepareIceServers: {writable: true, value: function prepareIceServers (stunServers, turnServers) { var servers = [], config = this.session.ua.configuration; stunServers = stunServers || config.stunServers; turnServers = turnServers || config.turnServers; [].concat(stunServers).forEach(function (server) { servers.push({'urls': server}); }); [].concat(turnServers).forEach(function (server) { var turnServer = {'urls': server.urls}; if (server.username) { turnServer.username = server.username; } if (server.password) { turnServer.credential = server.password; } servers.push(turnServer); }); return servers; }}, initPeerConnection: {writable: true, value: function initPeerConnection(servers) { var self = this, config = this.session.ua.configuration; this.onIceCompleted = SIP.Utils.defer(); this.onIceCompleted.promise.then(function(pc) { self.emit('iceGatheringComplete', pc); if (self.iceCheckingTimer) { SIP.Timers.clearTimeout(self.iceCheckingTimer); self.iceCheckingTimer = null; } }); if (this.peerConnection) { this.peerConnection.close(); } var connConfig = { iceServers: servers }; if (config.rtcpMuxPolicy) { connConfig.rtcpMuxPolicy = config.rtcpMuxPolicy; } this.peerConnection = new SIP.WebRTC.RTCPeerConnection(connConfig); // Firefox (35.0.1) sometimes throws on calls to peerConnection.getRemoteStreams // even if peerConnection.onaddstream was just called. In order to make // MediaHandler.prototype.getRemoteStreams work, keep track of them manually this._remoteStreams = []; this.peerConnection.onaddstream = function(e) { self.logger.log('stream added: '+ e.stream.id); self._remoteStreams.push(e.stream); self.render(); self.emit('addStream', e); }; this.peerConnection.onremovestream = function(e) { self.logger.log('stream removed: '+ e.stream.id); }; this.startIceCheckingTimer = function () { if (!self.iceCheckingTimer) { self.iceCheckingTimer = SIP.Timers.setTimeout(function() { self.logger.log('RTCIceChecking Timeout Triggered after '+config.iceCheckingTimeout+' milliseconds'); self.onIceCompleted.resolve(this); }.bind(this.peerConnection), config.iceCheckingTimeout); } }; this.peerConnection.onicecandidate = function(e) { self.emit('iceCandidate', e); if (e.candidate) { self.logger.log('ICE candidate received: '+ (e.candidate.candidate === null ? null : e.candidate.candidate.trim())); self.startIceCheckingTimer(); } else { self.onIceCompleted.resolve(this); } }; this.peerConnection.onicegatheringstatechange = function () { self.logger.log('RTCIceGatheringState changed: ' + this.iceGatheringState); if (this.iceGatheringState === 'gathering') { self.emit('iceGathering', this); } if (this.iceGatheringState === 'complete') { self.onIceCompleted.resolve(this); } }; this.peerConnection.oniceconnectionstatechange = function() { //need e for commented out case var stateEvent; if (this.iceConnectionState === 'checking') { self.startIceCheckingTimer(); } switch (this.iceConnectionState) { case 'new': stateEvent = 'iceConnection'; break; case 'checking': stateEvent = 'iceConnectionChecking'; break; case 'connected': stateEvent = 'iceConnectionConnected'; break; case 'completed': stateEvent = 'iceConnectionCompleted'; break; case 'failed': stateEvent = 'iceConnectionFailed'; break; case 'disconnected': stateEvent = 'iceConnectionDisconnected'; break; case 'closed': stateEvent = 'iceConnectionClosed'; break; default: self.logger.warn('Unknown iceConnection state:', this.iceConnectionState); return; } self.emit(stateEvent, this); //Bria state changes are always connected -> disconnected -> connected on accept, so session gets terminated //normal calls switch from failed to connected in some cases, so checking for failed and terminated /*if (this.iceConnectionState === 'failed') { self.session.terminate({ cause: SIP.C.causes.RTP_TIMEOUT, status_code: 200, reason_phrase: SIP.C.causes.RTP_TIMEOUT }); } else if (e.currentTarget.iceGatheringState === 'complete' && this.iceConnectionState !== 'closed') { self.onIceCompleted(this); }*/ }; this.peerConnection.onstatechange = function() { self.logger.log('PeerConnection state changed to "'+ this.readyState +'"'); }; }}, createOfferOrAnswer: {writable: true, value: function createOfferOrAnswer (constraints) { var self = this; var methodName; var pc = self.peerConnection; self.ready = false; methodName = self.hasOffer('remote') ? 'createAnswer' : 'createOffer'; return SIP.Utils.promisify(pc, methodName, true)(constraints) .catch(function methodError(e) { self.emit('peerConnection-' + methodName + 'Failed', e); throw e; }) .then(SIP.Utils.promisify(pc, 'setLocalDescription')) .catch(function localDescError(e) { self.emit('peerConnection-selLocalDescriptionFailed', e); throw e; }) .then(function onSetLocalDescriptionSuccess() { var deferred = SIP.Utils.defer(); if (pc.iceGatheringState === 'complete' && (pc.iceConnectionState === 'connected' || pc.iceConnectionState === 'completed')) { deferred.resolve(); } else { self.onIceCompleted.promise.then(deferred.resolve); } return deferred.promise; }) .then(function readySuccess () { var sdp = pc.localDescription.sdp; sdp = SIP.Hacks.Chrome.needsExplicitlyInactiveSDP(sdp); sdp = SIP.Hacks.AllBrowsers.unmaskDtls(sdp); var sdpWrapper = { type: methodName === 'createOffer' ? 'offer' : 'answer', sdp: sdp }; self.emit('getDescription', sdpWrapper); if (self.session.ua.configuration.hackStripTcp) { sdpWrapper.sdp = sdpWrapper.sdp.replace(/^a=candidate:\d+ \d+ tcp .*?\r\n/img, ""); } self.ready = true; return sdpWrapper.sdp; }) .catch(function createOfferAnswerError (e) { self.logger.error(e); self.ready = true; throw new SIP.Exceptions.GetDescriptionError(e); }) ; }}, addStreams: {writable: true, value: function addStreams (streams) { try { streams = [].concat(streams); streams.forEach(function (stream) { this.peerConnection.addStream(stream); }, this); } catch(e) { this.logger.error('error adding stream'); this.logger.error(e); return SIP.Utils.Promise.reject(e); } return SIP.Utils.Promise.resolve(); }}, toggleMuteHelper: {writable: true, value: function toggleMuteHelper (trackGetter, mute) { this.getLocalStreams().forEach(function (stream) { stream[trackGetter]().forEach(function (track) { track.enabled = !mute; }); }); }}, toggleMuteAudio: {writable: true, value: function toggleMuteAudio (mute) { this.toggleMuteHelper('getAudioTracks', mute); }}, toggleMuteVideo: {writable: true, value: function toggleMuteVideo (mute) { this.toggleMuteHelper('getVideoTracks', mute); }} }); // Return since it will be assigned to a variable. return MediaHandler; }; },{}],34:[function(require,module,exports){ "use strict"; /** * @fileoverview MediaStreamManager */ /* MediaStreamManager * @class Manages the acquisition and release of MediaStreams. * @param {mediaHint} [defaultMediaHint] The mediaHint to use if none is provided to acquire() */ module.exports = function (SIP, environment) { // Default MediaStreamManager provides single-use streams created with getUserMedia var MediaStreamManager = function MediaStreamManager (logger, defaultMediaHint) { if (!SIP.WebRTC.isSupported()) { throw new SIP.Exceptions.NotSupportedError('Media not supported'); } this.mediaHint = defaultMediaHint || { constraints: {audio: true, video: true} }; // map of streams to acquisition manner: // true -> passed in as mediaHint.stream // false -> getUserMedia this.acquisitions = {}; }; MediaStreamManager.streamId = function (stream) { return stream.getAudioTracks().concat(stream.getVideoTracks()) .map(function trackId (track) { return track.id; }) .join(''); }; /** * @param {(Array of) MediaStream} streams - The streams to render * * @param {(Array of) HTMLMediaElement} elements * - The