UNPKG

jssip

Version:

the Javascript SIP library

1,064 lines (863 loc) 30 kB
"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; } } }