10949 lines
330 KiB
JavaScript
10949 lines
330 KiB
JavaScript
|
/*
|
|||
|
* SIP version 0.6.1
|
|||
|
* Copyright (c) 2014-2014 Junction Networks, Inc <http://www.onsip.com>
|
|||
|
* 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 <http://www.versatica.com>
|
|||
|
*
|
|||
|
* 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(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.SIP=e()}}(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);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){
|
|||
|
module.exports={
|
|||
|
"name": "sip.js",
|
|||
|
"title": "SIP.js",
|
|||
|
"description": "A simple, intuitive, and powerful JavaScript signaling library",
|
|||
|
"version": "0.6.1",
|
|||
|
"main": "src/SIP.js",
|
|||
|
"homepage": "http://sipjs.com",
|
|||
|
"author": "Will Mitchell <will@onsip.com>",
|
|||
|
"contributors": [
|
|||
|
{
|
|||
|
"url": "http://sipjs.com/authors/"
|
|||
|
}
|
|||
|
],
|
|||
|
"repository": {
|
|||
|
"type": "git",
|
|||
|
"url": "https://github.com/onsip/SIP.js.git"
|
|||
|
},
|
|||
|
"keywords": [
|
|||
|
"sip",
|
|||
|
"websocket",
|
|||
|
"webrtc",
|
|||
|
"library",
|
|||
|
"javascript"
|
|||
|
],
|
|||
|
"devDependencies": {
|
|||
|
"grunt": "~0.4.0",
|
|||
|
"grunt-cli": "~0.1.6",
|
|||
|
"grunt-contrib-jasmine": "~0.6.0",
|
|||
|
"grunt-contrib-jshint": ">0.5.0",
|
|||
|
"grunt-contrib-uglify": "~0.2.0",
|
|||
|
"grunt-peg": "~1.3.1",
|
|||
|
"grunt-trimtrailingspaces": "^0.4.0",
|
|||
|
"node-minify": "~0.7.2",
|
|||
|
"pegjs": "0.8.0",
|
|||
|
"sdp-transform": "~0.4.0",
|
|||
|
"grunt-contrib-copy": "^0.5.0",
|
|||
|
"browserify": "^4.1.8",
|
|||
|
"grunt-browserify": "^2.1.0"
|
|||
|
},
|
|||
|
"engines": {
|
|||
|
"node": ">=0.8"
|
|||
|
},
|
|||
|
"license": "MIT",
|
|||
|
"scripts": {
|
|||
|
"test": "grunt travis --verbose"
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
},{}],2:[function(_dereq_,module,exports){
|
|||
|
module.exports = function (SIP) {
|
|||
|
var ClientContext;
|
|||
|
|
|||
|
ClientContext = function (ua, method, target, options) {
|
|||
|
var params, extraHeaders,
|
|||
|
originalTarget = target,
|
|||
|
events = [
|
|||
|
'progress',
|
|||
|
'accepted',
|
|||
|
'rejected',
|
|||
|
'failed',
|
|||
|
'cancel'
|
|||
|
];
|
|||
|
|
|||
|
if (target === undefined) {
|
|||
|
throw new TypeError('Not enough arguments');
|
|||
|
}
|
|||
|
|
|||
|
// Check target validity
|
|||
|
target = ua.normalizeTarget(target);
|
|||
|
if (!target) {
|
|||
|
throw new TypeError('Invalid target: ' + originalTarget);
|
|||
|
}
|
|||
|
|
|||
|
this.ua = ua;
|
|||
|
this.logger = ua.getLogger('sip.clientcontext');
|
|||
|
this.method = method;
|
|||
|
|
|||
|
params = options && options.params;
|
|||
|
extraHeaders = (options && options.extraHeaders || []).slice();
|
|||
|
|
|||
|
if (options && options.body) {
|
|||
|
this.body = options.body;
|
|||
|
}
|
|||
|
if (options && options.contentType) {
|
|||
|
this.contentType = options.contentType;
|
|||
|
extraHeaders.push('Content-Type: ' + this.contentType);
|
|||
|
}
|
|||
|
|
|||
|
this.request = new SIP.OutgoingRequest(this.method, target, this.ua, params, extraHeaders);
|
|||
|
|
|||
|
this.localIdentity = this.request.from;
|
|||
|
this.remoteIdentity = this.request.to;
|
|||
|
|
|||
|
if (this.body) {
|
|||
|
this.request.body = this.body;
|
|||
|
}
|
|||
|
|
|||
|
this.data = {};
|
|||
|
|
|||
|
this.initEvents(events);
|
|||
|
};
|
|||
|
ClientContext.prototype = new SIP.EventEmitter();
|
|||
|
|
|||
|
ClientContext.prototype.send = function () {
|
|||
|
(new SIP.RequestSender(this, this.ua)).send();
|
|||
|
return this;
|
|||
|
};
|
|||
|
|
|||
|
ClientContext.prototype.cancel = function (options) {
|
|||
|
options = options || {};
|
|||
|
|
|||
|
var
|
|||
|
status_code = options.status_code,
|
|||
|
reason_phrase = options.reason_phrase,
|
|||
|
cancel_reason;
|
|||
|
|
|||
|
if (status_code && status_code < 200 || status_code > 699) {
|
|||
|
throw new TypeError('Invalid status_code: ' + status_code);
|
|||
|
} else if (status_code) {
|
|||
|
reason_phrase = reason_phrase || SIP.C.REASON_PHRASE[status_code] || '';
|
|||
|
cancel_reason = 'SIP ;cause=' + status_code + ' ;text="' + reason_phrase + '"';
|
|||
|
}
|
|||
|
this.request.cancel(cancel_reason);
|
|||
|
|
|||
|
this.emit('cancel');
|
|||
|
};
|
|||
|
|
|||
|
ClientContext.prototype.receiveResponse = function (response) {
|
|||
|
var cause = SIP.C.REASON_PHRASE[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;
|
|||
|
};
|
|||
|
|
|||
|
},{}],3:[function(_dereq_,module,exports){
|
|||
|
/**
|
|||
|
* @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'
|
|||
|
}
|
|||
|
};
|
|||
|
};
|
|||
|
|
|||
|
},{}],4:[function(_dereq_,module,exports){
|
|||
|
|
|||
|
/**
|
|||
|
* @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.off('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;
|
|||
|
};
|
|||
|
|
|||
|
},{}],5:[function(_dereq_,module,exports){
|
|||
|
/**
|
|||
|
* @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, RequestSender) {
|
|||
|
|
|||
|
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'));
|
|||
|
};
|
|||
|
|
|||
|
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.off('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(),
|
|||
|
body = options.body || null,
|
|||
|
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;
|
|||
|
};
|
|||
|
|
|||
|
},{}],6:[function(_dereq_,module,exports){
|
|||
|
|
|||
|
/**
|
|||
|
* @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;
|
|||
|
};
|
|||
|
|
|||
|
},{}],7:[function(_dereq_,module,exports){
|
|||
|
/**
|
|||
|
* @fileoverview EventEmitter
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* @augments SIP
|
|||
|
* @class Class creating an event emitter.
|
|||
|
*/
|
|||
|
module.exports = function (SIP) {
|
|||
|
var
|
|||
|
EventEmitter,
|
|||
|
Event,
|
|||
|
logger = new SIP.LoggerFactory().getLogger('sip.eventemitter'),
|
|||
|
C = {
|
|||
|
MAX_LISTENERS: 10
|
|||
|
};
|
|||
|
|
|||
|
EventEmitter = function(){};
|
|||
|
EventEmitter.prototype = {
|
|||
|
/**
|
|||
|
* Initialize events dictionaries.
|
|||
|
* @param {Array} events
|
|||
|
*/
|
|||
|
initEvents: function(events) {
|
|||
|
this.events = {};
|
|||
|
|
|||
|
return this.initMoreEvents(events);
|
|||
|
},
|
|||
|
|
|||
|
initMoreEvents: function(events) {
|
|||
|
var idx;
|
|||
|
|
|||
|
if (!this.logger) {
|
|||
|
this.logger = logger;
|
|||
|
}
|
|||
|
|
|||
|
this.maxListeners = C.MAX_LISTENERS;
|
|||
|
|
|||
|
for (idx = 0; idx < events.length; idx++) {
|
|||
|
if (!this.events[events[idx]]) {
|
|||
|
this.logger.log('adding event '+ events[idx]);
|
|||
|
this.events[events[idx]] = [];
|
|||
|
} else {
|
|||
|
this.logger.log('skipping event '+ events[idx]+ ' - Event exists');
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
return this;
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Check whether an event exists or not.
|
|||
|
* @param {String} event
|
|||
|
* @returns {Boolean}
|
|||
|
*/
|
|||
|
checkEvent: function(event) {
|
|||
|
return !!(this.events && this.events[event]);
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Check whether an event exists and has at least one listener or not.
|
|||
|
* @param {String} event
|
|||
|
* @returns {Boolean}
|
|||
|
*/
|
|||
|
checkListener: function(event) {
|
|||
|
return this.checkEvent(event) && this.events[event].length > 0;
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Add a listener to the end of the listeners array for the specified event.
|
|||
|
* @param {String} event
|
|||
|
* @param {Function} listener
|
|||
|
*/
|
|||
|
on: function(event, listener, bindTarget) {
|
|||
|
if (listener === undefined) {
|
|||
|
return this;
|
|||
|
} else if (typeof listener !== 'function') {
|
|||
|
this.logger.error('listener must be a function');
|
|||
|
return this;
|
|||
|
} else if (!this.checkEvent(event)) {
|
|||
|
this.logger.error('unable to add a listener to a nonexistent event '+ event);
|
|||
|
throw new TypeError('Invalid or uninitialized event: ' + event);
|
|||
|
}
|
|||
|
|
|||
|
var listenerObj = { listener: listener };
|
|||
|
if (bindTarget) {
|
|||
|
listenerObj.bindTarget = bindTarget;
|
|||
|
}
|
|||
|
|
|||
|
if (this.events[event].length >= this.maxListeners) {
|
|||
|
this.logger.warn('max listeners exceeded for event '+ event);
|
|||
|
return this;
|
|||
|
}
|
|||
|
|
|||
|
this.events[event].push(listenerObj);
|
|||
|
this.logger.log('new listener added to event '+ event);
|
|||
|
return this;
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Add a one time listener for the specified event.
|
|||
|
* The listener is invoked only the next time the event is fired, then it is removed.
|
|||
|
* @param {String} event
|
|||
|
* @param {Function} listener
|
|||
|
*/
|
|||
|
once: function(event, listener, bindTarget) {
|
|||
|
var self = this;
|
|||
|
function listenOnce () {
|
|||
|
listener.apply(this, arguments);
|
|||
|
self.off(event, listenOnce, bindTarget);
|
|||
|
}
|
|||
|
|
|||
|
return this.on(event, listenOnce, bindTarget);
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Remove a listener from the listener array for the specified event.
|
|||
|
* Note that the order of the array elements will change after removing the listener
|
|||
|
* @param {String} event
|
|||
|
* @param {Function} listener
|
|||
|
*/
|
|||
|
off: function(event, listener, bindTarget) {
|
|||
|
var events, length,
|
|||
|
idx = 0;
|
|||
|
|
|||
|
if (listener && typeof listener !== 'function') {
|
|||
|
this.logger.error('listener must be a function');
|
|||
|
return this;
|
|||
|
} else if (!event) {
|
|||
|
for (idx in this.events) {
|
|||
|
this.events[idx] = [];
|
|||
|
}
|
|||
|
return this;
|
|||
|
} else if (!this.checkEvent(event)) {
|
|||
|
this.logger.error('unable to remove a listener from a nonexistent event '+ event);
|
|||
|
throw new TypeError('Invalid or uninitialized event: ' + event);
|
|||
|
}
|
|||
|
|
|||
|
events = this.events[event];
|
|||
|
length = events.length;
|
|||
|
|
|||
|
while (idx < length) {
|
|||
|
if (events[idx] &&
|
|||
|
(!listener || events[idx].listener === listener) &&
|
|||
|
(!bindTarget || events[idx].bindTarget === bindTarget)) {
|
|||
|
events.splice(idx,1);
|
|||
|
} else {
|
|||
|
idx ++;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
return this;
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* By default EventEmitter will print a warning
|
|||
|
* if more than C.MAX_LISTENERS listeners are added for a particular event.
|
|||
|
* This function allows that limit to be modified.
|
|||
|
* @param {Number} listeners
|
|||
|
*/
|
|||
|
setMaxListeners: function(listeners) {
|
|||
|
if (typeof listeners !== 'number' || listeners < 0) {
|
|||
|
this.logger.error('listeners must be a positive number');
|
|||
|
return this;
|
|||
|
}
|
|||
|
|
|||
|
this.maxListeners = listeners;
|
|||
|
return this;
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Execute each of the listeners in order with the supplied arguments.
|
|||
|
* @param {String} events
|
|||
|
* @param {Array} args
|
|||
|
*/
|
|||
|
emit: function(event) {
|
|||
|
if (!this.checkEvent(event)) {
|
|||
|
this.logger.error('unable to emit a nonexistent event '+ event);
|
|||
|
throw new TypeError('Invalid or uninitialized event: ' + event);
|
|||
|
}
|
|||
|
|
|||
|
this.logger.log('emitting event '+ event);
|
|||
|
|
|||
|
// Fire event listeners
|
|||
|
var args = Array.prototype.slice.call(arguments, 1);
|
|||
|
this.events[event].slice().forEach(function (listener) {
|
|||
|
try {
|
|||
|
listener.listener.apply(listener.bindTarget || this, args);
|
|||
|
} catch(err) {
|
|||
|
this.logger.error(err.stack);
|
|||
|
}
|
|||
|
}, this);
|
|||
|
|
|||
|
return this;
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
Event = function(type, sender, data) {
|
|||
|
this.type = type;
|
|||
|
this.sender= sender;
|
|||
|
this.data = data;
|
|||
|
};
|
|||
|
|
|||
|
EventEmitter.C = C;
|
|||
|
|
|||
|
SIP.EventEmitter = EventEmitter;
|
|||
|
SIP.Event = Event;
|
|||
|
};
|
|||
|
|
|||
|
},{}],8:[function(_dereq_,module,exports){
|
|||
|
/**
|
|||
|
* @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;
|
|||
|
}()),
|
|||
|
|
|||
|
NotReadyError: (function(){
|
|||
|
var exception = function(message) {
|
|||
|
this.code = 4;
|
|||
|
this.name = 'NOT_READY_ERROR';
|
|||
|
this.message = message;
|
|||
|
};
|
|||
|
exception.prototype = new Error();
|
|||
|
return exception;
|
|||
|
}())
|
|||
|
};
|
|||
|
|
|||
|
},{}],9:[function(_dereq_,module,exports){
|
|||
|
/* jshint ignore:start */
|
|||
|
module.exports = function(SIP) {
|
|||
|
/*
|
|||
|
* 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: 182, Via: 190, absoluteURI: 84, Call_ID: 117, Content_Length: 134, Content_Type: 135, CSeq: 145, displayName: 121, Event: 148, From: 150, host: 52, Max_Forwards: 153, Proxy_Authenticate: 156, quoted_string: 40, Refer_To: 177, stun_URI: 209, To: 188, turn_URI: 216, uuid: 220, WWW_Authenticate: 205, 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: "\"%\"" },
|
|||
|
function(escaped) {return escaped.join(''); },
|
|||
|
null,
|
|||
|
[],
|
|||
|
function() {return " "; },
|
|||
|
function() {return ':'; },
|
|||
|
function() {
|
|||
|
return input.substring(peg$currPos, offset()); },
|
|||
|
/^[!-~]/,
|
|||
|
{ 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: "\"`\"" },
|
|||
|
function() {
|
|||
|
return input.substring(peg$currPos, offset()); },
|
|||
|
"<",
|
|||
|
{ 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() {
|
|||
|
return input.substring(peg$currPos-1, offset()+1); },
|
|||
|
/^[#-[]/,
|
|||
|
{ 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() {
|
|||
|
data.uri = new SIP.URI(data.scheme, data.user, data.host, data.port);
|
|||
|
delete data.scheme;
|
|||
|
delete data.user;
|
|||
|
delete data.host;
|
|||
|
delete data.host_type;
|
|||
|
delete data.port;
|
|||
|
},
|
|||
|
function() {
|
|||
|
var header;
|
|||
|
data.uri = new SIP.URI(data.scheme, data.user, data.host, data.port, data.uri_params, data.uri_headers);
|
|||
|
delete data.scheme;
|
|||
|
delete data.user;
|
|||
|
delete data.host;
|
|||
|
delete data.host_type;
|
|||
|
delete data.port;
|
|||
|
delete data.uri_params;
|
|||
|
|
|||
|
if (options.startRule === 'SIP_URI') { data = data.uri;}
|
|||
|
},
|
|||
|
"sips",
|
|||
|
{ type: "literal", value: "sips", description: "\"sips\"" },
|
|||
|
"sip",
|
|||
|
{ type: "literal", value: "sip", description: "\"sip\"" },
|
|||
|
function(uri_scheme) {
|
|||
|
data.scheme = uri_scheme.toLowerCase(); },
|
|||
|
function() {
|
|||
|
data.user = decodeURIComponent(input.substring(peg$currPos-1, offset()));},
|
|||
|
function() {
|
|||
|
data.password = input.substring(peg$currPos, offset()); },
|
|||
|
function() {
|
|||
|
data.host = input.substring(peg$currPos, offset()).toLowerCase();
|
|||
|
return data.host; },
|
|||
|
function() {
|
|||
|
data.host_type = 'domain';
|
|||
|
return input.substring(peg$currPos, offset()); },
|
|||
|
/^[a-zA-Z0-9_\-]/,
|
|||
|
{ type: "class", value: "[a-zA-Z0-9_\\-]", description: "[a-zA-Z0-9_\\-]" },
|
|||
|
/^[a-zA-Z_\-]/,
|
|||
|
{ type: "class", value: "[a-zA-Z_\\-]", description: "[a-zA-Z_\\-]" },
|
|||
|
function() {
|
|||
|
data.host_type = 'IPv6';
|
|||
|
return input.substring(peg$currPos, offset()); },
|
|||
|
"::",
|
|||
|
{ type: "literal", value: "::", description: "\"::\"" },
|
|||
|
function() {
|
|||
|
data.host_type = 'IPv6';
|
|||
|
return input.substring(peg$currPos, offset()); },
|
|||
|
function() {
|
|||
|
data.host_type = 'IPv4';
|
|||
|
return input.substring(peg$currPos, offset()); },
|
|||
|
"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(''));
|
|||
|
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(!data.uri_params) data.uri_params={};
|
|||
|
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(!data.uri_params) data.uri_params={};
|
|||
|
data.uri_params['user'] = user.toLowerCase(); },
|
|||
|
"method=",
|
|||
|
{ type: "literal", value: "method=", description: "\"method=\"" },
|
|||
|
function(method) {
|
|||
|
if(!data.uri_params) data.uri_params={};
|
|||
|
data.uri_params['method'] = method; },
|
|||
|
"ttl=",
|
|||
|
{ type: "literal", value: "ttl=", description: "\"ttl=\"" },
|
|||
|
function(ttl) {
|
|||
|
if(!data.params) data.params={};
|
|||
|
data.params['ttl'] = ttl; },
|
|||
|
"maddr=",
|
|||
|
{ type: "literal", value: "maddr=", description: "\"maddr=\"" },
|
|||
|
function(maddr) {
|
|||
|
if(!data.uri_params) data.uri_params={};
|
|||
|
data.uri_params['maddr'] = maddr; },
|
|||
|
"lr",
|
|||
|
{ type: "literal", value: "lr", description: "\"lr\"" },
|
|||
|
function() {
|
|||
|
if(!data.uri_params) data.uri_params={};
|
|||
|
data.uri_params['lr'] = undefined; },
|
|||
|
function(param, value) {
|
|||
|
if(!data.uri_params) data.uri_params = {};
|
|||
|
if (value === null){
|
|||
|
value = undefined;
|
|||
|
}
|
|||
|
else {
|
|||
|
value = value[1];
|
|||
|
}
|
|||
|
data.uri_params[param.toLowerCase()] = value && value.toLowerCase();},
|
|||
|
function(pname) {return pname.join(''); },
|
|||
|
function(pvalue) {return pvalue.join(''); },
|
|||
|
function(hname, hvalue) {
|
|||
|
hname = hname.join('').toLowerCase();
|
|||
|
hvalue = hvalue.join('');
|
|||
|
if(!data.uri_headers) data.uri_headers = {};
|
|||
|
if (!data.uri_headers[hname]) {
|
|||
|
data.uri_headers[hname] = [hvalue];
|
|||
|
} else {
|
|||
|
data.uri_headers[hname].push(hvalue);
|
|||
|
}},
|
|||
|
"//",
|
|||
|
{ type: "literal", value: "//", description: "\"//\"" },
|
|||
|
function() {
|
|||
|
data.scheme= input.substring(peg$currPos, offset()); },
|
|||
|
{ type: "literal", value: "SIP", description: "\"SIP\"" },
|
|||
|
function() {
|
|||
|
data.sip_version = input.substring(peg$currPos, offset()); },
|
|||
|
"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() {
|
|||
|
|
|||
|
data.method = input.substring(peg$currPos, offset());
|
|||
|
return data.method; },
|
|||
|
function(status_code) {
|
|||
|
data.status_code = parseInt(status_code.join('')); },
|
|||
|
function() {
|
|||
|
data.reason_phrase = input.substring(peg$currPos, offset()); },
|
|||
|
function() {
|
|||
|
data = input.substring(peg$currPos, offset()); },
|
|||
|
function() {
|
|||
|
var idx, length;
|
|||
|
length = data.multi_header.length;
|
|||
|
for (idx = 0; idx < length; idx++) {
|
|||
|
if (data.multi_header[idx].parsed === null) {
|
|||
|
data = null;
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
if (data !== null) {
|
|||
|
data = data.multi_header;
|
|||
|
} else {
|
|||
|
data = -1;
|
|||
|
}},
|
|||
|
function() {
|
|||
|
var header;
|
|||
|
if(!data.multi_header) data.multi_header = [];
|
|||
|
try {
|
|||
|
header = new SIP.NameAddrHeader(data.uri, data.displayName, data.params);
|
|||
|
delete data.uri;
|
|||
|
delete data.displayName;
|
|||
|
delete data.params;
|
|||
|
} catch(e) {
|
|||
|
header = null;
|
|||
|
}
|
|||
|
data.multi_header.push( { 'position': peg$currPos,
|
|||
|
'offset': offset(),
|
|||
|
'parsed': header
|
|||
|
});},
|
|||
|
function(displayName) {
|
|||
|
displayName = input.substring(peg$currPos, offset()).trim();
|
|||
|
if (displayName[0] === '\"') {
|
|||
|
displayName = displayName.substring(1, displayName.length-1);
|
|||
|
}
|
|||
|
data.displayName = displayName; },
|
|||
|
"q",
|
|||
|
{ type: "literal", value: "q", description: "\"q\"" },
|
|||
|
function(q) {
|
|||
|
if(!data.params) data.params = {};
|
|||
|
data.params['q'] = q; },
|
|||
|
"expires",
|
|||
|
{ type: "literal", value: "expires", description: "\"expires\"" },
|
|||
|
function(expires) {
|
|||
|
if(!data.params) data.params = {};
|
|||
|
data.params['expires'] = expires; },
|
|||
|
function(delta_seconds) {
|
|||
|
return parseInt(delta_seconds.join('')); },
|
|||
|
"0",
|
|||
|
{ type: "literal", value: "0", description: "\"0\"" },
|
|||
|
function() {
|
|||
|
return parseFloat(input.substring(peg$currPos, offset())); },
|
|||
|
function(param, value) {
|
|||
|
if(!data.params) data.params = {};
|
|||
|
if (value === null){
|
|||
|
value = undefined;
|
|||
|
}
|
|||
|
else {
|
|||
|
value = value[1];
|
|||
|
}
|
|||
|
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\"" },
|
|||
|
"handling",
|
|||
|
{ type: "literal", value: "handling", description: "\"handling\"" },
|
|||
|
"optional",
|
|||
|
{ type: "literal", value: "optional", description: "\"optional\"" },
|
|||
|
"required",
|
|||
|
{ type: "literal", value: "required", description: "\"required\"" },
|
|||
|
function(length) {
|
|||
|
data = parseInt(length.join('')); },
|
|||
|
function() {
|
|||
|
data = input.substring(peg$currPos, offset()); },
|
|||
|
"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) {
|
|||
|
data.value=parseInt(cseq_value.join('')); },
|
|||
|
function(expires) {data = expires; },
|
|||
|
function(event_type) {
|
|||
|
data.event = event_type.join('').toLowerCase(); },
|
|||
|
function() {
|
|||
|
var tag = data.tag;
|
|||
|
data = new SIP.NameAddrHeader(data.uri, data.displayName, data.params);
|
|||
|
if (tag) {data.setParam('tag',tag)}
|
|||
|
},
|
|||
|
"tag",
|
|||
|
{ type: "literal", value: "tag", description: "\"tag\"" },
|
|||
|
function(tag) {data.tag = tag; },
|
|||
|
function(forwards) {
|
|||
|
data = parseInt(forwards.join('')); },
|
|||
|
function(min_expires) {data = min_expires; },
|
|||
|
function() {
|
|||
|
data = new SIP.NameAddrHeader(data.uri, data.displayName, data.params);
|
|||
|
},
|
|||
|
"digest",
|
|||
|
{ type: "literal", value: "Digest", description: "\"Digest\"" },
|
|||
|
"realm",
|
|||
|
{ type: "literal", value: "realm", description: "\"realm\"" },
|
|||
|
function(realm) { data.realm = realm; },
|
|||
|
"domain",
|
|||
|
{ type: "literal", value: "domain", description: "\"domain\"" },
|
|||
|
"nonce",
|
|||
|
{ type: "literal", value: "nonce", description: "\"nonce\"" },
|
|||
|
function(nonce) { data.nonce=nonce; },
|
|||
|
"opaque",
|
|||
|
{ type: "literal", value: "opaque", description: "\"opaque\"" },
|
|||
|
function(opaque) { data.opaque=opaque; },
|
|||
|
"stale",
|
|||
|
{ type: "literal", value: "stale", description: "\"stale\"" },
|
|||
|
"true",
|
|||
|
{ type: "literal", value: "true", description: "\"true\"" },
|
|||
|
function() { data.stale=true; },
|
|||
|
"false",
|
|||
|
{ type: "literal", value: "false", description: "\"false\"" },
|
|||
|
function() { 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) {
|
|||
|
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) {
|
|||
|
data.qop || (data.qop=[]);
|
|||
|
data.qop.push(qop_value.toLowerCase()); },
|
|||
|
function(rack_value) {
|
|||
|
data.value=parseInt(rack_value.join('')); },
|
|||
|
function() {
|
|||
|
var idx, length;
|
|||
|
length = data.multi_header.length;
|
|||
|
for (idx = 0; idx < length; idx++) {
|
|||
|
if (data.multi_header[idx].parsed === null) {
|
|||
|
data = null;
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
if (data !== null) {
|
|||
|
data = data.multi_header;
|
|||
|
} else {
|
|||
|
data = -1;
|
|||
|
}},
|
|||
|
function() {
|
|||
|
var header;
|
|||
|
if(!data.multi_header) data.multi_header = [];
|
|||
|
try {
|
|||
|
header = new SIP.NameAddrHeader(data.uri, data.displayName, data.params);
|
|||
|
delete data.uri;
|
|||
|
delete data.displayName;
|
|||
|
delete data.params;
|
|||
|
} catch(e) {
|
|||
|
header = null;
|
|||
|
}
|
|||
|
data.multi_header.push( { 'position': peg$currPos,
|
|||
|
'offset': offset(),
|
|||
|
'parsed': header
|
|||
|
});},
|
|||
|
function() {
|
|||
|
data = new SIP.NameAddrHeader(data.uri, data.displayName, data.params);
|
|||
|
},
|
|||
|
function(rseq_value) {
|
|||
|
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() {
|
|||
|
data.state = input.substring(peg$currPos, offset()); },
|
|||
|
"reason",
|
|||
|
{ type: "literal", value: "reason", description: "\"reason\"" },
|
|||
|
function(reason) {
|
|||
|
if (typeof reason !== 'undefined') data.reason = reason; },
|
|||
|
function(expires) {
|
|||
|
if (typeof expires !== 'undefined') data.expires = expires; },
|
|||
|
"retry_after",
|
|||
|
{ type: "literal", value: "retry_after", description: "\"retry_after\"" },
|
|||
|
function(retry_after) {
|
|||
|
if (typeof retry_after !== 'undefined') 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() {
|
|||
|
var tag = data.tag;
|
|||
|
data = new SIP.NameAddrHeader(data.uri, data.displayName, data.params);
|
|||
|
if (tag) {data.setParam('tag',tag)}
|
|||
|
},
|
|||
|
"ttl",
|
|||
|
{ type: "literal", value: "ttl", description: "\"ttl\"" },
|
|||
|
function(via_ttl_value) {
|
|||
|
data.ttl = via_ttl_value; },
|
|||
|
"maddr",
|
|||
|
{ type: "literal", value: "maddr", description: "\"maddr\"" },
|
|||
|
function(via_maddr) {
|
|||
|
data.maddr = via_maddr; },
|
|||
|
"received",
|
|||
|
{ type: "literal", value: "received", description: "\"received\"" },
|
|||
|
function(via_received) {
|
|||
|
data.received = via_received; },
|
|||
|
"branch",
|
|||
|
{ type: "literal", value: "branch", description: "\"branch\"" },
|
|||
|
function(via_branch) {
|
|||
|
data.branch = via_branch; },
|
|||
|
"rport",
|
|||
|
{ type: "literal", value: "rport", description: "\"rport\"" },
|
|||
|
function() {
|
|||
|
if(typeof response_port !== 'undefined')
|
|||
|
data.rport = response_port.join(''); },
|
|||
|
function(via_protocol) {
|
|||
|
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) {
|
|||
|
data.transport = via_transport; },
|
|||
|
function() {
|
|||
|
data.host = input.substring(peg$currPos, offset()); },
|
|||
|
function(via_sent_by_port) {
|
|||
|
data.port = parseInt(via_sent_by_port.join('')); },
|
|||
|
function(ttl) {
|
|||
|
return parseInt(ttl.join('')); },
|
|||
|
"stuns",
|
|||
|
{ type: "literal", value: "stuns", description: "\"stuns\"" },
|
|||
|
"stun",
|
|||
|
{ type: "literal", value: "stun", description: "\"stun\"" },
|
|||
|
function(scheme) {
|
|||
|
data.scheme = scheme; },
|
|||
|
function(host) {
|
|||
|
data.host = host; },
|
|||
|
function() {
|
|||
|
return input.substring(peg$currPos, offset()); },
|
|||
|
"?transport=",
|
|||
|
{ type: "literal", value: "?transport=", description: "\"?transport=\"" },
|
|||
|
"turns",
|
|||
|
{ type: "literal", value: "turns", description: "\"turns\"" },
|
|||
|
"turn",
|
|||
|
{ type: "literal", value: "turn", description: "\"turn\"" },
|
|||
|
function() {
|
|||
|
data.transport = transport; },
|
|||
|
"uuid:",
|
|||
|
{ type: "literal", value: "uuid:", description: "\"uuid:\"" },
|
|||
|
function(uuid) {
|
|||
|
data = input.substring(peg$currPos+5, offset()); }
|
|||
|
],
|
|||
|
|
|||
|
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+' 4!6[!! %"),
|
|||
|
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+s$ ]! ]7.,#&7.\"+-$72+#%'\"%$\"# X\"# X,@&! ]7.,#&7.\"+-$72+#%'\"%$\"# X\"# X\"+'%4\"6`\" %$\"# X\"# X"),
|
|||
|
peg$decode("0a\"\"1!3b*# \"73"),
|
|||
|
peg$decode("0c\"\"1!3d"),
|
|||
|
peg$decode("0e\"\"1!3f"),
|
|||
|
peg$decode("7!*) \"0g\"\"1!3h"),
|
|||
|
peg$decode("! ]7)*\x95 \".F\"\"2F3G*\x89 \".J\"\"2J3K*} \".L\"\"2L3M*q \".Y\"\"2Y3Z*e \".P\"\"2P3Q*Y \".H\"\"2H3I*M \".@\"\"2@3A*A \".i\"\"2i3j*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 \".i\"\"2i3j*5 \".R\"\"2R3S*) \".N\"\"2N3O\"\"\" X+& 4!6k! %"),
|
|||
|
peg$decode("! ]7)*\x89 \".F\"\"2F3G*} \".L\"\"2L3M*q \".Y\"\"2Y3Z*e \".P\"\"2P3Q*Y \".H\"\"2H3I*M \".@\"\"2@3A*A \".i\"\"2i3j*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 \".i\"\"2i3j*5 \".R\"\"2R3S*) \".N\"\"2N3O\"\"\" X+& 4!6k! %"),
|
|||
|
peg$decode(".T\"\"2T3U*\xE3 \".V\"\"2V3W*\xD7 \".l\"\"2l3m*\xCB \".n\"\"2n3o*\xBF \".:\"\"2:3;*\xB3 \".D\"\"2D3E*\xA7 \".2\"\"2233*\x9B \".8\"\"2839*\x8F \".p\"\"2p3q*\x83 \"7&*} \".4\"\"2435*q \".r\"\"2r3s*e \".t\"\"2t3u*Y \".6\"\"2637*M \".>\"\"2>3?*A \".v\"\"2v3w*5 \".x\"\"2x3y*) \"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 \".i\"\"2i3j*\xCB \".R\"\"2R3S*\xBF \".N\"\"2N3O*\xB3 \".T\"\"2T3U*\xA7 \".V\"\"2V3W*\x9B \".l\"\"2l3m*\x8F \".n\"\"2n3o*\x83 \".8\"\"2839*w \".p\"\"2p3q*k \"7&*e \".4\"\"2435*Y \".r\"\"2r3s*M \".t\"\"2t3u*A \".6\"\"2637*5 \".v\"\"2v3w*) \".x\"\"2x3y+\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 \".i\"\"2i3j*\xCB \".R\"\"2R3S*\xBF \".N\"\"2N3O*\xB3 \".T\"\"2T3U*\xA7 \".V\"\"2V3W*\x9B \".l\"\"2l3m*\x8F \".n\"\"2n3o*\x83 \".8\"\"2839*w \".p\"\"2p3q*k \"7&*e \".4\"\"2435*Y \".r\"\"2r3s*M \".t\"\"2t3u*A \".6\"\"2637*5 \".v\"\"2v3w*) \".x\"\"2x3y\"\"\" X+& 4!6k! %"),
|
|||
|
peg$decode("!7/+A$.P\"\"2P3Q+1%7/+'%4#6z# %$## X$\"# X\"# X"),
|
|||
|
peg$decode("!7/+A$.4\"\"2435+1%7/+'%4#6{# %$## X$\"# X\"# X"),
|
|||
|
peg$decode("!7/+A$.>\"\"2>3?+1%7/+'%4#6|# %$## X$\"# X\"# X"),
|
|||
|
peg$decode("!7/+A$.T\"\"2T3U+1%7/+'%4#6}# %$## X$\"# X\"# X"),
|
|||
|
peg$decode("!7/+A$.V\"\"2V3W+1%7/+'%4#6~# %$## X$\"# X\"# X"),
|
|||
|
peg$decode("!.n\"\"2n3o+1$7/+'%4\"6\" %$\"# X\"# X"),
|
|||
|
peg$decode("!7/+7$.l\"\"2l3m+'%4\"6\x80\" %$\"# X\"# X"),
|
|||
|
peg$decode("!7/+A$.D\"\"2D3E+1%7/+'%4#6\x81# %$## X$\"# X\"# X"),
|
|||
|
peg$decode("!7/+A$.2\"\"2233+1%7/+'%4#6\x82# %$## X$\"# X\"# X"),
|
|||
|
peg$decode("!7/+A$.8\"\"2839+1%7/+'%4#6\x83# %$## X$\"# X\"# X"),
|
|||
|
peg$decode("!7/+1$7&+'%4\"6\x84\" %$\"# X\"# X"),
|
|||
|
peg$decode("!7&+1$7/+'%4\"6\x84\" %$\"# X\"# X"),
|
|||
|
peg$decode("!7=+W$ ]7G*) \"7K*# \"7F,/&7G*) \"7K*# \"7F\"+-%7>+#%'#%$## X$\"# X\"# X"),
|
|||
|
peg$decode("0\x85\"\"1!3\x86*A \"0\x87\"\"1!3\x88*5 \"0\x89\"\"1!3\x8A*) \"73*# \"7."),
|
|||
|
peg$decode("!7/+Y$7&+O% ]7J*# \"7K,)&7J*# \"7K\"+1%7&+'%4$6k$ %$$# X$## X$\"# X\"# X"),
|
|||
|
peg$decode("!7/+Y$7&+O% ]7J*# \"7K,)&7J*# \"7K\"+1%7&+'%4$6\x8B$ %$$# X$## X$\"# X\"# X"),
|
|||
|
peg$decode("7.*G \".L\"\"2L3M*; \"0\x8C\"\"1!3\x8D*/ \"0\x89\"\"1!3\x8A*# \"73"),
|
|||
|
peg$decode("!.p\"\"2p3q+K$0\x8E\"\"1!3\x8F*5 \"0\x90\"\"1!3\x91*) \"0\x92\"\"1!3\x93+#%'\"%$\"# X\"# X"),
|
|||
|
peg$decode("!7N+Q$.8\"\"2839+A%7O*# \" \\+1%7S+'%4$6\x94$ %$$# X$## X$\"# X\"# X"),
|
|||
|
peg$decode("!7N+k$.8\"\"2839+[%7O*# \" \\+K%7S+A%7_+7%7l*# \" \\+'%4&6\x95& %$&# X$%# X$$# X$## X$\"# X\"# X"),
|
|||
|
peg$decode("!/\x96\"\"1$3\x97*) \"/\x98\"\"1#3\x99+' 4!6\x9A!! %"),
|
|||
|
peg$decode("!7P+b$!.8\"\"2839+-$7R+#%'\"%$\"# X\"# X*# \" \\+7%.:\"\"2:3;+'%4#6\x9B# %$## 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\x9C! %"),
|
|||
|
peg$decode("!7T+N$!.8\"\"2839+-$7^+#%'\"%$\"# X\"# X*# \" \\+#%'\"%$\"# X\"# X"),
|
|||
|
peg$decode("!7U*) \"7\\*# \"7X+& 4!6\x9D! %"),
|
|||
|
peg$decode("! ]!7V+3$.J\"\"2J3K+#%'\"%$\"# X\"# X,>&!7V+3$.J\"\"2J3K+#%'\"%$\"# X\"# X\"+G$7W+=%.J\"\"2J3K*# \" \\+'%4#6\x9E# %$## X$\"# X\"# X"),
|
|||
|
peg$decode(" ]0\x9F\"\"1!3\xA0+,$,)&0\x9F\"\"1!3\xA0\"\"\" X"),
|
|||
|
peg$decode(" ]0\xA1\"\"1!3\xA2+,$,)&0\xA1\"\"1!3\xA2\"\"\" X"),
|
|||
|
peg$decode("!.r\"\"2r3s+A$7Y+7%.t\"\"2t3u+'%4#6\xA3# %$## 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 \"!.\xA4\"\"2\xA43\xA5+\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 \"!.\xA4\"\"2\xA43\xA5+\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 \"!.\xA4\"\"2\xA43\xA5+{$7Z+q%.8\"\"2839+a%7Z+W%.8\"\"2839+G%7Z+=%.8\"\"2839+-%7[+#%'(%$(# X$'# X$&# X$%# X$$# X$## X$\"# X\"# X*\u069D \"!.\xA4\"\"2\xA43\xA5+a$7Z+W%.8\"\"2839+G%7Z+=%.8\"\"2839+-%7[+#%'&%$&# X$%# X$$# X$## X$\"# X\"# X*\u0648 \"!.\xA4\"\"2\xA43\xA5+G$7Z+=%.8\"\"2839+-%7[+#%'$%$$# X$## X$\"# X\"# X*\u060D \"!.\xA4\"\"2\xA43\xA5+-$7[+#%'\"%$\"# X\"# X*\u05EC \"!.\xA4\"\"2\xA43\xA5+-$7Z+#%'\"%$\"# X\"# X*\u05CB \"!7Z+\xA5$.\xA4\"\"2\xA43\xA5+\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%.\xA4\"\"2\xA43\xA5+{%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%.\xA4\"\"2\xA43\xA5+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%.\xA4\"\"2\xA43\xA5+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*# \" \\+=%.\xA4\"\"2\xA43\xA5+-%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*# \" \\+=%.\xA4\"\"2\xA43\xA5+-%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%.\xA4\"\"2\xA43\xA5+#%'(%$(# X$'# X$&# X$%# X$$# X$## X$\"# X\"# X+& 4!6\xA6! %"),
|
|||
|
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\xA7' %$'# X$&# X$%# X$$# X$## X$\"# X\"# X"),
|
|||
|
peg$decode("!.\xA8\"\"2\xA83\xA9+3$0\xAA\"\"1!3\xAB+#%'\"%$\"# X\"# X*\xA0 \"!.\xAC\"\"2\xAC3\xAD+=$0\xAE\"\"1!3\xAF+-%7!+#%'#%$## X$\"# X\"# X*o \"!.\xB0\"\"2\xB03\xB1+7$7!+-%7!+#%'#%$## X$\"# X\"# X*D \"!0\xB2\"\"1!3\xB3+-$7!+#%'\"%$\"# X\"# X*# \"7!"),
|
|||
|
peg$decode("!!7!*# \" \\+c$7!*# \" \\+S%7!*# \" \\+C%7!*# \" \\+3%7!*# \" \\+#%'%%$%# X$$# X$## X$\"# X\"# X+' 4!6\xB4!! %"),
|
|||
|
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("!/\xB5\"\"1*3\xB6+b$/\xB7\"\"1#3\xB8*G \"/\xB9\"\"1#3\xBA*; \"/\xBB\"\"1$3\xBC*/ \"/\xBD\"\"1#3\xBE*# \"76+(%4\"6\xBF\"! %$\"# X\"# X"),
|
|||
|
peg$decode("!/\xC0\"\"1%3\xC1+J$/\xC2\"\"1%3\xC3*/ \"/\xC4\"\"1\"3\xC5*# \"76+(%4\"6\xC6\"! %$\"# X\"# X"),
|
|||
|
peg$decode("!/\xC7\"\"1'3\xC8+2$7\x8F+(%4\"6\xC9\"! %$\"# X\"# X"),
|
|||
|
peg$decode("!/\xCA\"\"1$3\xCB+2$7\xEC+(%4\"6\xCC\"! %$\"# X\"# X"),
|
|||
|
peg$decode("!/\xCD\"\"1&3\xCE+2$7T+(%4\"6\xCF\"! %$\"# X\"# X"),
|
|||
|
peg$decode("!/\xD0\"\"1\"3\xD1+R$!.>\"\"2>3?+-$76+#%'\"%$\"# X\"# X*# \" \\+'%4\"6\xD2\" %$\"# X\"# X"),
|
|||
|
peg$decode("!7h+T$!.>\"\"2>3?+-$7i+#%'\"%$\"# X\"# X*# \" \\+)%4\"6\xD3\"\"! %$\"# X\"# X"),
|
|||
|
peg$decode("! ]7j+&$,#&7j\"\"\" X+' 4!6\xD4!! %"),
|
|||
|
peg$decode("! ]7j+&$,#&7j\"\"\" X+' 4!6\xD5!! %"),
|
|||
|
peg$decode("7k*) \"7+*# \"7-"),
|
|||
|
peg$decode(".r\"\"2r3s*e \".t\"\"2t3u*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\xD6#\"\" %$## X$\"# X\"# X"),
|
|||
|
peg$decode(" ]7p*) \"7+*# \"7-+2$,/&7p*) \"7+*# \"7-\"\"\" X"),
|
|||
|
peg$decode(" ]7p*) \"7+*# \"7-,/&7p*) \"7+*# \"7-\""),
|
|||
|
peg$decode(".r\"\"2r3s*e \".t\"\"2t3u*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+C$.8\"\"2839+3%7u*# \"7x+#%'#%$## X$\"# X\"# X"),
|
|||
|
peg$decode("!7v*# \"7w+N$!.6\"\"2637+-$7\x83+#%'\"%$\"# X\"# X*# \" \\+#%'\"%$\"# X\"# X"),
|
|||
|
peg$decode("!.\xD7\"\"2\xD73\xD8+=$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\xD9\" %$\"# 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("!/\x98\"\"1#3\xDA+y$.4\"\"2435+i% ]7!+&$,#&7!\"\"\" X+P%.J\"\"2J3K+@% ]7!+&$,#&7!\"\"\" X+'%4%6\xDB% %$%# X$$# X$## X$\"# X\"# X"),
|
|||
|
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(".\xEC\"\"2\xEC3\xED"),
|
|||
|
peg$decode(".\xEE\"\"2\xEE3\xEF"),
|
|||
|
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\xF0! %"),
|
|||
|
peg$decode("!7\x84+K$7'+A%7\x91+7%7'+-%7\x93+#%'%%$%# X$$# X$## X$\"# X\"# X"),
|
|||
|
peg$decode("!7\x92+' 4!6\xF1!! %"),
|
|||
|
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\xF2! %"),
|
|||
|
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\xF3\" %$\"# X\"# X"),
|
|||
|
peg$decode("!7:*j \"!7\x97+_$ ]!7A+-$7\x97+#%'\"%$\"# X\"# X,8&!7A+-$7\x97+#%'\"%$\"# X\"# X\"+#%'\"%$\"# X\"# X+& 4!6\xF4! %"),
|
|||
|
peg$decode("!7L*# \"7\x98+c$ ]!7B+-$7\x9A+#%'\"%$\"# X\"# X,8&!7B+-$7\x9A+#%'\"%$\"# X\"# X\"+'%4\"6\xF5\" %$\"# 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\xF6!! %"),
|
|||
|
peg$decode("7\x9B*) \"7\x9C*# \"7\x9F"),
|
|||
|
peg$decode("!/\xF7\"\"1!3\xF8+<$7<+2%7\x9E+(%4#6\xF9#! %$## X$\"# X\"# X"),
|
|||
|
peg$decode("!/\xFA\"\"1'3\xFB+<$7<+2%7\x9D+(%4#6\xFC#! %$## X$\"# X\"# X"),
|
|||
|
peg$decode("! ]7!+&$,#&7!\"\"\" X+' 4!6\xFD!! %"),
|
|||
|
peg$decode("!.\xFE\"\"2\xFE3\xFF+x$!.J\"\"2J3K+S$7!*# \" \\+C%7!*# \" \\+3%7!*# \" \\+#%'$%$$# X$## X$\"# X\"# X*# \" \\+'%4\"6\u0100\" %$\"# X\"# X"),
|
|||
|
peg$decode("!76+N$!7<+-$7\xA0+#%'\"%$\"# X\"# X*# \" \\+)%4\"6\u0101\"\"! %$\"# 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("/\u0102\"\"1&3\u0103*G \"/\u0104\"\"1'3\u0105*; \"/\u0106\"\"1$3\u0107*/ \"/\u0108\"\"1%3\u0109*# \"76"),
|
|||
|
peg$decode("7\xA4*# \"7\x9F"),
|
|||
|
peg$decode("!/\u010A\"\"1(3\u010B+O$7<+E%/\u010C\"\"1(3\u010D*/ \"/\u010E\"\"1(3\u010F*# \"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\u0110!! %"),
|
|||
|
peg$decode("!7\xA8+& 4!6\u0111! %"),
|
|||
|
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("/\u0112\"\"1$3\u0113*S \"/\u0114\"\"1%3\u0115*G \"/\u0116\"\"1%3\u0117*; \"/\u0118\"\"1%3\u0119*/ \"/\u011A\"\"1+3\u011B*# \"7\xAC"),
|
|||
|
peg$decode("/\u011C\"\"1'3\u011D*/ \"/\u011E\"\"1)3\u011F*# \"7\xAC"),
|
|||
|
peg$decode("76*# \"7\xAD"),
|
|||
|
peg$decode("!/\u0120\"\"1\"3\u0121+-$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\u0122!! %"),
|
|||
|
peg$decode("!7\x9D+' 4!6\u0123!! %"),
|
|||
|
peg$decode("!7\xB5+d$ ]!7B+-$7\x9F+#%'\"%$\"# X\"# X,8&!7B+-$7\x9F+#%'\"%$\"# X\"# X\"+(%4\"6\u0124\"!!%$\"# 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\u0125\" %$\"# X\"# X"),
|
|||
|
peg$decode("7\xB8*# \"7\x9F"),
|
|||
|
peg$decode("!/\u0126\"\"1#3\u0127+<$7<+2%76+(%4#6\u0128#! %$## X$\"# X\"# X"),
|
|||
|
peg$decode("! ]7!+&$,#&7!\"\"\" X+' 4!6\u0129!! %"),
|
|||
|
peg$decode("!7\x9D+' 4!6\u012A!! %"),
|
|||
|
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\u012B% %$%# X$$# X$## X$\"# X\"# X"),
|
|||
|
peg$decode("7\xBD"),
|
|||
|
peg$decode("!/\u012C\"\"1&3\u012D+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("!/\u012E\"\"1%3\u012F+7$7<+-%7\xC2+#%'#%$## X$\"# X\"# X"),
|
|||
|
peg$decode("!7I+' 4!6\u0130!! %"),
|
|||
|
peg$decode("!/\u0131\"\"1&3\u0132+\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("!/\u0133\"\"1%3\u0134+7$7<+-%7\xC6+#%'#%$## X$\"# X\"# X"),
|
|||
|
peg$decode("!7I+' 4!6\u0135!! %"),
|
|||
|
peg$decode("!/\u0136\"\"1&3\u0137+<$7<+2%7I+(%4#6\u0138#! %$## X$\"# X\"# X"),
|
|||
|
peg$decode("!/\u0139\"\"1%3\u013A+_$7<+U%!/\u013B\"\"1$3\u013C+& 4!6\u013D! %*4 \"!/\u013E\"\"1%3\u013F+& 4!6\u0140! %+#%'#%$## X$\"# X\"# X"),
|
|||
|
peg$decode("!/\u0141\"\"1)3\u0142+T$7<+J%/\u0143\"\"1#3\u0144*/ \"/\u0145\"\"1(3\u0146*# \"76+(%4#6\u0147#! %$## X$\"# X\"# X"),
|
|||
|
peg$decode("!/\u0148\"\"1#3\u0149+\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("!/\u014A\"\"1(3\u014B*/ \"/\u014C\"\"1$3\u014D*# \"76+' 4!6\u014E!! %"),
|
|||
|
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\u014F!! %"),
|
|||
|
peg$decode("!7\xD0+c$ ]!7A+-$7\xD0+#%'\"%$\"# X\"# X,8&!7A+-$7\xD0+#%'\"%$\"# X\"# X\"+'%4\"6\u0150\" %$\"# X\"# X"),
|
|||
|
peg$decode("!7\x98+c$ ]!7B+-$7\x9F+#%'\"%$\"# X\"# X,8&!7B+-$7\x9F+#%'\"%$\"# X\"# X\"+'%4\"6\u0151\" %$\"# X\"# X"),
|
|||
|
peg$decode("!7L*# \"7\x98+c$ ]!7B+-$7\x9F+#%'\"%$\"# X\"# X,8&!7B+-$7\x9F+#%'\"%$\"# X\"# X\"+'%4\"6\u0152\" %$\"# X\"# X"),
|
|||
|
peg$decode("!76+_$ ]!7A+-$76+#%'\"%$\"# X\"# X,8&!7A+-$76+#%'\"%$\"# X\"# X\"+#%'\"%$\"# X\"# X"),
|
|||
|
peg$decode("!7\xD4+_$ ]!7A+-$7\xD4+#%'\"%$\"# X\"# X,8&!7A+-$7\xD4+#%'\"%$\"# 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\u0153!! %"),
|
|||
|
peg$decode("!7\xD7+_$ ]!7B+-$7\xD8+#%'\"%$\"# X\"# X,8&!7B+-$7\xD8+#%'\"%$\"# X\"# X\"+#%'\"%$\"# X\"# X"),
|
|||
|
peg$decode("!/\u0154\"\"1&3\u0155*; \"/\u0156\"\"1'3\u0157*/ \"/\u0158\"\"1*3\u0159*# \"76+& 4!6\u015A! %"),
|
|||
|
peg$decode("!/\u015B\"\"1&3\u015C+<$7<+2%7\xD9+(%4#6\u015D#! %$## X$\"# X\"# X*\x83 \"!/\xFA\"\"1'3\xFB+<$7<+2%7\x9D+(%4#6\u015E#! %$## X$\"# X\"# X*S \"!/\u015F\"\"1+3\u0160+<$7<+2%7\x9D+(%4#6\u0161#! %$## X$\"# X\"# X*# \"7\x9F"),
|
|||
|
peg$decode("/\u0162\"\"1+3\u0163*k \"/\u0164\"\"1)3\u0165*_ \"/\u0166\"\"1(3\u0167*S \"/\u0168\"\"1'3\u0169*G \"/\u016A\"\"1&3\u016B*; \"/\u016C\"\"1*3\u016D*/ \"/\u016E\"\"1)3\u016F*# \"76"),
|
|||
|
peg$decode("71*# \" \\"),
|
|||
|
peg$decode("!76+_$ ]!7A+-$76+#%'\"%$\"# X\"# X,8&!7A+-$76+#%'\"%$\"# X\"# X\"+#%'\"%$\"# X\"# X*# \" \\"),
|
|||
|
peg$decode("!7L*# \"7\x98+c$ ]!7B+-$7\xDD+#%'\"%$\"# X\"# X,8&!7B+-$7\xDD+#%'\"%$\"# X\"# X\"+'%4\"6\u0170\" %$\"# X\"# X"),
|
|||
|
peg$decode("7\xB8*# \"7\x9F"),
|
|||
|
peg$decode("!7\xDF+_$ ]!7A+-$7\xDF+#%'\"%$\"# X\"# X,8&!7A+-$7\xDF+#%'\"%$\"# X\"# X\"+#%'\"%$\"# X\"# X"),
|
|||
|
peg$decode("!7\xE6+s$7.+i%7\xE9+_% ]!7B+-$7\xE0+#%'\"%$\"# X\"# X,8&!7B+-$7\xE0+#%'\"%$\"# X\"# X\"+#%'$%$$# X$## X$\"# X\"# X"),
|
|||
|
peg$decode("7\xE1*; \"7\xE2*5 \"7\xE3*/ \"7\xE4*) \"7\xE5*# \"7\x9F"),
|
|||
|
peg$decode("!/\u0171\"\"1#3\u0172+<$7<+2%7\xEC+(%4#6\u0173#! %$## X$\"# X\"# X"),
|
|||
|
peg$decode("!/\u0174\"\"1%3\u0175+<$7<+2%7T+(%4#6\u0176#! %$## X$\"# X\"# X"),
|
|||
|
peg$decode("!/\u0177\"\"1(3\u0178+B$7<+8%7\\*# \"7Y+(%4#6\u0179#! %$## X$\"# X\"# X"),
|
|||
|
peg$decode("!/\u017A\"\"1&3\u017B+<$7<+2%76+(%4#6\u017C#! %$## X$\"# X\"# X"),
|
|||
|
peg$decode("!/\u017D\"\"1%3\u017E+T$!7<+5$ ]7!,#&7!\"+#%'\"%$\"# X\"# X*# \" \\+'%4\"6\u017F\" %$\"# X\"# X"),
|
|||
|
peg$decode("!7\xE7+K$7;+A%76+7%7;+-%7\xE8+#%'%%$%# X$$# X$## X$\"# X\"# X"),
|
|||
|
peg$decode("!/\x98\"\"1#3\xDA*# \"76+' 4!6\u0180!! %"),
|
|||
|
peg$decode("!/\xB7\"\"1#3\u0181*G \"/\xB9\"\"1#3\u0182*; \"/\xBD\"\"1#3\u0183*/ \"/\xBB\"\"1$3\u0184*# \"76+' 4!6\u0185!! %"),
|
|||
|
peg$decode("!7\xEA+H$!7C+-$7\xEB+#%'\"%$\"# X\"# X*# \" \\+#%'\"%$\"# X\"# X"),
|
|||
|
peg$decode("!7U*) \"7\\*# \"7X+& 4!6\u0186! %"),
|
|||
|
peg$decode("!!7!*# \" \\+c$7!*# \" \\+S%7!*# \" \\+C%7!*# \" \\+3%7!*# \" \\+#%'%%$%# X$$# X$## X$\"# X\"# X+' 4!6\u0187!! %"),
|
|||
|
peg$decode("!!7!+C$7!*# \" \\+3%7!*# \" \\+#%'#%$## X$\"# X\"# X+' 4!6\u0188!! %"),
|
|||
|
peg$decode("7\xBD"),
|
|||
|
peg$decode("!76+7$70+-%7\xEF+#%'#%$## X$\"# X\"# X"),
|
|||
|
peg$decode(" ]72*) \"74*# \"7.,/&72*) \"74*# \"7.\""),
|
|||
|
peg$decode(" ]7%,#&7%\""),
|
|||
|
peg$decode("!7\xF2+=$.8\"\"2839+-%7\xF3+#%'#%$## X$\"# X\"# X"),
|
|||
|
peg$decode("!/\u0189\"\"1%3\u018A*) \"/\u018B\"\"1$3\u018C+' 4!6\u018D!! %"),
|
|||
|
peg$decode("!7\xF4+N$!.8\"\"2839+-$7^+#%'\"%$\"# X\"# X*# \" \\+#%'\"%$\"# X\"# X"),
|
|||
|
peg$decode("!7\\*) \"7X*# \"7\x82+' 4!6\u018E!! %"),
|
|||
|
peg$decode("! ]7\xF6*) \"7-*# \"7\xF7,/&7\xF6*) \"7-*# \"7\xF7\"+& 4!6\u018F! %"),
|
|||
|
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\xF9+h$.8\"\"2839+X%7\xF3+N%!.\u0190\"\"2\u01903\u0191+-$7\xE8+#%'\"%$\"# X\"# X*# \" \\+#%'$%$$# X$## X$\"# X\"# X"),
|
|||
|
peg$decode("!/\u0192\"\"1%3\u0193*) \"/\u0194\"\"1$3\u0195+' 4!6\u018D!! %"),
|
|||
|
peg$decode("!7\xE8+Q$/\xB7\"\"1#3\xB8*7 \"/\xB9\"\"1#3\xBA*+ \" ]7+,#&7+\"+'%4\"6\u0196\" %$\"# X\"# X"),
|
|||
|
peg$decode("!.\u0197\"\"2\u01973\u0198+-$7\xFC+#%'\"%$\"# X\"# X"),
|
|||
|
peg$decode("!7\xFE+\x90$.F\"\"2F3G+\x80%7\xFD+v%.F\"\"2F3G+f%7\xFD+\\%.F\"\"2F3G+L%7\xFD+B%.F\"\"2F3G+2%7\xFF+(%4)6\u0199)!(%$)# X$(# X$'# X$&# X$%# X$$# X$## X$\"# X\"# X"),
|
|||
|
peg$decode("!7#+A$7#+7%7#+-%7#+#%'$%$$# X$## X$\"# X\"# X"),
|
|||
|
peg$decode("!7\xFD+-$7\xFD+#%'\"%$\"# X\"# X"),
|
|||
|
peg$decode("!7\xFD+7$7\xFD+-%7\xFD+#%'#%$## 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];
|
|||
|
}
|
|||
|
|
|||
|
var data = {};
|
|||
|
|
|||
|
peg$result = peg$parseRule(peg$startRuleIndex);
|
|||
|
|
|||
|
if (peg$result !== peg$FAILED && peg$currPos === input.length) {
|
|||
|
return data;
|
|||
|
} else {
|
|||
|
if (peg$result !== peg$FAILED && peg$currPos < input.length) {
|
|||
|
peg$fail({ type: "end", description: "end of input" });
|
|||
|
}
|
|||
|
|
|||
|
return -1;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
return {
|
|||
|
SyntaxError: SyntaxError,
|
|||
|
parse: function (input, startRule) {return parse(input, {startRule: startRule});}
|
|||
|
};
|
|||
|
};
|
|||
|
/* jshint ignore:end */
|
|||
|
|
|||
|
},{}],10:[function(_dereq_,module,exports){
|
|||
|
/**
|
|||
|
* @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 (window) {
|
|||
|
|
|||
|
var Hacks;
|
|||
|
|
|||
|
Hacks = {
|
|||
|
|
|||
|
Firefox: {
|
|||
|
/* Condition to detect if hacks are applicable */
|
|||
|
isFirefox: function () {
|
|||
|
return window.mozRTCPeerConnection !== undefined;
|
|||
|
},
|
|||
|
|
|||
|
cannotHandleRelayCandidates: function (message) {
|
|||
|
if (this.isFirefox() && message.body) {
|
|||
|
message.body = message.body.replace(/relay/g, 'host generation 0');
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
cannotHandleExtraWhitespace: function (message) {
|
|||
|
if (this.isFirefox() && message.body) {
|
|||
|
message.body = message.body.replace(/ \r\n/g, "\r\n");
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
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<mlines.length; i++) {
|
|||
|
|
|||
|
// If it has an i= line, check if the next line is the c= line
|
|||
|
if (mlines[i].toString().search(/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 IP 4 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;
|
|||
|
};
|
|||
|
|
|||
|
},{}],11:[function(_dereq_,module,exports){
|
|||
|
|
|||
|
module.exports = (function() {
|
|||
|
|
|||
|
var Logger = function(logger, category, label) {
|
|||
|
this.logger = logger;
|
|||
|
this.category = category;
|
|||
|
this.label = label;
|
|||
|
};
|
|||
|
|
|||
|
|
|||
|
Logger.prototype.debug = function(content) {
|
|||
|
this.logger.debug(this.category, this.label, content);
|
|||
|
};
|
|||
|
|
|||
|
Logger.prototype.log = function(content) {
|
|||
|
this.logger.log(this.category, this.label, content);
|
|||
|
};
|
|||
|
|
|||
|
Logger.prototype.warn = function(content) {
|
|||
|
this.logger.warn(this.category, this.label, content);
|
|||
|
};
|
|||
|
|
|||
|
Logger.prototype.error = function(content) {
|
|||
|
this.logger.error(this.category, this.label, content);
|
|||
|
};
|
|||
|
|
|||
|
return Logger;
|
|||
|
})();
|
|||
|
|
|||
|
},{}],12:[function(_dereq_,module,exports){
|
|||
|
|
|||
|
module.exports = function (window, Logger) {
|
|||
|
|
|||
|
// Console is not defined in ECMAScript, so just in case...
|
|||
|
var console = window.console || {
|
|||
|
debug: function () {},
|
|||
|
log: function () {},
|
|||
|
warn: function () {},
|
|||
|
error: function () {}
|
|||
|
};
|
|||
|
|
|||
|
var LoggerFactory = function() {
|
|||
|
var logger,
|
|||
|
levels = {
|
|||
|
'error': 0,
|
|||
|
'warn': 1,
|
|||
|
'log': 2,
|
|||
|
'debug': 3
|
|||
|
},
|
|||
|
|
|||
|
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) {
|
|||
|
var prefix = [];
|
|||
|
|
|||
|
prefix.push(new Date());
|
|||
|
|
|||
|
prefix.push(category);
|
|||
|
|
|||
|
if (label) {
|
|||
|
prefix.push(label);
|
|||
|
}
|
|||
|
|
|||
|
prefix.push('');
|
|||
|
|
|||
|
if (typeof content === 'string') {
|
|||
|
target.call(console, prefix.join(' | ') + content);
|
|||
|
} else {
|
|||
|
target.call(console, content);
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
LoggerFactory.prototype.debug = function(category, label, content) {
|
|||
|
if (this.level === 3) {
|
|||
|
if (this.builtinEnabled) {
|
|||
|
this.print(console.debug, category, label, content);
|
|||
|
}
|
|||
|
|
|||
|
if (this.connector) {
|
|||
|
this.connector('debug', category, label, content);
|
|||
|
}
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
LoggerFactory.prototype.log = function(category, label, content) {
|
|||
|
if (this.level >= 2) {
|
|||
|
if (this.builtinEnabled) {
|
|||
|
this.print(console.log, category, label, content);
|
|||
|
}
|
|||
|
|
|||
|
if (this.connector) {
|
|||
|
this.connector('log', category, label, content);
|
|||
|
}
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
LoggerFactory.prototype.warn = function(category, label, content) {
|
|||
|
if (this.level >= 1) {
|
|||
|
if (this.builtinEnabled) {
|
|||
|
this.print(console.warn, category, label, content);
|
|||
|
}
|
|||
|
|
|||
|
if (this.connector) {
|
|||
|
this.connector('warn', category, label, content);
|
|||
|
}
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
LoggerFactory.prototype.error = function(category, label, content) {
|
|||
|
if (this.builtinEnabled) {
|
|||
|
this.print(console.error,category, label, content);
|
|||
|
}
|
|||
|
|
|||
|
if (this.connector) {
|
|||
|
this.connector('error', 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;
|
|||
|
};
|
|||
|
|
|||
|
},{}],13:[function(_dereq_,module,exports){
|
|||
|
/**
|
|||
|
* @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 {Function} onSuccess called with the obtained local media description
|
|||
|
* @param {Function} onFailure
|
|||
|
* @param {Object} [mediaHint] A custom object describing the media to be used during this session.
|
|||
|
*/
|
|||
|
getDescription: {value: function getDescription (onSuccess, onFailure, mediaHint) {
|
|||
|
// keep jshint happy
|
|||
|
onSuccess = onSuccess;
|
|||
|
onFailure = onFailure;
|
|||
|
mediaHint = mediaHint;
|
|||
|
}},
|
|||
|
|
|||
|
/**
|
|||
|
* Message reception.
|
|||
|
* @param {String} type
|
|||
|
* @param {String} description
|
|||
|
* @param {Function} onSuccess
|
|||
|
* @param {Function} onFailure
|
|||
|
*/
|
|||
|
setDescription: {value: function setDescription (description, onSuccess, onFailure) {
|
|||
|
// keep jshint happy
|
|||
|
description = description;
|
|||
|
onSuccess = onSuccess;
|
|||
|
onFailure = onFailure;
|
|||
|
}}
|
|||
|
});
|
|||
|
|
|||
|
return MediaHandler;
|
|||
|
};
|
|||
|
|
|||
|
},{}],14:[function(_dereq_,module,exports){
|
|||
|
/**
|
|||
|
* @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, {
|
|||
|
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;
|
|||
|
};
|
|||
|
|
|||
|
},{}],15:[function(_dereq_,module,exports){
|
|||
|
/**
|
|||
|
* @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;
|
|||
|
}
|
|||
|
|
|||
|
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;
|
|||
|
}
|
|||
|
|
|||
|
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;
|
|||
|
};
|
|||
|
|
|||
|
},{}],16:[function(_dereq_,module,exports){
|
|||
|
module.exports = function (SIP) {
|
|||
|
|
|||
|
var RegisterContext;
|
|||
|
|
|||
|
RegisterContext = function (ua) {
|
|||
|
var params = {},
|
|||
|
regId = 1,
|
|||
|
events = [
|
|||
|
'registered',
|
|||
|
'unregistered'
|
|||
|
];
|
|||
|
|
|||
|
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="<urn:uuid:'+ ua.configuration.instanceId+'>"';
|
|||
|
}
|
|||
|
|
|||
|
// 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.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');
|
|||
|
this.initMoreEvents(events);
|
|||
|
};
|
|||
|
|
|||
|
RegisterContext.prototype = {
|
|||
|
register: function (options) {
|
|||
|
var self = this, extraHeaders;
|
|||
|
|
|||
|
// Handle Options
|
|||
|
options = options || {};
|
|||
|
extraHeaders = (options.extraHeaders || []).slice();
|
|||
|
extraHeaders.push('Contact: ' + this.contact + ';expires=' + this.expires);
|
|||
|
extraHeaders.push('Allow: ' + SIP.Utils.getAllowedMethods(this.ua));
|
|||
|
|
|||
|
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(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(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();
|
|||
|
},
|
|||
|
|
|||
|
close: function() {
|
|||
|
this.registered_before = this.registered;
|
|||
|
this.unregister();
|
|||
|
},
|
|||
|
|
|||
|
unregister: function(options) {
|
|||
|
var extraHeaders;
|
|||
|
|
|||
|
if(!this.registered) {
|
|||
|
this.logger.warn('already unregistered');
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
options = options || {};
|
|||
|
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;
|
|||
|
};
|
|||
|
|
|||
|
},{}],17:[function(_dereq_,module,exports){
|
|||
|
|
|||
|
/**
|
|||
|
* @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) && this.ua.configuration.password !== null) {
|
|||
|
|
|||
|
// 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 = new SIP.DigestAuthentication(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;
|
|||
|
};
|
|||
|
|
|||
|
},{}],18:[function(_dereq_,module,exports){
|
|||
|
(function (global){
|
|||
|
/**
|
|||
|
* @name SIP
|
|||
|
* @namespace
|
|||
|
*/
|
|||
|
module.exports = (function(window) {
|
|||
|
"use strict";
|
|||
|
|
|||
|
var SIP = {};
|
|||
|
|
|||
|
var pkg = _dereq_('../package.json');
|
|||
|
|
|||
|
Object.defineProperties(SIP, {
|
|||
|
version: {
|
|||
|
get: function(){ return pkg.version; }
|
|||
|
},
|
|||
|
name: {
|
|||
|
get: function(){ return pkg.title; }
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
_dereq_('./Utils.js')(SIP);
|
|||
|
var Logger = _dereq_('./Logger.js');
|
|||
|
SIP.LoggerFactory = _dereq_('./LoggerFactory.js')(window, Logger);
|
|||
|
_dereq_('./EventEmitter.js')(SIP);
|
|||
|
SIP.C = _dereq_('./Constants.js')(SIP.name, SIP.version);
|
|||
|
SIP.Exceptions = _dereq_('./Exceptions.js');
|
|||
|
SIP.Timers = _dereq_('./Timers.js')(window);
|
|||
|
_dereq_('./Transport.js')(SIP, window);
|
|||
|
_dereq_('./Parser.js')(SIP);
|
|||
|
_dereq_('./SIPMessage.js')(SIP);
|
|||
|
_dereq_('./URI.js')(SIP);
|
|||
|
_dereq_('./NameAddrHeader.js')(SIP);
|
|||
|
_dereq_('./Transactions.js')(SIP, window);
|
|||
|
var DialogRequestSender = _dereq_('./Dialog/RequestSender.js')(SIP, window);
|
|||
|
_dereq_('./Dialogs.js')(SIP, DialogRequestSender);
|
|||
|
_dereq_('./RequestSender.js')(SIP);
|
|||
|
_dereq_('./RegisterContext.js')(SIP, window);
|
|||
|
SIP.MediaHandler = _dereq_('./MediaHandler.js')(SIP.EventEmitter);
|
|||
|
_dereq_('./ClientContext.js')(SIP);
|
|||
|
_dereq_('./ServerContext.js')(SIP);
|
|||
|
var SessionDTMF = _dereq_('./Session/DTMF.js')(SIP);
|
|||
|
_dereq_('./Session.js')(SIP, window, SessionDTMF);
|
|||
|
_dereq_('./Subscription.js')(SIP, window);
|
|||
|
var WebRTCMediaHandler = _dereq_('./WebRTC/MediaHandler.js')(SIP);
|
|||
|
var WebRTCMediaStreamManager = _dereq_('./WebRTC/MediaStreamManager.js')(SIP);
|
|||
|
SIP.WebRTC = _dereq_('./WebRTC.js')(SIP.Utils, window, WebRTCMediaHandler, WebRTCMediaStreamManager);
|
|||
|
_dereq_('./UA.js')(SIP, window);
|
|||
|
SIP.Hacks = _dereq_('./Hacks.js')(window);
|
|||
|
_dereq_('./SanityCheck.js')(SIP);
|
|||
|
SIP.DigestAuthentication = _dereq_('./DigestAuthentication.js')(SIP.Utils);
|
|||
|
SIP.Grammar = _dereq_('./Grammar/dist/Grammar')(SIP);
|
|||
|
|
|||
|
return SIP;
|
|||
|
})((typeof window !== 'undefined') ? window : global);
|
|||
|
|
|||
|
}).call(this,typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
|
|||
|
},{"../package.json":1,"./ClientContext.js":2,"./Constants.js":3,"./Dialog/RequestSender.js":4,"./Dialogs.js":5,"./DigestAuthentication.js":6,"./EventEmitter.js":7,"./Exceptions.js":8,"./Grammar/dist/Grammar":9,"./Hacks.js":10,"./Logger.js":11,"./LoggerFactory.js":12,"./MediaHandler.js":13,"./NameAddrHeader.js":14,"./Parser.js":15,"./RegisterContext.js":16,"./RequestSender.js":17,"./SIPMessage.js":19,"./SanityCheck.js":20,"./ServerContext.js":21,"./Session.js":22,"./Session/DTMF.js":23,"./Subscription.js":24,"./Timers.js":25,"./Transactions.js":26,"./Transport.js":27,"./UA.js":28,"./URI.js":29,"./Utils.js":30,"./WebRTC.js":31,"./WebRTC/MediaHandler.js":32,"./WebRTC/MediaStreamManager.js":33}],19:[function(_dereq_,module,exports){
|
|||
|
/**
|
|||
|
* @fileoverview SIP Message
|
|||
|
*/
|
|||
|
|
|||
|
module.exports = function (SIP) {
|
|||
|
var
|
|||
|
OutgoingRequest,
|
|||
|
IncomingMessage,
|
|||
|
IncomingRequest,
|
|||
|
IncomingResponse;
|
|||
|
|
|||
|
/**
|
|||
|
* @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:
|
|||
|
* <br>
|
|||
|
* - 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;
|
|||
|
|
|||
|
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 = (params.to_displayName || params.to_displayName === 0) ? '"' + params.to_displayName + '" ' : '';
|
|||
|
to += '<' + (params.to_uri || ruri) + '>';
|
|||
|
to += params.to_tag ? ';tag=' + params.to_tag : '';
|
|||
|
this.to = new SIP.NameAddrHeader.parse(to);
|
|||
|
this.setHeader('to', to);
|
|||
|
|
|||
|
// From
|
|||
|
if (params.from_displayName || params.from_displayName === 0) {
|
|||
|
from = '"' + params.from_displayName + '" ';
|
|||
|
} else if (ua.configuration.displayName) {
|
|||
|
from = '"' + ua.configuration.displayName + '" ';
|
|||
|
} else {
|
|||
|
from = '';
|
|||
|
}
|
|||
|
from += '<' + (params.from_uri || ua.configuration.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, supported = [];
|
|||
|
|
|||
|
msg += this.method + ' ' + 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';
|
|||
|
}
|
|||
|
|
|||
|
//Supported
|
|||
|
if (this.method === SIP.C.REGISTER) {
|
|||
|
supported.push('path', 'gruu');
|
|||
|
} else if (this.method === SIP.C.INVITE &&
|
|||
|
(this.ua.contact.pub_gruu || this.ua.contact.temp_gruu)) {
|
|||
|
supported.push('gruu');
|
|||
|
}
|
|||
|
|
|||
|
if (this.ua.configuration.rel100 === SIP.C.supported.SUPPORTED) {
|
|||
|
supported.push('100rel');
|
|||
|
}
|
|||
|
|
|||
|
supported.push('outbound');
|
|||
|
|
|||
|
msg += 'Supported: ' + supported +'\r\n';
|
|||
|
msg += 'User-Agent: ' + this.ua.configuration.userAgentString +'\r\n';
|
|||
|
|
|||
|
if(this.body) {
|
|||
|
length = SIP.Utils.str_utf8_length(this.body);
|
|||
|
msg += 'Content-Length: ' + length + '\r\n\r\n';
|
|||
|
msg += this.body;
|
|||
|
} 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,
|
|||
|
supported = [],
|
|||
|
to = this.getHeader('To'),
|
|||
|
r = 0,
|
|||
|
v = 0;
|
|||
|
|
|||
|
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 = reason || SIP.C.REASON_PHRASE[code] || '';
|
|||
|
extraHeaders = (extraHeaders || []).slice();
|
|||
|
|
|||
|
response = 'SIP/2.0 ' + code + ' ' + reason + '\r\n';
|
|||
|
|
|||
|
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';
|
|||
|
}
|
|||
|
|
|||
|
//Supported
|
|||
|
if (this.method === SIP.C.INVITE &&
|
|||
|
(this.ua.contact.pub_gruu || this.ua.contact.temp_gruu)) {
|
|||
|
supported.push('gruu');
|
|||
|
}
|
|||
|
|
|||
|
if (this.ua.configuration.rel100 === SIP.C.supported.SUPPORTED) {
|
|||
|
supported.push('100rel');
|
|||
|
}
|
|||
|
|
|||
|
supported.push('outbound');
|
|||
|
|
|||
|
response += 'Supported: ' + supported + '\r\n';
|
|||
|
|
|||
|
if(body) {
|
|||
|
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 {
|
|||
|
response += 'Content-Length: ' + 0 + '\r\n\r\n';
|
|||
|
}
|
|||
|
|
|||
|
this.server_transaction.receiveResponse(code, response, 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;
|
|||
|
|
|||
|
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 = reason || SIP.C.REASON_PHRASE[code] || '';
|
|||
|
|
|||
|
response = 'SIP/2.0 ' + code + ' ' + reason + '\r\n';
|
|||
|
|
|||
|
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 += '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;
|
|||
|
};
|
|||
|
|
|||
|
},{}],20:[function(_dereq_,module,exports){
|
|||
|
/**
|
|||
|
* @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,
|
|||
|
logger,
|
|||
|
message, ua, transport,
|
|||
|
requests = [],
|
|||
|
responses = [],
|
|||
|
all = [];
|
|||
|
|
|||
|
/*
|
|||
|
* 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() {
|
|||
|
if(!message.ruri || message.ruri.scheme !== 'sip') {
|
|||
|
reply(416);
|
|||
|
return false;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function rfc3261_16_3_4() {
|
|||
|
if(!message.to_tag) {
|
|||
|
if(message.call_id.substr(0, 5) === ua.configuration.sipjsId) {
|
|||
|
reply(482);
|
|||
|
return false;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function rfc3261_18_3_request() {
|
|||
|
var len = SIP.Utils.str_utf8_length(message.body),
|
|||
|
contentLength = message.getHeader('content-length');
|
|||
|
|
|||
|
if(len < contentLength) {
|
|||
|
reply(400);
|
|||
|
return false;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function rfc3261_8_2_2_2() {
|
|||
|
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);
|
|||
|
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);
|
|||
|
return false;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// Sanity Check functions for responses
|
|||
|
function rfc3261_8_1_3_3() {
|
|||
|
if(message.getHeaders('via').length > 1) {
|
|||
|
logger.warn('More than one Via header field present in the response. Dropping the response');
|
|||
|
return false;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function rfc3261_18_1_2() {
|
|||
|
var viaHost = ua.configuration.viaHost;
|
|||
|
if(message.via.host !== viaHost || message.via.port !== undefined) {
|
|||
|
logger.warn('Via sent-by in the response does not match UA Via host value. Dropping the response');
|
|||
|
return false;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function rfc3261_18_3_response() {
|
|||
|
var
|
|||
|
len = SIP.Utils.str_utf8_length(message.body),
|
|||
|
contentLength = message.getHeader('content-length');
|
|||
|
|
|||
|
if(len < contentLength) {
|
|||
|
logger.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() {
|
|||
|
var
|
|||
|
mandatoryHeaders = ['from', 'to', 'call_id', 'cseq', 'via'],
|
|||
|
idx = mandatoryHeaders.length;
|
|||
|
|
|||
|
while(idx--) {
|
|||
|
if(!message.hasHeader(mandatoryHeaders[idx])) {
|
|||
|
logger.warn('Missing mandatory header field : '+ mandatoryHeaders[idx] +'. Dropping the response');
|
|||
|
return false;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// Reply
|
|||
|
function reply(status_code) {
|
|||
|
var to,
|
|||
|
response = "SIP/2.0 " + status_code + " " + SIP.C.REASON_PHRASE[status_code] + "\r\n",
|
|||
|
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);
|
|||
|
}
|
|||
|
|
|||
|
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(m, u, t) {
|
|||
|
var len, pass;
|
|||
|
|
|||
|
message = m;
|
|||
|
ua = u;
|
|||
|
transport = t;
|
|||
|
|
|||
|
logger = ua.getLogger('sip.sanitycheck');
|
|||
|
|
|||
|
len = all.length;
|
|||
|
while(len--) {
|
|||
|
pass = all[len](message);
|
|||
|
if(pass === false) {
|
|||
|
return false;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if(message instanceof SIP.IncomingRequest) {
|
|||
|
len = requests.length;
|
|||
|
while(len--) {
|
|||
|
pass = requests[len](message);
|
|||
|
if(pass === false) {
|
|||
|
return false;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
else if(message instanceof SIP.IncomingResponse) {
|
|||
|
len = responses.length;
|
|||
|
while(len--) {
|
|||
|
pass = responses[len](message);
|
|||
|
if(pass === false) {
|
|||
|
return false;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
//Everything is OK
|
|||
|
return true;
|
|||
|
};
|
|||
|
|
|||
|
SIP.sanityCheck = sanityCheck;
|
|||
|
};
|
|||
|
|
|||
|
},{}],21:[function(_dereq_,module,exports){
|
|||
|
module.exports = function (SIP) {
|
|||
|
var ServerContext;
|
|||
|
|
|||
|
ServerContext = function (ua, request) {
|
|||
|
var events = [
|
|||
|
'progress',
|
|||
|
'accepted',
|
|||
|
'rejected',
|
|||
|
'failed'
|
|||
|
];
|
|||
|
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;
|
|||
|
|
|||
|
this.initEvents(events);
|
|||
|
};
|
|||
|
|
|||
|
ServerContext.prototype = new SIP.EventEmitter();
|
|||
|
|
|||
|
ServerContext.prototype.progress = function (options) {
|
|||
|
options = options || {};
|
|||
|
var
|
|||
|
statusCode = options.statusCode || 180,
|
|||
|
reasonPhrase = options.reasonPhrase || SIP.C.REASON_PHRASE[statusCode],
|
|||
|
extraHeaders = (options.extraHeaders || []).slice(),
|
|||
|
body = options.body,
|
|||
|
response;
|
|||
|
|
|||
|
if (statusCode < 100 || statusCode > 199) {
|
|||
|
throw new TypeError('Invalid statusCode: ' + statusCode);
|
|||
|
}
|
|||
|
response = this.request.reply(statusCode, reasonPhrase, extraHeaders, body);
|
|||
|
this.emit('progress', response, reasonPhrase);
|
|||
|
|
|||
|
return this;
|
|||
|
};
|
|||
|
|
|||
|
ServerContext.prototype.accept = function (options) {
|
|||
|
options = options || {};
|
|||
|
var
|
|||
|
statusCode = options.statusCode || 200,
|
|||
|
reasonPhrase = options.reasonPhrase || SIP.C.REASON_PHRASE[statusCode],
|
|||
|
extraHeaders = (options.extraHeaders || []).slice(),
|
|||
|
body = options.body,
|
|||
|
response;
|
|||
|
|
|||
|
if (statusCode < 200 || statusCode > 299) {
|
|||
|
throw new TypeError('Invalid statusCode: ' + statusCode);
|
|||
|
}
|
|||
|
response = this.request.reply(statusCode, reasonPhrase, extraHeaders, body);
|
|||
|
this.emit('accepted', response, reasonPhrase);
|
|||
|
|
|||
|
return this;
|
|||
|
};
|
|||
|
|
|||
|
ServerContext.prototype.reject = function (options) {
|
|||
|
options = options || {};
|
|||
|
var
|
|||
|
statusCode = options.statusCode || 480,
|
|||
|
reasonPhrase = options.reasonPhrase || SIP.C.REASON_PHRASE[statusCode],
|
|||
|
extraHeaders = (options.extraHeaders || []).slice(),
|
|||
|
body = options.body,
|
|||
|
response;
|
|||
|
|
|||
|
if (statusCode < 300 || statusCode > 699) {
|
|||
|
throw new TypeError('Invalid statusCode: ' + statusCode);
|
|||
|
}
|
|||
|
response = this.request.reply(statusCode, reasonPhrase, extraHeaders, body);
|
|||
|
this.emit('rejected', response, reasonPhrase);
|
|||
|
this.emit('failed', response, reasonPhrase);
|
|||
|
|
|||
|
return this;
|
|||
|
};
|
|||
|
|
|||
|
ServerContext.prototype.reply = function (options) {
|
|||
|
options = options || {};
|
|||
|
var
|
|||
|
statusCode = options.statusCode,
|
|||
|
reasonPhrase = options.reasonPhrase,
|
|||
|
extraHeaders = (options.extraHeaders || []).slice(),
|
|||
|
body = options.body;
|
|||
|
|
|||
|
this.request.reply(statusCode, reasonPhrase, extraHeaders, body);
|
|||
|
|
|||
|
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;
|
|||
|
};
|
|||
|
|
|||
|
},{}],22:[function(_dereq_,module,exports){
|
|||
|
module.exports = function (SIP, window, DTMF) {
|
|||
|
|
|||
|
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) {
|
|||
|
var events = [
|
|||
|
'connecting',
|
|||
|
'terminated',
|
|||
|
'dtmf',
|
|||
|
'invite',
|
|||
|
'cancel',
|
|||
|
'refer',
|
|||
|
'bye',
|
|||
|
'hold',
|
|||
|
'unhold',
|
|||
|
'muted',
|
|||
|
'unmuted'
|
|||
|
];
|
|||
|
|
|||
|
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<length; idx++) {
|
|||
|
if (this.actions[idx].name === name) {
|
|||
|
return true;
|
|||
|
}
|
|||
|
}
|
|||
|
return false;
|
|||
|
},
|
|||
|
|
|||
|
shift: function() {
|
|||
|
return this.actions.shift();
|
|||
|
},
|
|||
|
|
|||
|
push: function(name) {
|
|||
|
this.actions.push({
|
|||
|
name: name
|
|||
|
});
|
|||
|
},
|
|||
|
|
|||
|
pop: function(name) {
|
|||
|
var
|
|||
|
idx = 0,
|
|||
|
length = this.actions.length;
|
|||
|
|
|||
|
for (idx; idx<length; idx++) {
|
|||
|
if (this.actions[idx].name === name) {
|
|||
|
this.actions.splice(idx,1);
|
|||
|
length --;
|
|||
|
idx--;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
this.early_sdp = null;
|
|||
|
this.rel100 = SIP.C.supported.UNSUPPORTED;
|
|||
|
|
|||
|
this.initMoreEvents(events);
|
|||
|
};
|
|||
|
|
|||
|
Session.prototype = {
|
|||
|
dtmf: function(tones, options) {
|
|||
|
var tone, dtmfs = [],
|
|||
|
self = this;
|
|||
|
|
|||
|
options = options || {};
|
|||
|
|
|||
|
if (tones === undefined) {
|
|||
|
throw new TypeError('Not enough arguments');
|
|||
|
}
|
|||
|
|
|||
|
// Check Session Status
|
|||
|
if (this.status !== C.STATUS_CONFIRMED && this.status !== C.STATUS_WAITING_FOR_ACK) {
|
|||
|
throw new SIP.Exceptions.InvalidStateError(this.status);
|
|||
|
}
|
|||
|
|
|||
|
// Check tones
|
|||
|
if (!tones || (typeof tones !== 'string' && typeof tones !== 'number') || !tones.toString().match(/^[0-9A-D#*,]+$/i)) {
|
|||
|
throw new TypeError('Invalid tones: '+ tones);
|
|||
|
}
|
|||
|
|
|||
|
tones = tones.toString().split('');
|
|||
|
|
|||
|
while (tones.length > 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 = options || {};
|
|||
|
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(), originalTarget;
|
|||
|
|
|||
|
if (target === undefined) {
|
|||
|
throw new TypeError('Not enough arguments');
|
|||
|
} else if (target instanceof SIP.InviteServerContext || target instanceof SIP.InviteClientContext) {
|
|||
|
//Attended Transfer
|
|||
|
// B.transfer(C)
|
|||
|
extraHeaders.push('Contact: '+ this.contact);
|
|||
|
extraHeaders.push('Allow: '+ SIP.Utils.getAllowedMethods(this.ua));
|
|||
|
extraHeaders.push('Refer-To: <' + 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
|
|||
|
|
|||
|
// Check Session Status
|
|||
|
if (this.status !== C.STATUS_CONFIRMED) {
|
|||
|
throw new SIP.Exceptions.InvalidStateError(this.status);
|
|||
|
}
|
|||
|
|
|||
|
// 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.Utils.getAllowedMethods(this.ua));
|
|||
|
extraHeaders.push('Refer-To: '+ target);
|
|||
|
}
|
|||
|
|
|||
|
// Send the request
|
|||
|
return this.
|
|||
|
sendRequest(SIP.C.REFER, {
|
|||
|
extraHeaders: extraHeaders,
|
|||
|
body: options.body,
|
|||
|
receiveResponse: function() {}
|
|||
|
}).
|
|||
|
terminate();
|
|||
|
},
|
|||
|
|
|||
|
followRefer: function followRefer (callback) {
|
|||
|
return function referListener (callback, request) {
|
|||
|
SIP.Hacks.Chrome.getsConfusedAboutGUM(this);
|
|||
|
|
|||
|
/*
|
|||
|
Harmless race condition. Both sides of REFER
|
|||
|
may send a BYE, but in the end the dialogs are destroyed.
|
|||
|
*/
|
|||
|
var referSession = this.ua.invite(request.parseHeader('refer-to').uri, {
|
|||
|
media: this.mediaHint
|
|||
|
});
|
|||
|
|
|||
|
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
|
|||
|
if (this.checkEvent(method.toLowerCase())) {
|
|||
|
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() {
|
|||
|
|
|||
|
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({
|
|||
|
mangle: function(body){
|
|||
|
|
|||
|
// Don't receive media
|
|||
|
// TODO - This will break for media streams with different directions.
|
|||
|
if (!(/a=(sendrecv|sendonly|recvonly|inactive)/).test(body)) {
|
|||
|
body = body.replace(/(m=[^\r]*\r\n)/g, '$1a=sendonly\r\n');
|
|||
|
} else {
|
|||
|
body = body.replace(/a=sendrecv\r\n/g, 'a=sendonly\r\n');
|
|||
|
body = body.replace(/a=recvonly\r\n/g, 'a=inactive\r\n');
|
|||
|
}
|
|||
|
|
|||
|
return body;
|
|||
|
}
|
|||
|
});
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Unhold
|
|||
|
*/
|
|||
|
unhold: function() {
|
|||
|
|
|||
|
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();
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* isOnHold
|
|||
|
*/
|
|||
|
isOnHold: function() {
|
|||
|
return {
|
|||
|
local: this.local_hold,
|
|||
|
remote: this.remote_hold
|
|||
|
};
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* In dialog INVITE Reception
|
|||
|
* @private
|
|||
|
*/
|
|||
|
receiveReinvite: function(request) {
|
|||
|
var self = this,
|
|||
|
contentType = request.getHeader('Content-Type'),
|
|||
|
hold = true;
|
|||
|
|
|||
|
if (request.body) {
|
|||
|
if (contentType !== 'application/sdp') {
|
|||
|
this.logger.warn('invalid Content-Type');
|
|||
|
request.reply(415);
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
// Are we holding?
|
|||
|
hold = (/a=(sendonly|inactive)/).test(request.body);
|
|||
|
|
|||
|
this.mediaHandler.setDescription(
|
|||
|
request.body,
|
|||
|
/*
|
|||
|
* onSuccess
|
|||
|
* SDP Offer is valid
|
|||
|
*/
|
|||
|
function() {
|
|||
|
self.mediaHandler.getDescription(
|
|||
|
function(body) {
|
|||
|
request.reply(200, null, ['Contact: ' + self.contact], body,
|
|||
|
function() {
|
|||
|
self.status = C.STATUS_WAITING_FOR_ACK;
|
|||
|
self.setInvite2xxTimer(request, body);
|
|||
|
self.setACKTimer();
|
|||
|
|
|||
|
if (self.remote_hold && !hold) {
|
|||
|
self.onunhold('remote');
|
|||
|
} else if (!self.remote_hold && hold) {
|
|||
|
self.onhold('remote');
|
|||
|
}
|
|||
|
});
|
|||
|
},
|
|||
|
function() {
|
|||
|
request.reply(500);
|
|||
|
},
|
|||
|
self.mediaHint
|
|||
|
);
|
|||
|
},
|
|||
|
/*
|
|||
|
* onFailure
|
|||
|
* Bad media description
|
|||
|
*/
|
|||
|
function(e) {
|
|||
|
self.logger.error(e);
|
|||
|
request.reply(488);
|
|||
|
}
|
|||
|
);
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
sendReinvite: function(options) {
|
|||
|
options = options || {};
|
|||
|
|
|||
|
var
|
|||
|
self = this,
|
|||
|
extraHeaders = (options.extraHeaders || []).slice(),
|
|||
|
eventHandlers = options.eventHandlers || {},
|
|||
|
mangle = options.mangle || null;
|
|||
|
|
|||
|
if (eventHandlers.succeeded) {
|
|||
|
this.reinviteSucceeded = eventHandlers.succeeded;
|
|||
|
} else {
|
|||
|
this.reinviteSucceeded = function(){
|
|||
|
SIP.Timers.clearTimeout(self.timers.ackTimer);
|
|||
|
SIP.Timers.clearTimeout(self.timers.invite2xxTimer);
|
|||
|
self.status = C.STATUS_CONFIRMED;
|
|||
|
};
|
|||
|
}
|
|||
|
if (eventHandlers.failed) {
|
|||
|
this.reinviteFailed = eventHandlers.failed;
|
|||
|
} else {
|
|||
|
this.reinviteFailed = function(){};
|
|||
|
}
|
|||
|
|
|||
|
extraHeaders.push('Contact: ' + this.contact);
|
|||
|
extraHeaders.push('Allow: '+ SIP.Utils.getAllowedMethods(this.ua));
|
|||
|
extraHeaders.push('Content-Type: application/sdp');
|
|||
|
|
|||
|
this.receiveResponse = this.receiveReinviteResponse;
|
|||
|
//REVISIT
|
|||
|
this.mediaHandler.getDescription(
|
|||
|
function(body){
|
|||
|
if (mangle) {
|
|||
|
body = mangle(body);
|
|||
|
}
|
|||
|
|
|||
|
self.dialog.sendRequest(self, SIP.C.INVITE, {
|
|||
|
extraHeaders: extraHeaders,
|
|||
|
body: body
|
|||
|
});
|
|||
|
},
|
|||
|
function() {
|
|||
|
if (self.isReadyToReinvite()) {
|
|||
|
self.onReadyToReinvite();
|
|||
|
}
|
|||
|
self.reinviteFailed();
|
|||
|
},
|
|||
|
self.mediaHint
|
|||
|
);
|
|||
|
},
|
|||
|
|
|||
|
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');
|
|||
|
// Switch these two lines to try re-INVITEs:
|
|||
|
//this.receiveReinvite(request);
|
|||
|
request.reply(488, null, ['Warning: 399 sipjs "Cannot update media description"']);
|
|||
|
}
|
|||
|
break;
|
|||
|
case SIP.C.INFO:
|
|||
|
if(this.status === C.STATUS_CONFIRMED || this.status === C.STATUS_WAITING_FOR_ACK) {
|
|||
|
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');
|
|||
|
request.reply(202, 'Accepted');
|
|||
|
var
|
|||
|
hasReferListener = this.checkListener('refer'),
|
|||
|
notifyBody = hasReferListener ?
|
|||
|
'SIP/2.0 100 Trying' :
|
|||
|
// RFC 3515.2.4.2: 'the UA MAY decline the request.'
|
|||
|
'SIP/2.0 603 Declined'
|
|||
|
;
|
|||
|
|
|||
|
this.sendRequest(SIP.C.NOTIFY, {
|
|||
|
extraHeaders:[
|
|||
|
'Event: refer',
|
|||
|
'Subscription-State: terminated',
|
|||
|
'Content-Type: message/sipfrag'
|
|||
|
],
|
|||
|
body: notifyBody,
|
|||
|
receiveResponse: function() {}
|
|||
|
});
|
|||
|
|
|||
|
if (hasReferListener) {
|
|||
|
this.emit('refer', request);
|
|||
|
}
|
|||
|
}
|
|||
|
break;
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Reception of Response for in-dialog INVITE
|
|||
|
* @private
|
|||
|
*/
|
|||
|
receiveReinviteResponse: function(response) {
|
|||
|
var self = this,
|
|||
|
contentType = response.getHeader('Content-Type');
|
|||
|
|
|||
|
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(!response.body) {
|
|||
|
this.reinviteFailed();
|
|||
|
break;
|
|||
|
} else if (contentType !== 'application/sdp') {
|
|||
|
this.reinviteFailed();
|
|||
|
break;
|
|||
|
}
|
|||
|
|
|||
|
//REVISIT
|
|||
|
this.mediaHandler.setDescription(
|
|||
|
response.body,
|
|||
|
/*
|
|||
|
* onSuccess
|
|||
|
* SDP Answer fits with Offer.
|
|||
|
*/
|
|||
|
function() {
|
|||
|
self.reinviteSucceeded();
|
|||
|
},
|
|||
|
/*
|
|||
|
* onFailure
|
|||
|
* SDP Answer does not fit the Offer.
|
|||
|
*/
|
|||
|
function() {
|
|||
|
self.reinviteFailed();
|
|||
|
}
|
|||
|
);
|
|||
|
break;
|
|||
|
default:
|
|||
|
this.reinviteFailed();
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
acceptAndTerminate: function(response, status_code, reason_phrase) {
|
|||
|
var extraHeaders = [];
|
|||
|
|
|||
|
if (status_code) {
|
|||
|
reason_phrase = reason_phrase || SIP.C.REASON_PHRASE[status_code] || '';
|
|||
|
extraHeaders.push('Reason: SIP ;cause=' + status_code + '; text="' + 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, body) {
|
|||
|
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');
|
|||
|
|
|||
|
request.reply(200, null, ['Contact: ' + self.contact], body);
|
|||
|
|
|||
|
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.terminated(null, SIP.C.causes.CONNECTION_ERROR);
|
|||
|
} else if (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);
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
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);
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* @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) {
|
|||
|
this.close();
|
|||
|
return this.emit('failed', response, cause);
|
|||
|
},
|
|||
|
|
|||
|
rejected: function(response, cause) {
|
|||
|
this.close();
|
|||
|
return this.emit('rejected',
|
|||
|
response || null,
|
|||
|
cause
|
|||
|
);
|
|||
|
},
|
|||
|
|
|||
|
canceled: function() {
|
|||
|
this.close();
|
|||
|
return this.emit('cancel');
|
|||
|
},
|
|||
|
|
|||
|
accepted: function(response, cause) {
|
|||
|
cause = cause || (response && SIP.C.REASON_PHRASE[response.status_code]) || '';
|
|||
|
|
|||
|
this.startTime = new Date();
|
|||
|
|
|||
|
return this.emit('accepted', response, cause);
|
|||
|
},
|
|||
|
|
|||
|
terminated: function(message, cause) {
|
|||
|
this.endTime = new Date();
|
|||
|
|
|||
|
this.close();
|
|||
|
return this.emit('terminated', {
|
|||
|
message: message || null,
|
|||
|
cause: cause || null
|
|||
|
});
|
|||
|
},
|
|||
|
|
|||
|
connecting: function(request) {
|
|||
|
return this.emit('connecting', { request: request });
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
Session.C = C;
|
|||
|
SIP.Session = Session;
|
|||
|
|
|||
|
|
|||
|
InviteServerContext = function(ua, request) {
|
|||
|
var expires,
|
|||
|
self = this,
|
|||
|
contentType = request.getHeader('Content-Type'),
|
|||
|
contentDisp = request.getHeader('Content-Disposition');
|
|||
|
|
|||
|
// Check body and content type
|
|||
|
if ((!contentDisp && contentType !== 'application/sdp') || (contentDisp && contentDisp.indexOf('render') >= 0)) {
|
|||
|
this.renderbody = request.body;
|
|||
|
this.rendertype = contentType;
|
|||
|
} else if (contentType !== 'application/sdp' && (contentDisp && contentDisp === 'session')) {
|
|||
|
request.reply(415);
|
|||
|
//TODO: instead of 415, pass off to the media handler, who can then decide if we can use it
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
//TODO: move this into media handler
|
|||
|
SIP.Hacks.Firefox.cannotHandleRelayCandidates(request);
|
|||
|
SIP.Hacks.Firefox.cannotHandleExtraWhitespace(request);
|
|||
|
|
|||
|
SIP.Utils.augment(this, SIP.ServerContext, [ua, request]);
|
|||
|
SIP.Utils.augment(this, SIP.Session, [ua.configuration.mediaHandlerFactory]);
|
|||
|
|
|||
|
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;
|
|||
|
}
|
|||
|
|
|||
|
//Initialize Media Session
|
|||
|
this.mediaHandler = this.mediaHandlerFactory(this, {
|
|||
|
RTCConstraints: {"optional": [{'DtlsSrtpKeyAgreement': 'true'}]}
|
|||
|
});
|
|||
|
|
|||
|
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.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);
|
|||
|
}
|
|||
|
}, expires);
|
|||
|
}
|
|||
|
|
|||
|
self.emit('invite',request);
|
|||
|
}
|
|||
|
|
|||
|
if (!request.body || this.renderbody) {
|
|||
|
SIP.Timers.setTimeout(fireNewSession, 0);
|
|||
|
} else {
|
|||
|
this.hasOffer = true;
|
|||
|
this.mediaHandler.setDescription(
|
|||
|
request.body,
|
|||
|
/*
|
|||
|
* onSuccess
|
|||
|
* SDP Offer is valid. Fire UA newRTCSession
|
|||
|
*/
|
|||
|
fireNewSession,
|
|||
|
/*
|
|||
|
* onFailure
|
|||
|
* Bad media description
|
|||
|
*/
|
|||
|
function(e) {
|
|||
|
self.logger.warn('invalid SDP');
|
|||
|
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.apply(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.request(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.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(),
|
|||
|
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;
|
|||
|
}
|
|||
|
|
|||
|
function do100rel() {
|
|||
|
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(
|
|||
|
// Success
|
|||
|
function succ(body) {
|
|||
|
if (this.isCanceled || this.status === C.STATUS_TERMINATED) {
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
this.early_sdp = 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, body);
|
|||
|
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, body);
|
|||
|
this.emit('progress', response, reasonPhrase);
|
|||
|
}.bind(this),
|
|||
|
|
|||
|
// Failure
|
|||
|
function fail() {
|
|||
|
this.failed(null, SIP.C.causes.WEBRTC_ERROR);
|
|||
|
}.bind(this),
|
|||
|
|
|||
|
// Media hint:
|
|||
|
options.media);
|
|||
|
} // end do100rel
|
|||
|
|
|||
|
function normalReply() {
|
|||
|
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 = options || {};
|
|||
|
|
|||
|
SIP.Utils.optionsOverride(options, 'media', 'mediaConstraints', true, this.logger, this.ua.configuration.media);
|
|||
|
this.mediaHint = options.media;
|
|||
|
|
|||
|
// 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,
|
|||
|
sdpCreationSucceeded = function(body) {
|
|||
|
var
|
|||
|
response,
|
|||
|
// run for reply success callback
|
|||
|
replySucceeded = function() {
|
|||
|
self.status = C.STATUS_WAITING_FOR_ACK;
|
|||
|
|
|||
|
self.setInvite2xxTimer(request, body);
|
|||
|
self.setACKTimer();
|
|||
|
},
|
|||
|
|
|||
|
// run for reply failure callback
|
|||
|
replyFailed = function() {
|
|||
|
self.failed(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);
|
|||
|
|
|||
|
if(!self.hasOffer) {
|
|||
|
self.hasOffer = true;
|
|||
|
} else {
|
|||
|
self.hasAnswer = true;
|
|||
|
}
|
|||
|
response = request.reply(200, null, extraHeaders,
|
|||
|
body,
|
|||
|
replySucceeded,
|
|||
|
replyFailed
|
|||
|
);
|
|||
|
if (self.status !== C.STATUS_TERMINATED) { // Didn't fail
|
|||
|
self.accepted(response, SIP.C.REASON_PHRASE[200]);
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
sdpCreationFailed = function() {
|
|||
|
if (self.status === C.STATUS_TERMINATED) {
|
|||
|
return;
|
|||
|
}
|
|||
|
// TODO - fail out on error
|
|||
|
//response = request.reply(480);
|
|||
|
//self.failed(response, SIP.C.causes.USER_DENIED_MEDIA_ACCESS);
|
|||
|
self.failed(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);
|
|||
|
}
|
|||
|
|
|||
|
// 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) {
|
|||
|
sdpCreationSucceeded();
|
|||
|
} else {
|
|||
|
this.mediaHandler.getDescription(
|
|||
|
sdpCreationSucceeded,
|
|||
|
sdpCreationFailed,
|
|||
|
self.mediaHint
|
|||
|
);
|
|||
|
}
|
|||
|
|
|||
|
return this;
|
|||
|
},
|
|||
|
|
|||
|
receiveRequest: function(request) {
|
|||
|
|
|||
|
// ISC RECEIVE REQUEST
|
|||
|
|
|||
|
function confirmSession() {
|
|||
|
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 (contentType !== 'application/sdp') {
|
|||
|
this.renderbody = request.body;
|
|||
|
this.rendertype = contentType;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
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);
|
|||
|
}
|
|||
|
break;
|
|||
|
case SIP.C.ACK:
|
|||
|
if(this.status === C.STATUS_WAITING_FOR_ACK) {
|
|||
|
if (!this.hasAnswer) {
|
|||
|
if(request.body && request.getHeader('content-type') === 'application/sdp') {
|
|||
|
// ACK contains answer to an INVITE w/o SDP negotiation
|
|||
|
SIP.Hacks.Firefox.cannotHandleRelayCandidates(request);
|
|||
|
SIP.Hacks.Firefox.cannotHandleExtraWhitespace(request);
|
|||
|
|
|||
|
this.hasAnswer = true;
|
|||
|
this.mediaHandler.setDescription(
|
|||
|
request.body,
|
|||
|
/*
|
|||
|
* onSuccess
|
|||
|
* SDP Answer fits with Offer. Media will start
|
|||
|
*/
|
|||
|
confirmSession.bind(this),
|
|||
|
/*
|
|||
|
* onFailure
|
|||
|
* SDP Answer does not fit the Offer. Terminate the call.
|
|||
|
*/
|
|||
|
function (e) {
|
|||
|
this.logger.warn(e);
|
|||
|
this.terminate({
|
|||
|
statusCode: '488',
|
|||
|
reasonPhrase: 'Bad Media Description'
|
|||
|
});
|
|||
|
this.failed(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);
|
|||
|
}
|
|||
|
} 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(request.body && request.getHeader('content-type') === 'application/sdp') {
|
|||
|
this.hasAnswer = true;
|
|||
|
this.mediaHandler.setDescription(
|
|||
|
request.body,
|
|||
|
/*
|
|||
|
* onSuccess
|
|||
|
* SDP Answer fits with Offer. Media will start
|
|||
|
*/
|
|||
|
function() {
|
|||
|
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 (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);
|
|||
|
}.bind(this)
|
|||
|
);
|
|||
|
} else {
|
|||
|
this.terminate({
|
|||
|
statusCode: '488',
|
|||
|
reasonPhrase: 'Bad Media Description'
|
|||
|
});
|
|||
|
this.failed(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;
|
|||
|
}
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
SIP.InviteServerContext = InviteServerContext;
|
|||
|
|
|||
|
InviteClientContext = function(ua, target, options) {
|
|||
|
options = options || {};
|
|||
|
var requestParams, iceServers,
|
|||
|
extraHeaders = (options.extraHeaders || []).slice(),
|
|||
|
stunServers = options.stunServers || null,
|
|||
|
turnServers = options.turnServers || null,
|
|||
|
isMediaSupported = ua.configuration.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';
|
|||
|
|
|||
|
requestParams = {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) {
|
|||
|
requestParams.from_displayName = 'Anonymous';
|
|||
|
requestParams.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.Utils.getAllowedMethods(ua));
|
|||
|
if (!this.inviteWithoutSdp) {
|
|||
|
extraHeaders.push('Content-Type: application/sdp');
|
|||
|
} else if (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');
|
|||
|
}
|
|||
|
|
|||
|
options.extraHeaders = extraHeaders;
|
|||
|
options.params = requestParams;
|
|||
|
|
|||
|
SIP.Utils.augment(this, SIP.ClientContext, [ua, SIP.C.INVITE, target, options]);
|
|||
|
SIP.Utils.augment(this, SIP.Session, [ua.configuration.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 = SIP.UA.configuration_check.optional['stunServers'](stunServers);
|
|||
|
if (!iceServers) {
|
|||
|
throw new TypeError('Invalid stunServers: '+ stunServers);
|
|||
|
} else {
|
|||
|
this.stunServers = iceServers;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if (turnServers) {
|
|||
|
iceServers = SIP.UA.configuration_check.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);
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
InviteClientContext.prototype = {
|
|||
|
/*
|
|||
|
* @param {Object} [options.media] gets passed to SIP.MediaHandler.getDescription as mediaHint
|
|||
|
*/
|
|||
|
invite: function (options) {
|
|||
|
var self = this;
|
|||
|
options = options || {};
|
|||
|
|
|||
|
SIP.Utils.optionsOverride(options, 'media', 'mediaConstraints', true, this.logger, this.ua.configuration.media);
|
|||
|
this.mediaHint = options.media;
|
|||
|
|
|||
|
//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(
|
|||
|
function onSuccess(offer) {
|
|||
|
if (self.isCanceled || self.status === C.STATUS_TERMINATED) {
|
|||
|
return;
|
|||
|
}
|
|||
|
self.hasOffer = true;
|
|||
|
self.request.body = offer;
|
|||
|
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.mediaHint
|
|||
|
);
|
|||
|
}
|
|||
|
|
|||
|
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);
|
|||
|
}
|
|||
|
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.earlyDialogs[id] && !this.createDialog(response, 'UAC', true)) {
|
|||
|
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);
|
|||
|
this.canceled(null);
|
|||
|
} else if(response.status_code >= 200 && response.status_code < 299) {
|
|||
|
this.acceptAndTerminate(response);
|
|||
|
this.emit('bye', this.request);
|
|||
|
}
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
switch(true) {
|
|||
|
case /^100$/.test(response.status_code):
|
|||
|
this.received_100 = true;
|
|||
|
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;
|
|||
|
}
|
|||
|
|
|||
|
SIP.Hacks.Firefox.cannotHandleRelayCandidates(response);
|
|||
|
SIP.Hacks.Firefox.cannotHandleExtraWhitespace(response);
|
|||
|
|
|||
|
if (!response.body) {
|
|||
|
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.mediaHandler.setDescription(
|
|||
|
response.body,
|
|||
|
/*
|
|||
|
* onSuccess
|
|||
|
* SDP Answer fits with Offer. Media will start
|
|||
|
*/
|
|||
|
function () {
|
|||
|
extraHeaders.push('RAck: ' + response.getHeader('rseq') + ' ' + response.getHeader('cseq'));
|
|||
|
session.dialog.pracked.push(response.getHeader('rseq'));
|
|||
|
|
|||
|
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;
|
|||
|
}
|
|||
|
}*/
|
|||
|
},
|
|||
|
/*
|
|||
|
* onFailure
|
|||
|
* SDP Answer does not fit the Offer. Accept the call and Terminate.
|
|||
|
*/
|
|||
|
function(e) {
|
|||
|
session.logger.warn(e);
|
|||
|
session.acceptAndTerminate(response, 488, 'Not Acceptable Here');
|
|||
|
session.failed(response, SIP.C.causes.BAD_MEDIA_DESCRIPTION);
|
|||
|
}
|
|||
|
);
|
|||
|
} else {
|
|||
|
this.earlyDialogs[id].pracked.push(response.getHeader('rseq'));
|
|||
|
this.earlyDialogs[id].mediaHandler.setDescription(
|
|||
|
response.body,
|
|||
|
function onSuccess() {
|
|||
|
session.earlyDialogs[id].mediaHandler.getDescription(
|
|||
|
function onSuccess(sdp) {
|
|||
|
extraHeaders.push('Content-Type: application/sdp');
|
|||
|
extraHeaders.push('RAck: ' + response.getHeader('rseq') + ' ' + response.getHeader('cseq'));
|
|||
|
session.earlyDialogs[id].sendRequest(session, SIP.C.PRACK, {
|
|||
|
extraHeaders: extraHeaders,
|
|||
|
body: sdp
|
|||
|
});
|
|||
|
session.status = C.STATUS_EARLY_MEDIA;
|
|||
|
session.emit('progress', response);
|
|||
|
},
|
|||
|
function onFailure() {
|
|||
|
session.earlyDialogs[id].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.mediaHint
|
|||
|
);
|
|||
|
},
|
|||
|
function onFailure(e) {
|
|||
|
session.earlyDialogs[id].pracked.splice(session.earlyDialogs[id].pracked.indexOf(response.getHeader('rseq')), 1);
|
|||
|
// Could not set remote description
|
|||
|
session.logger.warn('invalid SDP');
|
|||
|
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;
|
|||
|
}
|
|||
|
|
|||
|
SIP.Hacks.Firefox.cannotHandleRelayCandidates(response);
|
|||
|
SIP.Hacks.Firefox.cannotHandleExtraWhitespace(response);
|
|||
|
|
|||
|
// 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(!response.body) {
|
|||
|
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.body,
|
|||
|
function onSuccess() {
|
|||
|
session.mediaHandler.getDescription(
|
|||
|
function onSuccess(sdp) {
|
|||
|
//var localMedia;
|
|||
|
if(session.isCanceled || session.status === C.STATUS_TERMINATED) {
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
sdp = SIP.Hacks.Firefox.hasMissingCLineInSDP(sdp);
|
|||
|
|
|||
|
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: sdp,
|
|||
|
extraHeaders:['Content-Type: application/sdp'],
|
|||
|
cseq:response.cseq
|
|||
|
});
|
|||
|
session.accepted(response);
|
|||
|
},
|
|||
|
function onFailure() {
|
|||
|
// TODO do something here
|
|||
|
session.logger.warn("there was a problem");
|
|||
|
},
|
|||
|
session.mediaHint
|
|||
|
);
|
|||
|
},
|
|||
|
function onFailure(e) {
|
|||
|
session.logger.warn('invalid SDP');
|
|||
|
session.logger.warn(e);
|
|||
|
response.reply(488);
|
|||
|
}
|
|||
|
);
|
|||
|
}
|
|||
|
} 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(!response.body) {
|
|||
|
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.body,
|
|||
|
/*
|
|||
|
* onSuccess
|
|||
|
* SDP Answer fits with Offer. Media will start
|
|||
|
*/
|
|||
|
function() {
|
|||
|
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);
|
|||
|
},
|
|||
|
/*
|
|||
|
* onFailure
|
|||
|
* SDP Answer does not fit the Offer. Accept the call and Terminate.
|
|||
|
*/
|
|||
|
function(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.failed(response, cause);
|
|||
|
this.rejected(response, cause);
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
cancel: function(options) {
|
|||
|
options = options || {};
|
|||
|
|
|||
|
var
|
|||
|
statusCode = options.status_code,
|
|||
|
reasonPhrase = options.reasonPhrase,
|
|||
|
cancel_reason;
|
|||
|
|
|||
|
// Check Session Status
|
|||
|
if (this.status === C.STATUS_TERMINATED) {
|
|||
|
throw new SIP.Exceptions.InvalidStateError(this.status);
|
|||
|
}
|
|||
|
|
|||
|
this.logger.log('canceling RTCSession');
|
|||
|
|
|||
|
if (statusCode && (statusCode < 200 || statusCode >= 700)) {
|
|||
|
throw new TypeError('Invalid status_code: '+ statusCode);
|
|||
|
} else if (statusCode) {
|
|||
|
reasonPhrase = reasonPhrase || SIP.C.REASON_PHRASE[statusCode] || '';
|
|||
|
cancel_reason = 'SIP ;cause=' + statusCode + ' ;text="' + reasonPhrase + '"';
|
|||
|
}
|
|||
|
|
|||
|
// 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);
|
|||
|
}
|
|||
|
|
|||
|
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.terminated();
|
|||
|
},
|
|||
|
|
|||
|
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]);
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
SIP.InviteClientContext = InviteClientContext;
|
|||
|
|
|||
|
};
|
|||
|
|
|||
|
},{}],23:[function(_dereq_,module,exports){
|
|||
|
/**
|
|||
|
* @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 events = [
|
|||
|
'succeeded',
|
|||
|
'failed'
|
|||
|
], 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;
|
|||
|
|
|||
|
this.initEvents(events);
|
|||
|
};
|
|||
|
DTMF.prototype = new SIP.EventEmitter();
|
|||
|
|
|||
|
|
|||
|
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() : [];
|
|||
|
|
|||
|
extraHeaders.push('Content-Type: application/dtmf-relay');
|
|||
|
|
|||
|
body = "Signal= " + this.tone + "\r\n";
|
|||
|
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;
|
|||
|
};
|
|||
|
|
|||
|
},{}],24:[function(_dereq_,module,exports){
|
|||
|
|
|||
|
/**
|
|||
|
* @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) {
|
|||
|
var events;
|
|||
|
|
|||
|
options = options || {};
|
|||
|
options.extraHeaders = (options.extraHeaders || []).slice();
|
|||
|
|
|||
|
events = ['notify'];
|
|||
|
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 (!options.expires || options.expires < 3600) {
|
|||
|
this.expires = 3600; //1 hour (this is minimum by RFC 6665)
|
|||
|
} else 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.Utils.getAllowedMethods(ua));
|
|||
|
|
|||
|
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];
|
|||
|
|
|||
|
this.initMoreEvents(events);
|
|||
|
};
|
|||
|
|
|||
|
SIP.Subscription.prototype = {
|
|||
|
subscribe: function() {
|
|||
|
var sub = 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.send();
|
|||
|
|
|||
|
this.state = 'notify_wait';
|
|||
|
|
|||
|
return this;
|
|||
|
},
|
|||
|
|
|||
|
receiveResponse: function(response) {
|
|||
|
var expires, sub = this;
|
|||
|
|
|||
|
if (this.errorCodes.indexOf(response.status_code) !== -1) {
|
|||
|
this.failed(response, null);
|
|||
|
} else if (/^2[0-9]{2}$/.test(response.status_code)){
|
|||
|
expires = response.getHeader('Expires');
|
|||
|
SIP.Timers.clearTimeout(this.timers.N);
|
|||
|
|
|||
|
if (this.createConfirmedDialog(response,'UAC')) {
|
|||
|
this.id = this.dialog.id.toString();
|
|||
|
this.ua.subscriptions[this.id] = this;
|
|||
|
// UPDATE ROUTE SET TO BE BACKWARDS COMPATIBLE?
|
|||
|
}
|
|||
|
|
|||
|
if (expires && expires <= this.expires) {
|
|||
|
this.timers.sub_duration = SIP.Timers.setTimeout(sub.subscribe.bind(sub), expires * 1000);
|
|||
|
} 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);
|
|||
|
}
|
|||
|
}
|
|||
|
} //Used to just ignore provisional responses; now ignores everything except errorCodes and 2xx
|
|||
|
},
|
|||
|
|
|||
|
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.Utils.getAllowedMethods(this.ua));
|
|||
|
|
|||
|
this.request = new SIP.OutgoingRequest(this.method, this.request.to.uri.toString(), this.ua, null, extraHeaders);
|
|||
|
|
|||
|
//MAYBE, may want to see state
|
|||
|
this.receiveResponse = function(){};
|
|||
|
|
|||
|
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.send();
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* @private
|
|||
|
*/
|
|||
|
timer_fire: function(){
|
|||
|
if (this.state === 'terminated') {
|
|||
|
this.close();
|
|||
|
} else if (this.state === 'pending' || this.state === 'notify_wait') {
|
|||
|
this.state = 'terminated';
|
|||
|
this.close();
|
|||
|
} else {
|
|||
|
this.subscribe();
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* @private
|
|||
|
*/
|
|||
|
close: function() {
|
|||
|
if(this.state !== 'terminated') {
|
|||
|
this.unsubscribe();
|
|||
|
}
|
|||
|
|
|||
|
this.terminateDialog();
|
|||
|
SIP.Timers.clearTimeout(this.timers.N);
|
|||
|
SIP.Timers.clearTimeout(this.timers.sub_duration);
|
|||
|
|
|||
|
delete this.ua.subscriptions[this.id];
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* @private
|
|||
|
*/
|
|||
|
createConfirmedDialog: function(message, type) {
|
|||
|
var dialog;
|
|||
|
|
|||
|
dialog = new SIP.Dialog(this, message, type);
|
|||
|
|
|||
|
if(!dialog.error) {
|
|||
|
this.dialog = dialog;
|
|||
|
return true;
|
|||
|
}
|
|||
|
// Dialog not created due to an error
|
|||
|
else {
|
|||
|
return false;
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* @private
|
|||
|
*/
|
|||
|
terminateDialog: function() {
|
|||
|
if(this.dialog) {
|
|||
|
this.dialog.terminate();
|
|||
|
delete this.dialog;
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* @private
|
|||
|
*/
|
|||
|
receiveRequest: function(request) {
|
|||
|
var sub_state, sub = this;
|
|||
|
|
|||
|
function setExpiresTimeout() {
|
|||
|
if (sub_state.expires) {
|
|||
|
sub_state.expires = Math.min(sub.expires,
|
|||
|
Math.max(sub_state.expires, 3600));
|
|||
|
sub.timers.sub_duration = SIP.Timers.setTimeout(sub.subscribe.bind(sub),
|
|||
|
sub_state.expires * 1000);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if (!this.matchEvent(request)) { //checks event and subscription_state headers
|
|||
|
request.reply(489);
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
sub_state = request.parseHeader('Subscription-State');
|
|||
|
|
|||
|
request.reply(200, SIP.C.REASON_200);
|
|||
|
|
|||
|
SIP.Timers.clearTimeout(this.timers.N);
|
|||
|
SIP.Timers.clearTimeout(this.timers.sub_duration);
|
|||
|
|
|||
|
this.emit('notify', {request: request});
|
|||
|
|
|||
|
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':
|
|||
|
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();
|
|||
|
return this.emit('failed', response, cause);
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* @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;
|
|||
|
}
|
|||
|
}
|
|||
|
};
|
|||
|
};
|
|||
|
|
|||
|
},{}],25:[function(_dereq_,module,exports){
|
|||
|
/**
|
|||
|
* @fileoverview SIP TIMERS
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* @augments SIP
|
|||
|
*/
|
|||
|
var
|
|||
|
T1 = 500,
|
|||
|
T2 = 4000,
|
|||
|
T4 = 5000;
|
|||
|
module.exports = function (timers) {
|
|||
|
var exports = {
|
|||
|
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
|
|||
|
exports[name] = function () {
|
|||
|
return timers[name].apply(timers, arguments);
|
|||
|
};
|
|||
|
});
|
|||
|
|
|||
|
return exports;
|
|||
|
};
|
|||
|
|
|||
|
},{}],26:[function(_dereq_,module,exports){
|
|||
|
/**
|
|||
|
* @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'
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* @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,
|
|||
|
events = ['stateChanged'];
|
|||
|
|
|||
|
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 = 'SIP/2.0/' + (request_sender.ua.configuration.hackViaTcp ? 'TCP' : transport.server.scheme);
|
|||
|
via += ' ' + request_sender.ua.configuration.viaHost + ';branch=' + this.id;
|
|||
|
|
|||
|
this.request.setHeader('via', via);
|
|||
|
|
|||
|
this.request_sender.ua.newTransaction(this);
|
|||
|
|
|||
|
this.initEvents(events);
|
|||
|
};
|
|||
|
NonInviteClientTransaction.prototype = new SIP.EventEmitter();
|
|||
|
|
|||
|
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,
|
|||
|
events = ['stateChanged'];
|
|||
|
|
|||
|
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 = 'SIP/2.0/' + (request_sender.ua.configuration.hackViaTcp ? 'TCP' : transport.server.scheme);
|
|||
|
via += ' ' + request_sender.ua.configuration.viaHost + ';branch=' + 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) {
|
|||
|
tr.cancel_request(tr, reason);
|
|||
|
};
|
|||
|
|
|||
|
this.initEvents(events);
|
|||
|
};
|
|||
|
InviteClientTransaction.prototype = new SIP.EventEmitter();
|
|||
|
|
|||
|
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 += '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) {
|
|||
|
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';
|
|||
|
}
|
|||
|
|
|||
|
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 = 'SIP/2.0/' + (request_sender.ua.configuration.hackViaTcp ? 'TCP' : transport.server.scheme);
|
|||
|
via += ' ' + request_sender.ua.configuration.viaHost + ';branch=' + this.id;
|
|||
|
|
|||
|
this.request.setHeader('via', via);
|
|||
|
};
|
|||
|
AckClientTransaction.prototype = new SIP.EventEmitter();
|
|||
|
|
|||
|
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) {
|
|||
|
var events = ['stateChanged'];
|
|||
|
|
|||
|
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);
|
|||
|
|
|||
|
this.initEvents(events);
|
|||
|
};
|
|||
|
NonInviteServerTransaction.prototype = new SIP.EventEmitter();
|
|||
|
|
|||
|
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, onSuccess, onFailure) {
|
|||
|
var tr = this;
|
|||
|
|
|||
|
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();
|
|||
|
if (onFailure) {
|
|||
|
onFailure();
|
|||
|
}
|
|||
|
} else if (onSuccess) {
|
|||
|
onSuccess();
|
|||
|
}
|
|||
|
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();
|
|||
|
if (onFailure) {
|
|||
|
onFailure();
|
|||
|
}
|
|||
|
} else if (onSuccess) {
|
|||
|
onSuccess();
|
|||
|
}
|
|||
|
break;
|
|||
|
case C.STATUS_COMPLETED:
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* @augments SIP.Transactions
|
|||
|
* @class Invite Server Transaction
|
|||
|
* @param {SIP.IncomingRequest} request
|
|||
|
* @param {SIP.UA} ua
|
|||
|
*/
|
|||
|
var InviteServerTransaction = function(request, ua) {
|
|||
|
var events = ['stateChanged'];
|
|||
|
|
|||
|
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);
|
|||
|
|
|||
|
this.initEvents(events);
|
|||
|
};
|
|||
|
InviteServerTransaction.prototype = new SIP.EventEmitter();
|
|||
|
|
|||
|
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, onSuccess, onFailure) {
|
|||
|
var tr = this;
|
|||
|
|
|||
|
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();
|
|||
|
if (onFailure) {
|
|||
|
onFailure();
|
|||
|
}
|
|||
|
} else if (onSuccess) {
|
|||
|
onSuccess();
|
|||
|
}
|
|||
|
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();
|
|||
|
if (onFailure) {
|
|||
|
onFailure();
|
|||
|
}
|
|||
|
} else {
|
|||
|
this.stateChanged(C.STATUS_COMPLETED);
|
|||
|
this.H = SIP.Timers.setTimeout(tr.timer_H.bind(tr), SIP.Timers.TIMER_H);
|
|||
|
if (onSuccess) {
|
|||
|
onSuccess();
|
|||
|
}
|
|||
|
}
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* @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.state = 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
|
|||
|
};
|
|||
|
|
|||
|
};
|
|||
|
|
|||
|
},{}],27:[function(_dereq_,module,exports){
|
|||
|
/**
|
|||
|
* @fileoverview Transport
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* @augments SIP
|
|||
|
* @class Transport
|
|||
|
* @param {SIP.UA} ua
|
|||
|
* @param {Object} server ws_server Object
|
|||
|
*/
|
|||
|
module.exports = function (SIP, window) {
|
|||
|
var Transport,
|
|||
|
C = {
|
|||
|
// Transport status codes
|
|||
|
STATUS_READY: 0,
|
|||
|
STATUS_DISCONNECTED: 1,
|
|||
|
STATUS_ERROR: 2
|
|||
|
};
|
|||
|
|
|||
|
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.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 === window.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;
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* Disconnect socket.
|
|||
|
*/
|
|||
|
disconnect: function() {
|
|||
|
if(this.ws) {
|
|||
|
// Clear reconnectTimer
|
|||
|
SIP.Timers.clearTimeout(this.reconnectTimer);
|
|||
|
|
|||
|
this.closed = true;
|
|||
|
this.logger.log('closing WebSocket ' + this.server.ws_uri);
|
|||
|
this.ws.close();
|
|||
|
}
|
|||
|
|
|||
|
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.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 window.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);
|
|||
|
};
|
|||
|
|
|||
|
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);
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* @event
|
|||
|
* @param {event} e
|
|||
|
*/
|
|||
|
onClose: function(e) {
|
|||
|
var connected_before = this.connected;
|
|||
|
|
|||
|
this.connected = false;
|
|||
|
this.lastTransportError.code = e.code;
|
|||
|
this.lastTransportError.reason = e.reason;
|
|||
|
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') {
|
|||
|
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: ' + 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 {
|
|||
|
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;
|
|||
|
SIP.Transport = Transport;
|
|||
|
};
|
|||
|
|
|||
|
},{}],28:[function(_dereq_,module,exports){
|
|||
|
/**
|
|||
|
* @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) {
|
|||
|
var UA,
|
|||
|
C = {
|
|||
|
// UA status codes
|
|||
|
STATUS_INIT : 0,
|
|||
|
STATUS_READY: 1,
|
|||
|
STATUS_USER_CLOSED: 2,
|
|||
|
STATUS_NOT_READY: 3,
|
|||
|
|
|||
|
// UA error codes
|
|||
|
CONFIGURATION_ERROR: 1,
|
|||
|
NETWORK_ERROR: 2,
|
|||
|
|
|||
|
/* UA events and corresponding SIP Methods.
|
|||
|
* Dynamically added to 'Allow' header field if the
|
|||
|
* corresponding event handler is set.
|
|||
|
*/
|
|||
|
EVENT_METHODS: {
|
|||
|
'invite': 'INVITE',
|
|||
|
'message': 'MESSAGE'
|
|||
|
},
|
|||
|
|
|||
|
ALLOWED_METHODS: [
|
|||
|
'ACK',
|
|||
|
'CANCEL',
|
|||
|
'BYE',
|
|||
|
'OPTIONS',
|
|||
|
'INFO',
|
|||
|
'NOTIFY'
|
|||
|
],
|
|||
|
|
|||
|
ACCEPTED_BODY_TYPES: [
|
|||
|
'application/sdp',
|
|||
|
'application/dtmf-relay'
|
|||
|
],
|
|||
|
|
|||
|
MAX_FORWARDS: 70,
|
|||
|
TAG_LENGTH: 10
|
|||
|
};
|
|||
|
|
|||
|
UA = function(configuration) {
|
|||
|
var self = this,
|
|||
|
events = [
|
|||
|
'connecting',
|
|||
|
'connected',
|
|||
|
'disconnected',
|
|||
|
'newTransaction',
|
|||
|
'transactionDestroyed',
|
|||
|
'registered',
|
|||
|
'unregistered',
|
|||
|
'registrationFailed',
|
|||
|
'invite',
|
|||
|
'newSession',
|
|||
|
'message'
|
|||
|
], i, len;
|
|||
|
|
|||
|
// 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);
|
|||
|
}
|
|||
|
|
|||
|
for (i = 0, len = C.ALLOWED_METHODS.length; i < len; i++) {
|
|||
|
events.push(C.ALLOWED_METHODS[i].toLowerCase());
|
|||
|
}
|
|||
|
|
|||
|
// 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.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);
|
|||
|
this.initEvents(events);
|
|||
|
} 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 = new SIP.EventEmitter();
|
|||
|
|
|||
|
//=================
|
|||
|
// 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;
|
|||
|
this.registerContext.unregister(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;
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* 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) {
|
|||
|
options = options || {};
|
|||
|
SIP.Utils.optionsOverride(options, 'media', 'mediaConstraints', true, this.logger);
|
|||
|
|
|||
|
var context = new SIP.InviteClientContext(this, target, options);
|
|||
|
|
|||
|
if (this.isConnected()) {
|
|||
|
context.invite({media: options.media});
|
|||
|
} else {
|
|||
|
this.once('connected', function() {
|
|||
|
context.invite({media: options.media});
|
|||
|
});
|
|||
|
}
|
|||
|
return context;
|
|||
|
};
|
|||
|
|
|||
|
UA.prototype.subscribe = function(target, event, options) {
|
|||
|
var sub = new SIP.Subscription(this, target, event, options);
|
|||
|
|
|||
|
if (this.isConnected()) {
|
|||
|
sub.subscribe();
|
|||
|
} else {
|
|||
|
this.once('connected', function() {
|
|||
|
sub.subscribe();
|
|||
|
});
|
|||
|
}
|
|||
|
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');
|
|||
|
}
|
|||
|
|
|||
|
options = options || {};
|
|||
|
options.contentType = options.contentType || 'text/plain';
|
|||
|
options.body = body;
|
|||
|
|
|||
|
var mes = new SIP.ClientContext(this, SIP.C.MESSAGE, target, options);
|
|||
|
|
|||
|
if (this.isConnected()) {
|
|||
|
mes.send();
|
|||
|
} else {
|
|||
|
this.once('connected', function() {
|
|||
|
mes.send();
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
return mes;
|
|||
|
};
|
|||
|
|
|||
|
UA.prototype.request = function (method, target, options) {
|
|||
|
var req = new SIP.ClientContext(this, method, target, options);
|
|||
|
|
|||
|
if (this.isConnected()) {
|
|||
|
req.send();
|
|||
|
} else {
|
|||
|
this.once('connected', function() {
|
|||
|
req.send();
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
return req;
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* Gracefully close.
|
|||
|
*
|
|||
|
*/
|
|||
|
UA.prototype.stop = function() {
|
|||
|
var session, subscription, applicant,
|
|||
|
ua = this;
|
|||
|
|
|||
|
function transactionsListener() {
|
|||
|
if (ua.nistTransactionsCount === 0 && ua.nictTransactionsCount === 0) {
|
|||
|
ua.off('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 Subscription
|
|||
|
for(subscription in this.subscriptions) {
|
|||
|
this.logger.log('unsubscribing from subscription ' + subscription);
|
|||
|
this.subscriptions[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);
|
|||
|
}
|
|||
|
|
|||
|
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();
|
|||
|
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_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');
|
|||
|
}
|
|||
|
|
|||
|
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' and try the next one
|
|||
|
transport.server.status = SIP.Transport.C.STATUS_ERROR;
|
|||
|
|
|||
|
this.emit('disconnected', {
|
|||
|
transport: transport
|
|||
|
});
|
|||
|
|
|||
|
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.registerContext.onTransportConnected();
|
|||
|
}
|
|||
|
|
|||
|
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,
|
|||
|
method = request.method,
|
|||
|
transaction,
|
|||
|
methodLower = request.method.toLowerCase(),
|
|||
|
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.Utils.getAllowedMethods(this),
|
|||
|
'Accept: '+ C.ACCEPTED_BODY_TYPES
|
|||
|
]);
|
|||
|
} else if (method === SIP.C.MESSAGE) {
|
|||
|
if (!this.checkListener(methodLower)) {
|
|||
|
// UA is not listening for this. Reject immediately.
|
|||
|
new SIP.Transactions.NonInviteServerTransaction(request, this);
|
|||
|
request.reply(405, null, ['Allow: '+ SIP.Utils.getAllowedMethods(this)]);
|
|||
|
return;
|
|||
|
}
|
|||
|
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:
|
|||
|
var isMediaSupported = this.configuration.mediaHandlerFactory.isSupported;
|
|||
|
if(!isMediaSupported || isMediaSupported()) {
|
|||
|
session = new SIP.InviteServerContext(this, request)
|
|||
|
.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;
|
|||
|
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);
|
|||
|
if(session) {
|
|||
|
session.receiveRequest(request);
|
|||
|
} else {
|
|||
|
this.logger.warn('received NOTIFY request for a non existent session');
|
|||
|
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;
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* 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);
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* 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: '<sip:edge.sip.onsip.com;transport=ws;lr>',
|
|||
|
status: 0,
|
|||
|
weight: 0,
|
|||
|
ws_uri: 'wss://edge.sip.onsip.com'
|
|||
|
}],
|
|||
|
|
|||
|
// Password
|
|||
|
password: null,
|
|||
|
|
|||
|
// Registration parameters
|
|||
|
registerExpires: 600,
|
|||
|
register: true,
|
|||
|
registrarServer: null,
|
|||
|
|
|||
|
// Transport related parameters
|
|||
|
wsServerMaxReconnection: 3,
|
|||
|
wsServerReconnectionTimeout: 4,
|
|||
|
|
|||
|
connectionRecoveryMinInterval: 2,
|
|||
|
connectionRecoveryMaxInterval: 30,
|
|||
|
|
|||
|
usePreloadedRoute: false,
|
|||
|
|
|||
|
//string to be inserted into User-Agent request header
|
|||
|
userAgentString: SIP.C.USER_AGENT,
|
|||
|
|
|||
|
// Session parameters
|
|||
|
noAnswerTimeout: 60,
|
|||
|
stunServers: ['stun:stun.l.google.com:19302'],
|
|||
|
turnServers: [],
|
|||
|
|
|||
|
// Logging parameters
|
|||
|
traceSip: false,
|
|||
|
|
|||
|
// Hacks
|
|||
|
hackViaTcp: false,
|
|||
|
hackIpInContact: false,
|
|||
|
|
|||
|
//autostarting
|
|||
|
autostart: true,
|
|||
|
|
|||
|
//Reliable Provisional Responses
|
|||
|
rel100: SIP.C.supported.UNSUPPORTED,
|
|||
|
|
|||
|
mediaHandlerFactory: SIP.WebRTC.MediaHandler.defaultFactory
|
|||
|
};
|
|||
|
|
|||
|
// 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];
|
|||
|
}
|
|||
|
|
|||
|
// Check Mandatory parameters
|
|||
|
for(parameter in UA.configuration_check.mandatory) {
|
|||
|
aliasUnderscored(parameter, this.logger);
|
|||
|
if(!configuration.hasOwnProperty(parameter)) {
|
|||
|
throw new SIP.Exceptions.ConfigurationError(parameter);
|
|||
|
} else {
|
|||
|
value = configuration[parameter];
|
|||
|
checked_value = UA.configuration_check.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);
|
|||
|
|
|||
|
// Check Optional parameters
|
|||
|
for(parameter in UA.configuration_check.optional) {
|
|||
|
aliasUnderscored(parameter, this.logger);
|
|||
|
if(configuration.hasOwnProperty(parameter)) {
|
|||
|
value = configuration[parameter];
|
|||
|
|
|||
|
// If the parameter value is null, empty string,undefined, or empty array then apply its default value.
|
|||
|
if(value === null || value === "" || value === undefined || (value instanceof Array && value.length === 0)) { 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 = UA.configuration_check.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.toString().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) {
|
|||
|
settings.viaHost = SIP.Utils.getRandomTestNetIP();
|
|||
|
}
|
|||
|
|
|||
|
this.contact = {
|
|||
|
pub_gruu: null,
|
|||
|
temp_gruu: null,
|
|||
|
uri: new SIP.URI('sip', SIP.Utils.createRandomToken(8), settings.viaHost, null, {transport: 'ws'}),
|
|||
|
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=ws').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);
|
|||
|
|
|||
|
// Fill the value of the configuration_skeleton
|
|||
|
for(parameter in settings) {
|
|||
|
UA.configuration_skeleton[parameter].value = settings[parameter];
|
|||
|
}
|
|||
|
|
|||
|
Object.defineProperties(this.configuration, UA.configuration_skeleton);
|
|||
|
|
|||
|
// Clean UA.configuration_skeleton
|
|||
|
for(parameter in settings) {
|
|||
|
UA.configuration_skeleton[parameter].value = '';
|
|||
|
}
|
|||
|
|
|||
|
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 Object skeleton.
|
|||
|
* @private
|
|||
|
*/
|
|||
|
UA.configuration_skeleton = (function() {
|
|||
|
var idx, parameter,
|
|||
|
skeleton = {},
|
|||
|
parameters = [
|
|||
|
// Internal parameters
|
|||
|
"sipjsId",
|
|||
|
"wsServerMaxReconnection",
|
|||
|
"wsServerReconnectionTimeout",
|
|||
|
"hostportParams",
|
|||
|
|
|||
|
// Optional user configurable parameters
|
|||
|
"uri",
|
|||
|
"wsServers",
|
|||
|
"authorizationUser",
|
|||
|
"connectionRecoveryMaxInterval",
|
|||
|
"connectionRecoveryMinInterval",
|
|||
|
"displayName",
|
|||
|
"hackViaTcp", // false.
|
|||
|
"hackIpInContact", //false
|
|||
|
"instanceId",
|
|||
|
"noAnswerTimeout", // 30 seconds.
|
|||
|
"password",
|
|||
|
"registerExpires", // 600 seconds.
|
|||
|
"registrarServer",
|
|||
|
"reliable",
|
|||
|
"rel100",
|
|||
|
"userAgentString", //SIP.C.USER_AGENT
|
|||
|
"autostart",
|
|||
|
"stunServers",
|
|||
|
"traceSip",
|
|||
|
"turnServers",
|
|||
|
"usePreloadedRoute",
|
|||
|
"mediaHandlerFactory",
|
|||
|
"media",
|
|||
|
"mediaConstraints",
|
|||
|
|
|||
|
// Post-configuration generated parameters
|
|||
|
"via_core_value",
|
|||
|
"viaHost"
|
|||
|
];
|
|||
|
|
|||
|
for(idx in parameters) {
|
|||
|
parameter = parameters[idx];
|
|||
|
skeleton[parameter] = {
|
|||
|
value: '',
|
|||
|
writable: false,
|
|||
|
configurable: false
|
|||
|
};
|
|||
|
}
|
|||
|
|
|||
|
skeleton['register'] = {
|
|||
|
value: '',
|
|||
|
writable: true,
|
|||
|
configurable: false
|
|||
|
};
|
|||
|
|
|||
|
return skeleton;
|
|||
|
}());
|
|||
|
|
|||
|
/**
|
|||
|
* Configuration checker.
|
|||
|
* @private
|
|||
|
* @return {Boolean}
|
|||
|
*/
|
|||
|
UA.configuration_check = {
|
|||
|
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(url.scheme !== 'wss' && url.scheme !== 'ws') {
|
|||
|
return;
|
|||
|
} else {
|
|||
|
wsServers[idx].sip_uri = '<sip:' + url.host + (url.port ? ':' + url.port : '') + ';transport=ws;lr>';
|
|||
|
|
|||
|
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;
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
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;
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
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;
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
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, length, turn_server, 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 || !turn_server.username || !turn_server.password) {
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
if (!(turn_server.urls instanceof Array)) {
|
|||
|
turn_server.urls = [turn_server.urls];
|
|||
|
}
|
|||
|
|
|||
|
length = turn_server.urls.length;
|
|||
|
for (idx = 0; idx < length; idx++) {
|
|||
|
url = turn_server.urls[idx];
|
|||
|
|
|||
|
if (!(/^turns?:/.test(url))) {
|
|||
|
url = 'turn:' + url;
|
|||
|
}
|
|||
|
|
|||
|
if(SIP.Grammar.parse(url, 'turn_URI') === -1) {
|
|||
|
return;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
return turnServers;
|
|||
|
},
|
|||
|
|
|||
|
userAgentString: function(userAgentString) {
|
|||
|
if (typeof userAgentString === 'string') {
|
|||
|
return userAgentString;
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
usePreloadedRoute: function(usePreloadedRoute) {
|
|||
|
if (typeof usePreloadedRoute === 'boolean') {
|
|||
|
return usePreloadedRoute;
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
autostart: function(autostart) {
|
|||
|
if (typeof autostart === 'boolean') {
|
|||
|
return autostart;
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
mediaHandlerFactory: function(mediaHandlerFactory) {
|
|||
|
if (mediaHandlerFactory instanceof Function) {
|
|||
|
return mediaHandlerFactory;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
UA.C = C;
|
|||
|
SIP.UA = UA;
|
|||
|
};
|
|||
|
|
|||
|
},{}],29:[function(_dereq_,module,exports){
|
|||
|
/**
|
|||
|
* @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;
|
|||
|
|
|||
|
// 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]);
|
|||
|
}
|
|||
|
|
|||
|
Object.defineProperties(this, {
|
|||
|
scheme: {
|
|||
|
get: function(){ return scheme; },
|
|||
|
set: function(value){
|
|||
|
scheme = value.toLowerCase();
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
user: {
|
|||
|
get: function(){ return user; },
|
|||
|
set: function(value){
|
|||
|
user = value;
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
host: {
|
|||
|
get: function(){ return host; },
|
|||
|
set: function(value){
|
|||
|
host = value.toLowerCase();
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
port: {
|
|||
|
get: function(){ return port; },
|
|||
|
set: function(value){
|
|||
|
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.scheme,
|
|||
|
this.user,
|
|||
|
this.host,
|
|||
|
this.port,
|
|||
|
JSON.parse(JSON.stringify(this.parameters)),
|
|||
|
JSON.parse(JSON.stringify(this.headers)));
|
|||
|
},
|
|||
|
|
|||
|
toString: function(){
|
|||
|
var header, parameter, idx, uri,
|
|||
|
headers = [];
|
|||
|
|
|||
|
uri = this.scheme + ':';
|
|||
|
if (this.user) {
|
|||
|
uri += SIP.Utils.escapeUser(this.user) + '@';
|
|||
|
}
|
|||
|
uri += this.host;
|
|||
|
if (this.port || this.port === 0) {
|
|||
|
uri += ':' + this.port;
|
|||
|
}
|
|||
|
|
|||
|
for (parameter in this.parameters) {
|
|||
|
uri += ';' + parameter;
|
|||
|
|
|||
|
if (this.parameters[parameter] !== null) {
|
|||
|
uri += '='+ 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) {
|
|||
|
uri += '?' + headers.join('&');
|
|||
|
}
|
|||
|
|
|||
|
return uri;
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
|
|||
|
/**
|
|||
|
* 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;
|
|||
|
};
|
|||
|
|
|||
|
},{}],30:[function(_dereq_,module,exports){
|
|||
|
/**
|
|||
|
* @fileoverview Utils
|
|||
|
*/
|
|||
|
|
|||
|
module.exports = function (SIP) {
|
|||
|
var Utils;
|
|||
|
|
|||
|
Utils= {
|
|||
|
|
|||
|
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;
|
|||
|
},
|
|||
|
|
|||
|
getPrefixedProperty: function (object, name) {
|
|||
|
if (object == null) {
|
|||
|
return;
|
|||
|
}
|
|||
|
var capitalizedName = name.charAt(0).toUpperCase() + name.slice(1);
|
|||
|
var prefixedNames = [name, 'webkit' + capitalizedName, 'moz' + capitalizedName];
|
|||
|
for (var i in prefixedNames) {
|
|||
|
var property = object[prefixedNames[i]];
|
|||
|
if (property) {
|
|||
|
return property;
|
|||
|
}
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
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',
|
|||
|
'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;
|
|||
|
},
|
|||
|
|
|||
|
/**
|
|||
|
* 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);
|
|||
|
},
|
|||
|
|
|||
|
getAllowedMethods: function(ua) {
|
|||
|
var event,
|
|||
|
allowed = SIP.UA.C.ALLOWED_METHODS.toString();
|
|||
|
|
|||
|
for (event in SIP.UA.C.EVENT_METHODS) {
|
|||
|
if (ua.checkListener(event)) {
|
|||
|
allowed += ','+ SIP.UA.C.EVENT_METHODS[event];
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
return allowed;
|
|||
|
},
|
|||
|
|
|||
|
// MD5 (Message-Digest Algorithm) http://www.webtoolkit.info
|
|||
|
calculateMD5: function(string) {
|
|||
|
function RotateLeft(lValue, iShiftBits) {
|
|||
|
return (lValue<<iShiftBits) | (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)<<lBytePosition));
|
|||
|
lByteCount++;
|
|||
|
}
|
|||
|
lWordCount = (lByteCount-(lByteCount % 4))/4;
|
|||
|
lBytePosition = (lByteCount % 4)*8;
|
|||
|
lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80<<lBytePosition);
|
|||
|
lWordArray[lNumberOfWords-2] = lMessageLength<<3;
|
|||
|
lWordArray[lNumberOfWords-1] = lMessageLength>>>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<x.length;k+=16) {
|
|||
|
AA=a; BB=b; CC=c; DD=d;
|
|||
|
a=FF(a,b,c,d,x[k+0], S11,0xD76AA478);
|
|||
|
d=FF(d,a,b,c,x[k+1], S12,0xE8C7B756);
|
|||
|
c=FF(c,d,a,b,x[k+2], S13,0x242070DB);
|
|||
|
b=FF(b,c,d,a,x[k+3], S14,0xC1BDCEEE);
|
|||
|
a=FF(a,b,c,d,x[k+4], S11,0xF57C0FAF);
|
|||
|
d=FF(d,a,b,c,x[k+5], S12,0x4787C62A);
|
|||
|
c=FF(c,d,a,b,x[k+6], S13,0xA8304613);
|
|||
|
b=FF(b,c,d,a,x[k+7], S14,0xFD469501);
|
|||
|
a=FF(a,b,c,d,x[k+8], S11,0x698098D8);
|
|||
|
d=FF(d,a,b,c,x[k+9], S12,0x8B44F7AF);
|
|||
|
c=FF(c,d,a,b,x[k+10],S13,0xFFFF5BB1);
|
|||
|
b=FF(b,c,d,a,x[k+11],S14,0x895CD7BE);
|
|||
|
a=FF(a,b,c,d,x[k+12],S11,0x6B901122);
|
|||
|
d=FF(d,a,b,c,x[k+13],S12,0xFD987193);
|
|||
|
c=FF(c,d,a,b,x[k+14],S13,0xA679438E);
|
|||
|
b=FF(b,c,d,a,x[k+15],S14,0x49B40821);
|
|||
|
a=GG(a,b,c,d,x[k+1], S21,0xF61E2562);
|
|||
|
d=GG(d,a,b,c,x[k+6], S22,0xC040B340);
|
|||
|
c=GG(c,d,a,b,x[k+11],S23,0x265E5A51);
|
|||
|
b=GG(b,c,d,a,x[k+0], S24,0xE9B6C7AA);
|
|||
|
a=GG(a,b,c,d,x[k+5], S21,0xD62F105D);
|
|||
|
d=GG(d,a,b,c,x[k+10],S22,0x2441453);
|
|||
|
c=GG(c,d,a,b,x[k+15],S23,0xD8A1E681);
|
|||
|
b=GG(b,c,d,a,x[k+4], S24,0xE7D3FBC8);
|
|||
|
a=GG(a,b,c,d,x[k+9], S21,0x21E1CDE6);
|
|||
|
d=GG(d,a,b,c,x[k+14],S22,0xC33707D6);
|
|||
|
c=GG(c,d,a,b,x[k+3], S23,0xF4D50D87);
|
|||
|
b=GG(b,c,d,a,x[k+8], S24,0x455A14ED);
|
|||
|
a=GG(a,b,c,d,x[k+13],S21,0xA9E3E905);
|
|||
|
d=GG(d,a,b,c,x[k+2], S22,0xFCEFA3F8);
|
|||
|
c=GG(c,d,a,b,x[k+7], S23,0x676F02D9);
|
|||
|
b=GG(b,c,d,a,x[k+12],S24,0x8D2A4C8A);
|
|||
|
a=HH(a,b,c,d,x[k+5], S31,0xFFFA3942);
|
|||
|
d=HH(d,a,b,c,x[k+8], S32,0x8771F681);
|
|||
|
c=HH(c,d,a,b,x[k+11],S33,0x6D9D6122);
|
|||
|
b=HH(b,c,d,a,x[k+14],S34,0xFDE5380C);
|
|||
|
a=HH(a,b,c,d,x[k+1], S31,0xA4BEEA44);
|
|||
|
d=HH(d,a,b,c,x[k+4], S32,0x4BDECFA9);
|
|||
|
c=HH(c,d,a,b,x[k+7], S33,0xF6BB4B60);
|
|||
|
b=HH(b,c,d,a,x[k+10],S34,0xBEBFBC70);
|
|||
|
a=HH(a,b,c,d,x[k+13],S31,0x289B7EC6);
|
|||
|
d=HH(d,a,b,c,x[k+0], S32,0xEAA127FA);
|
|||
|
c=HH(c,d,a,b,x[k+3], S33,0xD4EF3085);
|
|||
|
b=HH(b,c,d,a,x[k+6], S34,0x4881D05);
|
|||
|
a=HH(a,b,c,d,x[k+9], S31,0xD9D4D039);
|
|||
|
d=HH(d,a,b,c,x[k+12],S32,0xE6DB99E5);
|
|||
|
c=HH(c,d,a,b,x[k+15],S33,0x1FA27CF8);
|
|||
|
b=HH(b,c,d,a,x[k+2], S34,0xC4AC5665);
|
|||
|
a=II(a,b,c,d,x[k+0], S41,0xF4292244);
|
|||
|
d=II(d,a,b,c,x[k+7], S42,0x432AFF97);
|
|||
|
c=II(c,d,a,b,x[k+14],S43,0xAB9423A7);
|
|||
|
b=II(b,c,d,a,x[k+5], S44,0xFC93A039);
|
|||
|
a=II(a,b,c,d,x[k+12],S41,0x655B59C3);
|
|||
|
d=II(d,a,b,c,x[k+3], S42,0x8F0CCC92);
|
|||
|
c=II(c,d,a,b,x[k+10],S43,0xFFEFF47D);
|
|||
|
b=II(b,c,d,a,x[k+1], S44,0x85845DD1);
|
|||
|
a=II(a,b,c,d,x[k+8], S41,0x6FA87E4F);
|
|||
|
d=II(d,a,b,c,x[k+15],S42,0xFE2CE6E0);
|
|||
|
c=II(c,d,a,b,x[k+6], S43,0xA3014314);
|
|||
|
b=II(b,c,d,a,x[k+13],S44,0x4E0811A1);
|
|||
|
a=II(a,b,c,d,x[k+4], S41,0xF7537E82);
|
|||
|
d=II(d,a,b,c,x[k+11],S42,0xBD3AF235);
|
|||
|
c=II(c,d,a,b,x[k+2], S43,0x2AD7D2BB);
|
|||
|
b=II(b,c,d,a,x[k+9], S44,0xEB86D391);
|
|||
|
a=AddUnsigned(a,AA);
|
|||
|
b=AddUnsigned(b,BB);
|
|||
|
c=AddUnsigned(c,CC);
|
|||
|
d=AddUnsigned(d,DD);
|
|||
|
}
|
|||
|
|
|||
|
var temp = WordToHex(a)+WordToHex(b)+WordToHex(c)+WordToHex(d);
|
|||
|
|
|||
|
return temp.toLowerCase();
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
SIP.Utils = Utils;
|
|||
|
};
|
|||
|
|
|||
|
},{}],31:[function(_dereq_,module,exports){
|
|||
|
/**
|
|||
|
* @fileoverview WebRTC
|
|||
|
*/
|
|||
|
|
|||
|
module.exports = function (Utils, window, MediaHandler, MediaStreamManager) {
|
|||
|
var WebRTC;
|
|||
|
|
|||
|
WebRTC = {};
|
|||
|
|
|||
|
WebRTC.MediaHandler = MediaHandler;
|
|||
|
WebRTC.MediaStreamManager = MediaStreamManager;
|
|||
|
|
|||
|
var _isSupported;
|
|||
|
|
|||
|
WebRTC.isSupported = function () {
|
|||
|
if (_isSupported !== undefined) {
|
|||
|
return _isSupported;
|
|||
|
}
|
|||
|
|
|||
|
WebRTC.MediaStream = Utils.getPrefixedProperty(window, 'MediaStream');
|
|||
|
WebRTC.getUserMedia = Utils.getPrefixedProperty(window.navigator, 'getUserMedia');
|
|||
|
WebRTC.RTCPeerConnection = Utils.getPrefixedProperty(window, 'RTCPeerConnection');
|
|||
|
WebRTC.RTCSessionDescription = Utils.getPrefixedProperty(window, 'RTCSessionDescription');
|
|||
|
|
|||
|
if (WebRTC.getUserMedia && WebRTC.RTCPeerConnection && WebRTC.RTCSessionDescription) {
|
|||
|
WebRTC.getUserMedia = WebRTC.getUserMedia.bind(window.navigator);
|
|||
|
_isSupported = true;
|
|||
|
}
|
|||
|
else {
|
|||
|
_isSupported = false;
|
|||
|
}
|
|||
|
return _isSupported;
|
|||
|
};
|
|||
|
|
|||
|
return WebRTC;
|
|||
|
};
|
|||
|
|
|||
|
},{}],32:[function(_dereq_,module,exports){
|
|||
|
/**
|
|||
|
* @fileoverview MediaHandler
|
|||
|
*/
|
|||
|
|
|||
|
/* MediaHandler
|
|||
|
* @class PeerConnection helper Class.
|
|||
|
* @param {SIP.Session} session
|
|||
|
* @param {Object} [options]
|
|||
|
* @param {SIP.WebRTC.MediaStreamManager} [options.mediaStreamManager]
|
|||
|
* The MediaStreamManager to acquire/release streams from/to.
|
|||
|
* If not provided, a default MediaStreamManager will be used.
|
|||
|
*/
|
|||
|
module.exports = function (SIP) {
|
|||
|
|
|||
|
var MediaHandler = function(session, options) {
|
|||
|
var events = [
|
|||
|
'userMediaRequest',
|
|||
|
'userMedia',
|
|||
|
'userMediaFailed',
|
|||
|
'iceGathering',
|
|||
|
'iceComplete',
|
|||
|
'iceFailed',
|
|||
|
'getDescription',
|
|||
|
'setDescription',
|
|||
|
'dataChannel',
|
|||
|
'addStream'
|
|||
|
];
|
|||
|
options = options || {};
|
|||
|
|
|||
|
this.logger = session.ua.getLogger('sip.invitecontext.mediahandler', session.id);
|
|||
|
this.session = session;
|
|||
|
this.localMedia = null;
|
|||
|
this.ready = true;
|
|||
|
this.mediaStreamManager = options.mediaStreamManager || new SIP.WebRTC.MediaStreamManager();
|
|||
|
this.audioMuted = false;
|
|||
|
this.videoMuted = false;
|
|||
|
|
|||
|
// old init() from here on
|
|||
|
var idx, length, server,
|
|||
|
self = this,
|
|||
|
servers = [],
|
|||
|
stunServers = options.stunServers || null,
|
|||
|
turnServers = options.turnServers || null,
|
|||
|
config = this.session.ua.configuration;
|
|||
|
this.RTCConstraints = options.RTCConstraints || {};
|
|||
|
|
|||
|
if (!stunServers) {
|
|||
|
stunServers = config.stunServers;
|
|||
|
}
|
|||
|
|
|||
|
if(!turnServers) {
|
|||
|
turnServers = config.turnServers;
|
|||
|
}
|
|||
|
|
|||
|
/* Change 'url' to 'urls' whenever this issue is solved:
|
|||
|
* https://code.google.com/p/webrtc/issues/detail?id=2096
|
|||
|
*/
|
|||
|
servers.push({'url': stunServers});
|
|||
|
|
|||
|
length = turnServers.length;
|
|||
|
for (idx = 0; idx < length; idx++) {
|
|||
|
server = turnServers[idx];
|
|||
|
servers.push({
|
|||
|
'url': server.urls,
|
|||
|
'username': server.username,
|
|||
|
'credential': server.password
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
this.peerConnection = new SIP.WebRTC.RTCPeerConnection({'iceServers': servers}, this.RTCConstraints);
|
|||
|
|
|||
|
this.peerConnection.onaddstream = function(e) {
|
|||
|
self.logger.log('stream added: '+ e.stream.id);
|
|||
|
self.render();
|
|||
|
self.emit('addStream', e);
|
|||
|
};
|
|||
|
|
|||
|
this.peerConnection.onremovestream = function(e) {
|
|||
|
self.logger.log('stream removed: '+ e.stream.id);
|
|||
|
};
|
|||
|
|
|||
|
this.peerConnection.onicecandidate = function(e) {
|
|||
|
if (e.candidate) {
|
|||
|
self.logger.log('ICE candidate received: '+ e.candidate.candidate);
|
|||
|
} else if (self.onIceCompleted !== undefined) {
|
|||
|
self.onIceCompleted(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 !== undefined) {
|
|||
|
self.onIceCompleted(this);
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
this.peerConnection.oniceconnectionstatechange = function() { //need e for commented out case
|
|||
|
self.logger.log('ICE connection state changed to "'+ this.iceConnectionState +'"');
|
|||
|
//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 +'"');
|
|||
|
};
|
|||
|
|
|||
|
this.initEvents(events);
|
|||
|
|
|||
|
function selfEmit(mh, event) {
|
|||
|
if (mh.mediaStreamManager.on &&
|
|||
|
mh.mediaStreamManager.checkEvent &&
|
|||
|
mh.mediaStreamManager.checkEvent(event)) {
|
|||
|
mh.mediaStreamManager.on(event, function () {
|
|||
|
mh.emit.apply(mh, [event].concat(Array.prototype.slice.call(arguments)));
|
|||
|
});
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
selfEmit(this, 'userMediaRequest');
|
|||
|
selfEmit(this, 'userMedia');
|
|||
|
selfEmit(this, 'userMediaFailed');
|
|||
|
};
|
|||
|
|
|||
|
MediaHandler.defaultFactory = function defaultFactory (session, options) {
|
|||
|
return new MediaHandler(session, options);
|
|||
|
};
|
|||
|
MediaHandler.defaultFactory.isSupported = function () {
|
|||
|
return SIP.WebRTC.isSupported();
|
|||
|
};
|
|||
|
|
|||
|
MediaHandler.prototype = Object.create(SIP.MediaHandler.prototype, {
|
|||
|
// Functions the session can use
|
|||
|
isReady: {writable: true, value: function isReady () {
|
|||
|
return this.ready;
|
|||
|
}},
|
|||
|
|
|||
|
close: {writable: true, value: function close () {
|
|||
|
this.logger.log('closing PeerConnection');
|
|||
|
// have to check signalingState since this.close() gets called multiple times
|
|||
|
// TODO figure out why that happens
|
|||
|
if(this.peerConnection && this.peerConnection.signalingState !== 'closed') {
|
|||
|
this.peerConnection.close();
|
|||
|
|
|||
|
if(this.localMedia) {
|
|||
|
this.mediaStreamManager.release(this.localMedia);
|
|||
|
}
|
|||
|
}
|
|||
|
}},
|
|||
|
|
|||
|
/**
|
|||
|
* @param {Function} onSuccess
|
|||
|
* @param {Function} onFailure
|
|||
|
* @param {SIP.WebRTC.MediaStream | (getUserMedia constraints)} [mediaHint]
|
|||
|
* the MediaStream (or the constraints describing it) to be used for the session
|
|||
|
*/
|
|||
|
getDescription: {writable: true, value: function getDescription (onSuccess, onFailure, mediaHint) {
|
|||
|
var self = this;
|
|||
|
mediaHint = mediaHint || {};
|
|||
|
if (mediaHint.dataChannel === true) {
|
|||
|
mediaHint.dataChannel = {};
|
|||
|
}
|
|||
|
this.mediaHint = mediaHint;
|
|||
|
|
|||
|
/*
|
|||
|
* 1. acquire stream (skip if MediaStream passed in)
|
|||
|
* 2. addStream
|
|||
|
* 3. createOffer/createAnswer
|
|||
|
* 4. call onSuccess()
|
|||
|
*/
|
|||
|
|
|||
|
/* Last functions first, to quiet JSLint */
|
|||
|
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.createOfferOrAnswer(onSuccess, onFailure, self.RTCConstraints);
|
|||
|
}
|
|||
|
|
|||
|
function acquireSucceeded(stream) {
|
|||
|
self.logger.log('acquired local media stream');
|
|||
|
self.localMedia = stream;
|
|||
|
self.session.connecting();
|
|||
|
self.addStream(
|
|||
|
stream,
|
|||
|
streamAdditionSucceeded,
|
|||
|
onFailure
|
|||
|
);
|
|||
|
}
|
|||
|
|
|||
|
if (self.localMedia) {
|
|||
|
self.logger.log('already have local media');
|
|||
|
streamAdditionSucceeded();
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
self.logger.log('acquiring local media');
|
|||
|
self.mediaStreamManager.acquire(
|
|||
|
acquireSucceeded,
|
|||
|
function acquireFailed(err) {
|
|||
|
self.logger.error('unable to acquire stream');
|
|||
|
self.logger.error(err);
|
|||
|
self.session.connecting();
|
|||
|
onFailure(err);
|
|||
|
},
|
|||
|
mediaHint
|
|||
|
);
|
|||
|
}},
|
|||
|
|
|||
|
/**
|
|||
|
* Message reception.
|
|||
|
* @param {String} type
|
|||
|
* @param {String} sdp
|
|||
|
* @param {Function} onSuccess
|
|||
|
* @param {Function} onFailure
|
|||
|
*/
|
|||
|
setDescription: {writable: true, value: function setDescription (sdp, onSuccess, onFailure) {
|
|||
|
var rawDescription = {
|
|||
|
type: this.hasOffer('local') ? 'answer' : 'offer',
|
|||
|
sdp: sdp
|
|||
|
};
|
|||
|
|
|||
|
this.emit('setDescription', rawDescription);
|
|||
|
|
|||
|
var description = new SIP.WebRTC.RTCSessionDescription(rawDescription);
|
|||
|
this.peerConnection.setRemoteDescription(description, onSuccess, onFailure);
|
|||
|
}},
|
|||
|
|
|||
|
// 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.toggleMuteAudio(true);
|
|||
|
this.toggleMuteVideo(true);
|
|||
|
}},
|
|||
|
|
|||
|
unhold: {writable: true, value: function unhold () {
|
|||
|
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 []');
|
|||
|
return [];
|
|||
|
}
|
|||
|
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]();
|
|||
|
if (streams.length) {
|
|||
|
SIP.WebRTC.MediaStreamManager.render(streams[0], 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'?
|
|||
|
}},
|
|||
|
|
|||
|
createOfferOrAnswer: {writable: true, value: function createOfferOrAnswer (onSuccess, onFailure, constraints) {
|
|||
|
var self = this;
|
|||
|
var methodName;
|
|||
|
|
|||
|
function readySuccess () {
|
|||
|
var sdp = self.peerConnection.localDescription.sdp;
|
|||
|
|
|||
|
sdp = SIP.Hacks.Chrome.needsExplicitlyInactiveSDP(sdp);
|
|||
|
|
|||
|
var sdpWrapper = {
|
|||
|
type: methodName === 'createOffer' ? 'offer' : 'answer',
|
|||
|
sdp: sdp
|
|||
|
};
|
|||
|
|
|||
|
self.emit('getDescription', sdpWrapper);
|
|||
|
|
|||
|
self.ready = true;
|
|||
|
onSuccess(sdpWrapper.sdp);
|
|||
|
}
|
|||
|
|
|||
|
function onSetLocalDescriptionSuccess() {
|
|||
|
if (self.peerConnection.iceGatheringState === 'complete' && self.peerConnection.iceConnectionState === 'connected') {
|
|||
|
readySuccess();
|
|||
|
} else {
|
|||
|
self.onIceCompleted = function(pc) {
|
|||
|
self.logger.log('ICE Gathering Completed');
|
|||
|
self.onIceCompleted = undefined;
|
|||
|
self.emit('iceComplete', pc);
|
|||
|
readySuccess();
|
|||
|
};
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function methodFailed (methodName, e) {
|
|||
|
self.logger.error('peerConnection.' + methodName + ' failed');
|
|||
|
self.logger.error(e);
|
|||
|
self.ready = true;
|
|||
|
onFailure(e);
|
|||
|
}
|
|||
|
|
|||
|
self.ready = false;
|
|||
|
|
|||
|
methodName = self.hasOffer('remote') ? 'createAnswer' : 'createOffer';
|
|||
|
|
|||
|
self.peerConnection[methodName](
|
|||
|
function(sessionDescription){
|
|||
|
self.peerConnection.setLocalDescription(
|
|||
|
sessionDescription,
|
|||
|
onSetLocalDescriptionSuccess,
|
|||
|
methodFailed.bind(null, 'setLocalDescription')
|
|||
|
);
|
|||
|
},
|
|||
|
methodFailed.bind(null, methodName),
|
|||
|
constraints
|
|||
|
);
|
|||
|
}},
|
|||
|
|
|||
|
addStream: {writable: true, value: function addStream (stream, onSuccess, onFailure) {
|
|||
|
try {
|
|||
|
this.peerConnection.addStream(stream);
|
|||
|
} catch(e) {
|
|||
|
this.logger.error('error adding stream');
|
|||
|
this.logger.error(e);
|
|||
|
onFailure(e);
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
onSuccess();
|
|||
|
}},
|
|||
|
|
|||
|
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;
|
|||
|
};
|
|||
|
|
|||
|
},{}],33:[function(_dereq_,module,exports){
|
|||
|
/**
|
|||
|
* @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) {
|
|||
|
|
|||
|
// Default MediaStreamManager provides single-use streams created with getUserMedia
|
|||
|
var MediaStreamManager = function MediaStreamManager (defaultMediaHint) {
|
|||
|
if (!SIP.WebRTC.isSupported()) {
|
|||
|
throw new SIP.Exceptions.NotSupportedError('Media not supported');
|
|||
|
}
|
|||
|
|
|||
|
var events = [
|
|||
|
'userMediaRequest',
|
|||
|
'userMedia',
|
|||
|
'userMediaFailed'
|
|||
|
];
|
|||
|
this.mediaHint = defaultMediaHint || {
|
|||
|
constraints: {audio: true, video: true}
|
|||
|
};
|
|||
|
|
|||
|
this.initEvents(events);
|
|||
|
|
|||
|
// 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('');
|
|||
|
};
|
|||
|
|
|||
|
MediaStreamManager.render = function render (stream, elements) {
|
|||
|
if (!elements) {
|
|||
|
return false;
|
|||
|
}
|
|||
|
|
|||
|
function attachAndPlay (element, stream) {
|
|||
|
(window.attachMediaStream || attachMediaStream)(element, stream);
|
|||
|
ensureMediaPlaying(element);
|
|||
|
}
|
|||
|
|
|||
|
function attachMediaStream(element, stream) {
|
|||
|
if (typeof element.src !== 'undefined') {
|
|||
|
URL.revokeObjectURL(element.src);
|
|||
|
element.src = URL.createObjectURL(stream);
|
|||
|
} else if (typeof (element.srcObject || element.mozSrcObject) !== 'undefined') {
|
|||
|
element.srcObject = element.mozSrcObject = stream;
|
|||
|
} else {
|
|||
|
return false;
|
|||
|
}
|
|||
|
|
|||
|
return true;
|
|||
|
}
|
|||
|
|
|||
|
function ensureMediaPlaying (mediaElement) {
|
|||
|
var interval = 100;
|
|||
|
mediaElement.ensurePlayingIntervalId = SIP.Timers.setInterval(function () {
|
|||
|
if (mediaElement.paused) {
|
|||
|
mediaElement.play();
|
|||
|
}
|
|||
|
else {
|
|||
|
SIP.Timers.clearInterval(mediaElement.ensurePlayingIntervalId);
|
|||
|
}
|
|||
|
}, interval);
|
|||
|
}
|
|||
|
|
|||
|
if (elements.video) {
|
|||
|
if (elements.audio) {
|
|||
|
elements.video.volume = 0;
|
|||
|
}
|
|||
|
attachAndPlay(elements.video, stream);
|
|||
|
}
|
|||
|
if (elements.audio) {
|
|||
|
attachAndPlay(elements.audio, stream);
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
MediaStreamManager.prototype = Object.create(SIP.EventEmitter.prototype, {
|
|||
|
'acquire': {value: function acquire (onSuccess, onFailure, mediaHint) {
|
|||
|
mediaHint = Object.keys(mediaHint || {}).length ? mediaHint : this.mediaHint;
|
|||
|
|
|||
|
var saveSuccess = function (onSuccess, stream, isHintStream) {
|
|||
|
var streamId = MediaStreamManager.streamId(stream);
|
|||
|
this.acquisitions[streamId] = !!isHintStream;
|
|||
|
onSuccess(stream);
|
|||
|
}.bind(this, onSuccess);
|
|||
|
|
|||
|
if (mediaHint.stream) {
|
|||
|
saveSuccess(mediaHint.stream, true);
|
|||
|
} else {
|
|||
|
// Fallback to audio/video enabled if no mediaHint can be found.
|
|||
|
var constraints = mediaHint.constraints ||
|
|||
|
(this.mediaHint && this.mediaHint.constraints) ||
|
|||
|
{audio: true, video: true};
|
|||
|
|
|||
|
/*
|
|||
|
* Make the call asynchronous, so that ICCs have a chance
|
|||
|
* to define callbacks to `userMediaRequest`
|
|||
|
*/
|
|||
|
SIP.Timers.setTimeout(function () {
|
|||
|
this.emit('userMediaRequest', constraints);
|
|||
|
|
|||
|
var emitThenCall = function (eventName, callback) {
|
|||
|
var callbackArgs = Array.prototype.slice.call(arguments, 2);
|
|||
|
// Emit with all of the arguments from the real callback.
|
|||
|
var newArgs = [eventName].concat(callbackArgs);
|
|||
|
|
|||
|
this.emit.apply(this, newArgs);
|
|||
|
|
|||
|
callback.apply(null, callbackArgs);
|
|||
|
}.bind(this);
|
|||
|
|
|||
|
SIP.WebRTC.getUserMedia(
|
|||
|
constraints,
|
|||
|
emitThenCall.bind(this, 'userMedia', saveSuccess),
|
|||
|
emitThenCall.bind(this, 'userMediaFailed', onFailure)
|
|||
|
);
|
|||
|
}.bind(this), 0);
|
|||
|
}
|
|||
|
}},
|
|||
|
|
|||
|
'release': {value: function release (stream) {
|
|||
|
var streamId = MediaStreamManager.streamId(stream);
|
|||
|
if (this.acquisitions[streamId] === false) {
|
|||
|
stream.stop();
|
|||
|
}
|
|||
|
delete this.acquisitions[streamId];
|
|||
|
}},
|
|||
|
});
|
|||
|
|
|||
|
// Return since it will be assigned to a variable.
|
|||
|
return MediaStreamManager;
|
|||
|
};
|
|||
|
|
|||
|
},{}]},{},[18])
|
|||
|
(18)
|
|||
|
});
|