jssip
Version:
the Javascript SIP library
1,064 lines (863 loc) • 30 kB
JavaScript
"use strict";
function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }
function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }
function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
var EventEmitter = require('events').EventEmitter;
var Logger = require('./Logger');
var JsSIP_C = require('./Constants');
var Registrator = require('./Registrator');
var RTCSession = require('./RTCSession');
var Message = require('./Message');
var Options = require('./Options');
var Transactions = require('./Transactions');
var Transport = require('./Transport');
var Utils = require('./Utils');
var Exceptions = require('./Exceptions');
var URI = require('./URI');
var Parser = require('./Parser');
var SIPMessage = require('./SIPMessage');
var sanityCheck = require('./sanityCheck');
var config = require('./Config');
var logger = new Logger('UA');
var 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
};
/**
* The User-Agent class.
* @class JsSIP.UA
* @param {Object} configuration Configuration parameters.
* @throws {JsSIP.Exceptions.ConfigurationError} If a configuration parameter is invalid.
* @throws {TypeError} If no configuration is given.
*/
module.exports = /*#__PURE__*/function (_EventEmitter) {
_inherits(UA, _EventEmitter);
var _super = _createSuper(UA);
_createClass(UA, null, [{
key: "C",
// Expose C object.
get: function get() {
return C;
}
}]);
function UA(configuration) {
var _this;
_classCallCheck(this, UA);
logger.debug('new() [configuration:%o]', configuration);
_this = _super.call(this);
_this._cache = {
credentials: {}
};
_this._configuration = Object.assign({}, config.settings);
_this._dynConfiguration = {};
_this._dialogs = {}; // User actions outside any session/dialog (MESSAGE/OPTIONS).
_this._applicants = {};
_this._sessions = {};
_this._transport = null;
_this._contact = null;
_this._status = C.STATUS_INIT;
_this._error = null;
_this._transactions = {
nist: {},
nict: {},
ist: {},
ict: {}
}; // Custom UA empty object for high level use.
_this._data = {};
_this._closeTimer = null; // Check configuration argument.
if (configuration === undefined) {
throw new TypeError('Not enough arguments');
} // Load configuration.
try {
_this._loadConfig(configuration);
} catch (e) {
_this._status = C.STATUS_NOT_READY;
_this._error = C.CONFIGURATION_ERROR;
throw e;
} // Initialize registrator.
_this._registrator = new Registrator(_assertThisInitialized(_this));
return _this;
}
_createClass(UA, [{
key: "start",
// =================
// High Level API
// =================
/**
* Connect to the server if status = STATUS_INIT.
* Resume UA after being closed.
*/
value: function start() {
logger.debug('start()');
if (this._status === C.STATUS_INIT) {
this._transport.connect();
} else if (this._status === C.STATUS_USER_CLOSED) {
logger.debug('restarting UA'); // Disconnect.
if (this._closeTimer !== null) {
clearTimeout(this._closeTimer);
this._closeTimer = null;
this._transport.disconnect();
} // Reconnect.
this._status = C.STATUS_INIT;
this._transport.connect();
} else if (this._status === C.STATUS_READY) {
logger.debug('UA is in READY status, not restarted');
} else {
logger.debug('ERROR: connection is down, Auto-Recovery system is trying to reconnect');
} // Set dynamic configuration.
this._dynConfiguration.register = this._configuration.register;
}
/**
* Register.
*/
}, {
key: "register",
value: function register() {
logger.debug('register()');
this._dynConfiguration.register = true;
this._registrator.register();
}
/**
* Unregister.
*/
}, {
key: "unregister",
value: function unregister(options) {
logger.debug('unregister()');
this._dynConfiguration.register = false;
this._registrator.unregister(options);
}
/**
* Get the Registrator instance.
*/
}, {
key: "registrator",
value: function registrator() {
return this._registrator;
}
/**
* Registration state.
*/
}, {
key: "isRegistered",
value: function isRegistered() {
return this._registrator.registered;
}
/**
* Connection state.
*/
}, {
key: "isConnected",
value: function isConnected() {
return this._transport.isConnected();
}
/**
* Make an outgoing call.
*
* -param {String} target
* -param {Object} [options]
*
* -throws {TypeError}
*
*/
}, {
key: "call",
value: function call(target, options) {
logger.debug('call()');
var session = new RTCSession(this);
session.connect(target, options);
return session;
}
/**
* Send a message.
*
* -param {String} target
* -param {String} body
* -param {Object} [options]
*
* -throws {TypeError}
*
*/
}, {
key: "sendMessage",
value: function sendMessage(target, body, options) {
logger.debug('sendMessage()');
var message = new Message(this);
message.send(target, body, options);
return message;
}
/**
* Send a SIP OPTIONS.
*
* -param {String} target
* -param {String} [body]
* -param {Object} [options]
*
* -throws {TypeError}
*
*/
}, {
key: "sendOptions",
value: function sendOptions(target, body, options) {
logger.debug('sendOptions()');
var message = new Options(this);
message.send(target, body, options);
return message;
}
/**
* Terminate ongoing sessions.
*/
}, {
key: "terminateSessions",
value: function terminateSessions(options) {
logger.debug('terminateSessions()');
for (var idx in this._sessions) {
if (!this._sessions[idx].isEnded()) {
this._sessions[idx].terminate(options);
}
}
}
/**
* Gracefully close.
*
*/
}, {
key: "stop",
value: function stop() {
var _this2 = this;
logger.debug('stop()'); // Remove dynamic settings.
this._dynConfiguration = {};
if (this._status === C.STATUS_USER_CLOSED) {
logger.debug('UA already closed');
return;
} // Close registrator.
this._registrator.close(); // If there are session wait a bit so CANCEL/BYE can be sent and their responses received.
var num_sessions = Object.keys(this._sessions).length; // Run _terminate_ on every Session.
for (var session in this._sessions) {
if (Object.prototype.hasOwnProperty.call(this._sessions, session)) {
logger.debug("closing session ".concat(session));
try {
this._sessions[session].terminate();
} catch (error) {}
}
} // Run _close_ on every applicant.
for (var applicant in this._applicants) {
if (Object.prototype.hasOwnProperty.call(this._applicants, applicant)) try {
this._applicants[applicant].close();
} catch (error) {}
}
this._status = C.STATUS_USER_CLOSED;
var num_transactions = Object.keys(this._transactions.nict).length + Object.keys(this._transactions.nist).length + Object.keys(this._transactions.ict).length + Object.keys(this._transactions.ist).length;
if (num_transactions === 0 && num_sessions === 0) {
this._transport.disconnect();
} else {
this._closeTimer = setTimeout(function () {
_this2._closeTimer = null;
_this2._transport.disconnect();
}, 2000);
}
}
/**
* Normalice a string into a valid SIP request URI
* -param {String} target
* -returns {JsSIP.URI|undefined}
*/
}, {
key: "normalizeTarget",
value: function normalizeTarget(target) {
return Utils.normalizeTarget(target, this._configuration.hostport_params);
}
/**
* Allow retrieving configuration and autogenerated fields in runtime.
*/
}, {
key: "get",
value: function get(parameter) {
switch (parameter) {
case 'authorization_user':
return this._configuration.authorization_user;
case 'realm':
return this._configuration.realm;
case 'ha1':
return this._configuration.ha1;
case 'authorization_jwt':
return this._configuration.authorization_jwt;
default:
logger.warn('get() | cannot get "%s" parameter in runtime', parameter);
return undefined;
}
}
/**
* Allow configuration changes in runtime.
* Returns true if the parameter could be set.
*/
}, {
key: "set",
value: function set(parameter, value) {
switch (parameter) {
case 'authorization_user':
{
this._configuration.authorization_user = String(value);
break;
}
case 'password':
{
this._configuration.password = String(value);
break;
}
case 'realm':
{
this._configuration.realm = String(value);
break;
}
case 'ha1':
{
this._configuration.ha1 = String(value); // Delete the plain SIP password.
this._configuration.password = null;
break;
}
case 'authorization_jwt':
{
this._configuration.authorization_jwt = String(value);
break;
}
case 'display_name':
{
this._configuration.display_name = value;
break;
}
case 'extra_headers':
{
this._configuration.extra_headers = value;
break;
}
default:
logger.warn('set() | cannot set "%s" parameter in runtime', parameter);
return false;
}
return true;
} // ==========================
// Event Handlers.
// ==========================
/**
* new Transaction
*/
}, {
key: "newTransaction",
value: function newTransaction(transaction) {
this._transactions[transaction.type][transaction.id] = transaction;
this.emit('newTransaction', {
transaction: transaction
});
}
/**
* Transaction destroyed.
*/
}, {
key: "destroyTransaction",
value: function destroyTransaction(transaction) {
delete this._transactions[transaction.type][transaction.id];
this.emit('transactionDestroyed', {
transaction: transaction
});
}
/**
* new Dialog
*/
}, {
key: "newDialog",
value: function newDialog(dialog) {
this._dialogs[dialog.id] = dialog;
}
/**
* Dialog destroyed.
*/
}, {
key: "destroyDialog",
value: function destroyDialog(dialog) {
delete this._dialogs[dialog.id];
}
/**
* new Message
*/
}, {
key: "newMessage",
value: function newMessage(message, data) {
this._applicants[message] = message;
this.emit('newMessage', data);
}
/**
* new Options
*/
}, {
key: "newOptions",
value: function newOptions(message, data) {
this._applicants[message] = message;
this.emit('newOptions', data);
}
/**
* Message destroyed.
*/
}, {
key: "destroyMessage",
value: function destroyMessage(message) {
delete this._applicants[message];
}
/**
* new RTCSession
*/
}, {
key: "newRTCSession",
value: function newRTCSession(session, data) {
this._sessions[session.id] = session;
this.emit('newRTCSession', data);
}
/**
* RTCSession destroyed.
*/
}, {
key: "destroyRTCSession",
value: function destroyRTCSession(session) {
delete this._sessions[session.id];
}
/**
* Registered
*/
}, {
key: "registered",
value: function registered(data) {
this.emit('registered', data);
}
/**
* Unregistered
*/
}, {
key: "unregistered",
value: function unregistered(data) {
this.emit('unregistered', data);
}
/**
* Registration Failed
*/
}, {
key: "registrationFailed",
value: function registrationFailed(data) {
this.emit('registrationFailed', data);
} // =========================
// ReceiveRequest.
// =========================
/**
* Request reception
*/
}, {
key: "receiveRequest",
value: function receiveRequest(request) {
var method = request.method; // Check that request URI points to us.
if (request.ruri.user !== this._configuration.uri.user && request.ruri.user !== this._contact.uri.user) {
logger.debug('Request-URI does not point to us');
if (request.method !== JsSIP_C.ACK) {
request.reply_sl(404);
}
return;
} // Check request URI scheme.
if (request.ruri.scheme === JsSIP_C.SIPS) {
request.reply_sl(416);
return;
} // Check transaction.
if (Transactions.checkTransaction(this, request)) {
return;
} // Create the server transaction.
if (method === JsSIP_C.INVITE) {
/* eslint-disable no-new */
new Transactions.InviteServerTransaction(this, this._transport, request);
/* eslint-enable no-new */
} else if (method !== JsSIP_C.ACK && method !== JsSIP_C.CANCEL) {
/* eslint-disable no-new */
new Transactions.NonInviteServerTransaction(this, this._transport, request);
/* eslint-enable no-new */
}
/* 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 === JsSIP_C.OPTIONS) {
if (this.listeners('newOptions').length === 0) {
request.reply(200);
return;
}
var message = new Options(this);
message.init_incoming(request);
} else if (method === JsSIP_C.MESSAGE) {
if (this.listeners('newMessage').length === 0) {
request.reply(405);
return;
}
var _message = new Message(this);
_message.init_incoming(request);
} else if (method === JsSIP_C.INVITE) {
// Initial INVITE.
if (!request.to_tag && this.listeners('newRTCSession').length === 0) {
request.reply(405);
return;
}
}
var dialog;
var session; // Initial Request.
if (!request.to_tag) {
switch (method) {
case JsSIP_C.INVITE:
if (window.RTCPeerConnection) {
// TODO
if (request.hasHeader('replaces')) {
var replaces = request.replaces;
dialog = this._findDialog(replaces.call_id, replaces.from_tag, replaces.to_tag);
if (dialog) {
session = dialog.owner;
if (!session.isEnded()) {
session.receiveRequest(request);
} else {
request.reply(603);
}
} else {
request.reply(481);
}
} else {
session = new RTCSession(this);
session.init_incoming(request);
}
} else {
logger.warn('INVITE received but WebRTC is not supported');
request.reply(488);
}
break;
case JsSIP_C.BYE:
// Out of dialog BYE received.
request.reply(481);
break;
case JsSIP_C.CANCEL:
session = this._findSession(request);
if (session) {
session.receiveRequest(request);
} else {
logger.debug('received CANCEL request for a non existent session');
}
break;
case JsSIP_C.ACK:
/* Absorb it.
* ACK request without a corresponding Invite Transaction
* and without To tag.
*/
break;
case JsSIP_C.NOTIFY:
// Receive new sip event.
this.emit('sipEvent', {
event: request.event,
request: request
});
request.reply(200);
break;
default:
request.reply(405);
break;
}
} // In-dialog request.
else {
dialog = this._findDialog(request.call_id, request.from_tag, request.to_tag);
if (dialog) {
dialog.receiveRequest(request);
} else if (method === JsSIP_C.NOTIFY) {
session = this._findSession(request);
if (session) {
session.receiveRequest(request);
} else {
logger.debug('received NOTIFY request for a non existent subscription');
request.reply(481, 'Subscription does not exist');
}
}
/* RFC3261 12.2.2
* Request with to tag, but no matching dialog found.
* Exception: ACK for an Invite request for which a dialog has not
* been created.
*/
else if (method !== JsSIP_C.ACK) {
request.reply(481);
}
}
} // =================
// Utils.
// =================
/**
* Get the session to which the request belongs to, if any.
*/
}, {
key: "_findSession",
value: function _findSession(_ref) {
var call_id = _ref.call_id,
from_tag = _ref.from_tag,
to_tag = _ref.to_tag;
var sessionIDa = call_id + from_tag;
var sessionA = this._sessions[sessionIDa];
var sessionIDb = call_id + to_tag;
var sessionB = this._sessions[sessionIDb];
if (sessionA) {
return sessionA;
} else if (sessionB) {
return sessionB;
} else {
return null;
}
}
/**
* Get the dialog to which the request belongs to, if any.
*/
}, {
key: "_findDialog",
value: function _findDialog(call_id, from_tag, to_tag) {
var id = call_id + from_tag + to_tag;
var dialog = this._dialogs[id];
if (dialog) {
return dialog;
} else {
id = call_id + to_tag + from_tag;
dialog = this._dialogs[id];
if (dialog) {
return dialog;
} else {
return null;
}
}
}
}, {
key: "_loadConfig",
value: function _loadConfig(configuration) {
// Check and load the given configuration.
try {
config.load(this._configuration, configuration);
} catch (e) {
throw e;
} // Post Configuration Process.
// Allow passing 0 number as display_name.
if (this._configuration.display_name === 0) {
this._configuration.display_name = '0';
} // Instance-id for GRUU.
if (!this._configuration.instance_id) {
this._configuration.instance_id = Utils.newUUID();
} // Jssip_id instance parameter. Static random tag of length 5.
this._configuration.jssip_id = Utils.createRandomToken(5); // String containing this._configuration.uri without scheme and user.
var hostport_params = this._configuration.uri.clone();
hostport_params.user = null;
this._configuration.hostport_params = hostport_params.toString().replace(/^sip:/i, ''); // Transport.
try {
this._transport = new Transport(this._configuration.sockets, {
// Recovery options.
max_interval: this._configuration.connection_recovery_max_interval,
min_interval: this._configuration.connection_recovery_min_interval
}); // Transport event callbacks.
this._transport.onconnecting = onTransportConnecting.bind(this);
this._transport.onconnect = onTransportConnect.bind(this);
this._transport.ondisconnect = onTransportDisconnect.bind(this);
this._transport.ondata = onTransportData.bind(this);
} catch (e) {
logger.warn(e);
throw new Exceptions.ConfigurationError('sockets', this._configuration.sockets);
} // Remove sockets instance from configuration object.
delete this._configuration.sockets; // Check whether authorization_user is explicitly defined.
// Take 'this._configuration.uri.user' value if not.
if (!this._configuration.authorization_user) {
this._configuration.authorization_user = this._configuration.uri.user;
} // If no 'registrar_server' is set use the 'uri' value without user portion and
// without URI params/headers.
if (!this._configuration.registrar_server) {
var registrar_server = this._configuration.uri.clone();
registrar_server.user = null;
registrar_server.clearParams();
registrar_server.clearHeaders();
this._configuration.registrar_server = registrar_server;
} // User no_answer_timeout.
this._configuration.no_answer_timeout *= 1000; // Via Host.
if (this._configuration.contact_uri) {
this._configuration.via_host = this._configuration.contact_uri.host;
} // Contact URI.
else {
this._configuration.contact_uri = new URI('sip', Utils.createRandomToken(8), this._configuration.via_host, null, {
transport: 'ws'
});
}
this._contact = {
pub_gruu: null,
temp_gruu: null,
uri: this._configuration.contact_uri,
toString: function toString() {
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
var anonymous = options.anonymous || null;
var outbound = options.outbound || null;
var contact = '<';
if (anonymous) {
contact += this.temp_gruu || 'sip:anonymous@anonymous.invalid;transport=ws';
} else {
contact += this.pub_gruu || this.uri.toString();
}
if (outbound && (anonymous ? !this.temp_gruu : !this.pub_gruu)) {
contact += ';ob';
}
contact += '>';
return contact;
}
}; // Seal the configuration.
var writable_parameters = ['authorization_user', 'password', 'realm', 'ha1', 'authorization_jwt', 'display_name', 'register', 'extra_headers'];
for (var parameter in this._configuration) {
if (Object.prototype.hasOwnProperty.call(this._configuration, parameter)) {
if (writable_parameters.indexOf(parameter) !== -1) {
Object.defineProperty(this._configuration, parameter, {
writable: true,
configurable: false
});
} else {
Object.defineProperty(this._configuration, parameter, {
writable: false,
configurable: false
});
}
}
}
logger.debug('configuration parameters after validation:');
for (var _parameter in this._configuration) {
// Only show the user user configurable parameters.
if (Object.prototype.hasOwnProperty.call(config.settings, _parameter)) {
switch (_parameter) {
case 'uri':
case 'registrar_server':
logger.debug("- ".concat(_parameter, ": ").concat(this._configuration[_parameter]));
break;
case 'password':
case 'ha1':
case 'authorization_jwt':
logger.debug("- ".concat(_parameter, ": NOT SHOWN"));
break;
default:
logger.debug("- ".concat(_parameter, ": ").concat(JSON.stringify(this._configuration[_parameter])));
}
}
}
return;
}
}, {
key: "C",
get: function get() {
return C;
}
}, {
key: "status",
get: function get() {
return this._status;
}
}, {
key: "contact",
get: function get() {
return this._contact;
}
}, {
key: "configuration",
get: function get() {
return this._configuration;
}
}, {
key: "transport",
get: function get() {
return this._transport;
}
}]);
return UA;
}(EventEmitter);
/**
* Transport event handlers
*/
// Transport connecting event.
function onTransportConnecting(data) {
this.emit('connecting', data);
} // Transport connected event.
function onTransportConnect(data) {
if (this._status === C.STATUS_USER_CLOSED) {
return;
}
this._status = C.STATUS_READY;
this._error = null;
this.emit('connected', data);
if (this._dynConfiguration.register) {
this._registrator.register();
}
} // Transport disconnected event.
function onTransportDisconnect(data) {
// Run _onTransportError_ callback on every client transaction using _transport_.
var client_transactions = ['nict', 'ict', 'nist', 'ist'];
for (var _i = 0, _client_transactions = client_transactions; _i < _client_transactions.length; _i++) {
var type = _client_transactions[_i];
for (var id in this._transactions[type]) {
if (Object.prototype.hasOwnProperty.call(this._transactions[type], id)) {
this._transactions[type][id].onTransportError();
}
}
}
this.emit('disconnected', data); // Call registrator _onTransportClosed_.
this._registrator.onTransportClosed();
if (this._status !== C.STATUS_USER_CLOSED) {
this._status = C.STATUS_NOT_READY;
this._error = C.NETWORK_ERROR;
}
} // Transport data event.
function onTransportData(data) {
var transport = data.transport;
var message = data.message;
message = Parser.parseMessage(message, this);
if (!message) {
return;
}
if (this._status === C.STATUS_USER_CLOSED && message instanceof SIPMessage.IncomingRequest) {
return;
} // Do some sanity check.
if (!sanityCheck(message, this, transport)) {
return;
}
if (message instanceof SIPMessage.IncomingRequest) {
message.transport = transport;
this.receiveRequest(message);
} else if (message instanceof SIPMessage.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.
*/
var transaction;
switch (message.method) {
case JsSIP_C.INVITE:
transaction = this._transactions.ict[message.via_branch];
if (transaction) {
transaction.receiveResponse(message);
}
break;
case JsSIP_C.ACK:
// Just in case ;-).
break;
default:
transaction = this._transactions.nict[message.via_branch];
if (transaction) {
transaction.receiveResponse(message);
}
break;
}
}
}