UNPKG

jssip

Version:

the Javascript SIP library

603 lines (462 loc) 21 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 _createForOfIteratorHelper(o, allowArrayLike) { var it; if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = o[Symbol.iterator](); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } 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 Utils = require('./Utils'); var Grammar = require('./Grammar'); var SIPMessage = require('./SIPMessage'); var RequestSender = require('./RequestSender'); var Dialog = require('./Dialog'); var logger = new Logger('Subscriber'); /** * Termination codes. */ var C = { // Termination codes. SUBSCRIBE_RESPONSE_TIMEOUT: 0, SUBSCRIBE_TRANSPORT_ERROR: 1, SUBSCRIBE_NON_OK_RESPONSE: 2, SUBSCRIBE_BAD_OK_RESPONSE: 3, SUBSCRIBE_FAILED_AUTHENTICATION: 4, UNSUBSCRIBE_TIMEOUT: 5, RECEIVE_FINAL_NOTIFY: 6, RECEIVE_BAD_NOTIFY: 7, // Subscriber states. STATE_PENDING: 0, STATE_ACTIVE: 1, STATE_TERMINATED: 2, STATE_INIT: 3, STATE_NOTIFY_WAIT: 4 }; /** * RFC 6665 Subscriber implementation. */ module.exports = /*#__PURE__*/function (_EventEmitter) { _inherits(Subscriber, _EventEmitter); var _super = _createSuper(Subscriber); _createClass(Subscriber, [{ key: "C", get: function get() { return C; } /** * @param {UA} ua - reference to JsSIP.UA * @param {string} target * @param {string} eventName - Event header value. May end with optional ;id=xxx * @param {string} accept - Accept header value. * * @param {SubscriberOption} options - optional parameters. * @param {number} expires - Expires header value. Default is 900. * @param {string} contentType - Content-Type header value. Used for SUBSCRIBE with body * @param {string} allowEvents - Allow-Events header value. * @param {RequestParams} params - Will have priority over ua.configuration. * If set please define: to_uri, to_display_name, from_uri, from_display_name * @param {Array<string>} extraHeaders - Additional SIP headers. */ }], [{ key: "C", /** * Expose C object. */ get: function get() { return C; } }]); function Subscriber(ua, target, eventName, accept, _ref) { var _this; var expires = _ref.expires, contentType = _ref.contentType, allowEvents = _ref.allowEvents, params = _ref.params, extraHeaders = _ref.extraHeaders; _classCallCheck(this, Subscriber); logger.debug('new'); _this = _super.call(this); // Check that arguments are defined if (!target) { throw new TypeError('target is undefined'); } if (!eventName) { throw new TypeError('eventName is undefined'); } if (!accept) { throw new TypeError('accept is undefined'); } _this._ua = ua; _this._target = target; if (expires !== 0 && !expires) { expires = 900; } _this._expires = expires; // Used to subscribe with body. _this._content_type = contentType; // Set initial subscribe parameters. _this._params = Utils.cloneObject(params); if (!_this._params.from_uri) { _this._params.from_uri = _this._ua.configuration.uri; } _this._params.from_tag = Utils.newTag(); _this._params.to_tag = null; _this._params.call_id = Utils.createRandomToken(20); // Create subscribe cseq if not defined custom cseq. if (_this._params.cseq === undefined) { _this._params.cseq = Math.floor(Math.random() * 10000 + 1); } // Subscriber state. _this._state = C.STATE_INIT; // Dialog _this._dialog = null; // To refresh subscription. _this._expires_timer = null; _this._expires_timestamp = null; // To prevent duplicate un-subscribe sending. _this._send_unsubscribe = false; // After send un-subscribe wait final notify limited time. _this._unsubscribe_timeout_timer = null; // Custom session empty object for high level use. _this.data = {}; var parsed = Grammar.parse(eventName, 'Event'); if (parsed === -1) { throw new TypeError('eventName - wrong format'); } _this._event_name = parsed.event; _this._event_id = parsed.params && parsed.params.id; var eventValue = _this._event_name; if (_this._event_id) { eventValue += ";id=".concat(_this._event_id); } _this._headers = Utils.cloneArray(extraHeaders); _this._headers = _this._headers.concat(["Event: ".concat(eventValue), "Expires: ".concat(_this._expires), "Accept: ".concat(accept)]); if (!_this._headers.find(function (header) { return header.startsWith('Contact'); })) { var contact = "Contact: ".concat(_this._ua._contact.toString()); _this._headers.push(contact); } if (allowEvents) { _this._headers.push("Allow-Events: ".concat(allowEvents)); } // To enqueue subscribes created before receive initial subscribe OK. _this._queue = []; return _this; } _createClass(Subscriber, [{ key: "onRequestTimeout", value: function onRequestTimeout() { this._dialogTerminated(C.SUBSCRIBE_RESPONSE_TIMEOUT); } }, { key: "onTransportError", value: function onTransportError() { this._dialogTerminated(C.SUBSCRIBE_TRANSPORT_ERROR); } /** * Dialog callback. */ }, { key: "receiveRequest", value: function receiveRequest(request) { if (request.method !== JsSIP_C.NOTIFY) { logger.warn('received non-NOTIFY request'); request.reply(405); return; } // RFC 6665 8.2.1. Check if event header matches. var event_header = request.parseHeader('Event'); if (!event_header) { logger.warn('missed Event header'); request.reply(400); this._dialogTerminated(C.RECEIVE_BAD_NOTIFY); return; } var event_name = event_header.event; var event_id = event_header.params && event_header.params.id; if (event_name !== this._event_name || event_id !== this._event_id) { logger.warn('Event header does not match SUBSCRIBE'); request.reply(489); this._dialogTerminated(C.RECEIVE_BAD_NOTIFY); return; } // Process Subscription-State header. var subs_state = request.parseHeader('subscription-state'); if (!subs_state) { logger.warn('missed Subscription-State header'); request.reply(400); this._dialogTerminated(C.RECEIVE_BAD_NOTIFY); return; } request.reply(200); var new_state = this._stateStringToNumber(subs_state.state); var prev_state = this._state; if (prev_state !== C.STATE_TERMINATED && new_state !== C.STATE_TERMINATED) { this._state = new_state; if (subs_state.expires !== undefined) { var expires = subs_state.expires; var expires_timestamp = new Date().getTime() + expires * 1000; var max_time_deviation = 2000; // Expiration time is shorter and the difference is not too small. if (this._expires_timestamp - expires_timestamp > max_time_deviation) { logger.debug('update sending re-SUBSCRIBE time'); this._scheduleSubscribe(expires); } } } if (prev_state !== C.STATE_PENDING && new_state === C.STATE_PENDING) { logger.debug('emit "pending"'); this.emit('pending'); } else if (prev_state !== C.STATE_ACTIVE && new_state === C.STATE_ACTIVE) { logger.debug('emit "active"'); this.emit('active'); } var body = request.body; // Check if the notify is final. var is_final = new_state === C.STATE_TERMINATED; // Notify event fired only for notify with body. if (body) { var content_type = request.getHeader('content-type'); logger.debug('emit "notify"'); this.emit('notify', is_final, request, body, content_type); } if (is_final) { var reason = subs_state.reason; var retry_after = undefined; if (subs_state.params && subs_state.params['retry-after'] !== undefined) { retry_after = parseInt(subs_state.params['retry-after']); } this._dialogTerminated(C.RECEIVE_FINAL_NOTIFY, reason, retry_after); } } /** * User API */ /** * Send the initial (non-fetch) and subsequent subscribe. * @param {string} body - subscribe request body. */ }, { key: "subscribe", value: function subscribe() { var body = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; logger.debug('subscribe()'); if (this._state === C.STATE_INIT) { this._sendInitialSubscribe(body, this._headers); } else { this._sendSubsequentSubscribe(body, this._headers); } } /** * terminate. * Send un-subscribe or fetch-subscribe (with Expires: 0). * @param {string} body - un-subscribe request body */ }, { key: "terminate", value: function terminate() { var _this2 = this; var body = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; logger.debug('terminate()'); // Prevent duplication un-subscribe sending. if (this._send_unsubscribe) { logger.warn('unsubscribe has already been sent'); return; } this._send_unsubscribe = true; // Set header Expires: 0. var headers = this._headers.map(function (s) { return s.startsWith('Expires') ? 'Expires: 0' : s; }); if (this._state === C.STATE_INIT) { // fetch-subscribe - initial subscribe with Expires: 0. this._sendInitialSubscribe(body, headers); } else { this._sendSubsequentSubscribe(body, headers); } // Waiting for the final notify for a while. var final_notify_timeout = 30000; this._unsubscribe_timeout_timer = setTimeout(function () { _this2._dialogTerminated(C.UNSUBSCRIBE_TIMEOUT); }, final_notify_timeout); } /** * Get dialog state. */ }, { key: "_sendInitialSubscribe", /** * Private API. */ value: function _sendInitialSubscribe(body, headers) { var _this3 = this; if (body) { if (!this._content_type) { throw new TypeError('content_type is undefined'); } headers = headers.slice(); headers.push("Content-Type: ".concat(this._content_type)); } this._state = C.STATE_NOTIFY_WAIT; var request = new SIPMessage.OutgoingRequest(JsSIP_C.SUBSCRIBE, this._ua.normalizeTarget(this._target), this._ua, this._params, headers, body); var request_sender = new RequestSender(this._ua, request, { onRequestTimeout: function onRequestTimeout() { _this3.onRequestTimeout(); }, onTransportError: function onTransportError() { _this3.onTransportError(); }, onReceiveResponse: function onReceiveResponse(response) { _this3._receiveSubscribeResponse(response); } }); request_sender.send(); } }, { key: "_receiveSubscribeResponse", value: function _receiveSubscribeResponse(response) { if (response.status_code >= 200 && response.status_code < 300) { // Create dialog if (this._dialog === null) { var dialog = new Dialog(this, response, 'UAC'); if (dialog.error) { // OK response without Contact logger.warn(dialog.error); this._dialogTerminated(C.SUBSCRIBE_BAD_OK_RESPONSE); return; } this._dialog = dialog; logger.debug('emit "dialogCreated"'); this.emit('dialogCreated'); // Subsequent subscribes saved in the queue until dialog created. var _iterator = _createForOfIteratorHelper(this._queue), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var subscribe = _step.value; logger.debug('dequeue subscribe'); this._sendSubsequentSubscribe(subscribe.body, subscribe.headers); } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } } // Check expires value. var expires_value = response.getHeader('expires'); if (expires_value !== 0 && !expires_value) { logger.warn('response without Expires header'); // RFC 6665 3.1.1 subscribe OK response must contain Expires header. // Use workaround expires value. expires_value = '900'; } var expires = parseInt(expires_value); if (expires > 0) { this._scheduleSubscribe(expires); } } else if (response.status_code === 401 || response.status_code === 407) { this._dialogTerminated(C.SUBSCRIBE_FAILED_AUTHENTICATION); } else if (response.status_code >= 300) { this._dialogTerminated(C.SUBSCRIBE_NON_OK_RESPONSE); } } }, { key: "_sendSubsequentSubscribe", value: function _sendSubsequentSubscribe(body, headers) { var _this4 = this; if (this._state === C.STATE_TERMINATED) { return; } if (!this._dialog) { logger.debug('enqueue subscribe'); this._queue.push({ body: body, headers: headers.slice() }); return; } if (body) { if (!this._content_type) { throw new TypeError('content_type is undefined'); } headers = headers.slice(); headers.push("Content-Type: ".concat(this._content_type)); } this._dialog.sendRequest(JsSIP_C.SUBSCRIBE, { body: body, extraHeaders: headers, eventHandlers: { onRequestTimeout: function onRequestTimeout() { _this4.onRequestTimeout(); }, onTransportError: function onTransportError() { _this4.onTransportError(); }, onSuccessResponse: function onSuccessResponse(response) { _this4._receiveSubscribeResponse(response); }, onErrorResponse: function onErrorResponse(response) { _this4._receiveSubscribeResponse(response); }, onDialogError: function onDialogError(response) { _this4._receiveSubscribeResponse(response); } } }); } }, { key: "_dialogTerminated", value: function _dialogTerminated(terminationCode) { var reason = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : undefined; var retryAfter = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : undefined; // To prevent duplicate emit terminated event. if (this._state === C.STATE_TERMINATED) { return; } this._state = C.STATE_TERMINATED; // Clear timers. clearTimeout(this._expires_timer); clearTimeout(this._unsubscribe_timeout_timer); if (this._dialog) { this._dialog.terminate(); this._dialog = null; } logger.debug("emit \"terminated\" code=".concat(terminationCode)); this.emit('terminated', terminationCode, reason, retryAfter); } }, { key: "_scheduleSubscribe", value: function _scheduleSubscribe(expires) { var _this5 = this; var timeout = expires >= 140 ? expires * 1000 / 2 + Math.floor((expires / 2 - 70) * 1000 * Math.random()) : expires * 1000 - 5000; this._expires_timestamp = new Date().getTime() + expires * 1000; logger.debug("next SUBSCRIBE will be sent in ".concat(Math.floor(timeout / 1000), " sec")); clearTimeout(this._expires_timer); this._expires_timer = setTimeout(function () { _this5._expires_timer = null; _this5._sendSubsequentSubscribe(null, _this5._headers); }, timeout); } }, { key: "_stateStringToNumber", value: function _stateStringToNumber(strState) { switch (strState) { case 'pending': return C.STATE_PENDING; case 'active': return C.STATE_ACTIVE; case 'terminated': return C.STATE_TERMINATED; case 'init': return C.STATE_INIT; case 'notify_wait': return C.STATE_NOTIFY_WAIT; default: throw new TypeError('wrong state value'); } } }, { key: "state", get: function get() { return this._state; } /** * Get dialog id. */ }, { key: "id", get: function get() { return this._dialog ? this._dialog.id : null; } }]); return Subscriber; }(EventEmitter);