UNPKG

jssip

Version:

The Javascript SIP library

307 lines (306 loc) 13.3 kB
"use strict"; const Logger = require('./Logger'); const Utils = require('./Utils'); const JsSIP_C = require('./Constants'); const SIPMessage = require('./SIPMessage'); const RequestSender = require('./RequestSender'); const logger = new Logger('Registrator'); const MIN_REGISTER_EXPIRES = 10; // In seconds. module.exports = class Registrator { constructor(ua, transport) { // Force reg_id to 1. this._reg_id = 1; this._ua = ua; this._transport = transport; this._registrar = ua.configuration.registrar_server; this._expires = ua.configuration.register_expires; // Call-ID and CSeq values RFC3261 10.2. this._call_id = Utils.createRandomToken(22); this._cseq = 0; this._to_uri = ua.configuration.uri; this._registrationTimer = null; // Ongoing Register request. this._registering = false; // Set status. this._registered = false; // Contact header. this._contact = this._ua.contact.toString(); // Sip.ice media feature tag (RFC 5768). this._contact += ';+sip.ice'; // Custom headers for REGISTER and un-REGISTER. this._extraHeaders = []; // Custom Contact header params for REGISTER and un-REGISTER. this._extraContactParams = ''; // Contents of the sip.instance Contact header parameter. this._sipInstance = `"<urn:uuid:${this._ua.configuration.instance_id}>"`; this._contact += `;reg-id=${this._reg_id}`; this._contact += `;+sip.instance=${this._sipInstance}`; } get registered() { return this._registered; } setExtraHeaders(extraHeaders) { if (!Array.isArray(extraHeaders)) { extraHeaders = []; } this._extraHeaders = extraHeaders.slice(); } setExtraContactParams(extraContactParams) { if (!(extraContactParams instanceof Object)) { extraContactParams = {}; } // Reset it. this._extraContactParams = ''; for (const param_key in extraContactParams) { if (Object.prototype.hasOwnProperty.call(extraContactParams, param_key)) { const param_value = extraContactParams[param_key]; this._extraContactParams += `;${param_key}`; if (param_value) { this._extraContactParams += `=${param_value}`; } } } } register() { if (this._registering) { logger.debug('Register request in progress...'); return; } const extraHeaders = Utils.cloneArray(this._extraHeaders); let contactValue; if (this._expires) { contactValue = `${this._contact};expires=${this._expires}${this._extraContactParams}`; extraHeaders.push(`Expires: ${this._expires}`); } else { contactValue = `${this._contact}${this._extraContactParams}`; } extraHeaders.push(`Contact: ${contactValue}`); let fromTag = Utils.newTag(); if (this._ua.configuration.register_from_tag_trail) { if (typeof this._ua.configuration.register_from_tag_trail === 'function') { fromTag += this._ua.configuration.register_from_tag_trail(); } else { fromTag += this._ua.configuration.register_from_tag_trail; } } const request = new SIPMessage.OutgoingRequest(JsSIP_C.REGISTER, this._registrar, this._ua, { to_uri: this._to_uri, call_id: this._call_id, cseq: (this._cseq += 1), from_tag: fromTag, }, extraHeaders); const request_sender = new RequestSender(this._ua, request, { onRequestTimeout: () => { this._registrationFailure(null, JsSIP_C.causes.REQUEST_TIMEOUT); }, onTransportError: () => { this._registrationFailure(null, JsSIP_C.causes.CONNECTION_ERROR); }, // Increase the CSeq on authentication. onAuthenticated: () => { this._cseq += 1; }, onReceiveResponse: response => { // Discard responses to older REGISTER/un-REGISTER requests. if (response.cseq !== this._cseq) { return; } // Clear registration timer. if (this._registrationTimer !== null) { clearTimeout(this._registrationTimer); this._registrationTimer = null; } 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._registering = false; if (!response.hasHeader('Contact')) { logger.debug('no Contact header in response to REGISTER, response ignored'); break; } const contacts = response.headers['Contact'].reduce((a, b) => a.concat(b.parsed), []); // Get the Contact pointing to us and update the expires value accordingly. // Try to find a matching Contact using sip.instance and reg-id. let contact = contacts.find(element => this._sipInstance === element.getParam('+sip.instance') && this._reg_id === parseInt(element.getParam('reg-id'))); // If no match was found using the sip.instance try comparing the URIs. if (!contact) { contact = contacts.find(element => element.uri.user === this._ua.contact.uri.user); } if (!contact) { logger.debug('no Contact header pointing to us, response ignored'); break; } let expires = contact.getParam('expires'); if (!expires && response.hasHeader('expires')) { expires = response.getHeader('expires'); } if (!expires) { expires = this._expires; } expires = Number(expires); if (expires < MIN_REGISTER_EXPIRES) { expires = MIN_REGISTER_EXPIRES; } const timeout = expires > 64 ? (expires * 1000) / 2 + Math.floor((expires / 2 - 32) * 1000 * Math.random()) : expires * 1000 - 5000; // Re-Register or emit an event before the expiration interval has elapsed. // For that, decrease the expires value. ie: 3 seconds. this._registrationTimer = setTimeout(() => { this._registrationTimer = null; // If there are no listeners for registrationExpiring, renew registration. // If there are listeners, let the function listening do the register call. if (this._ua.listeners('registrationExpiring').length === 0) { this.register(); } else { this._ua.emit('registrationExpiring'); } }, timeout); // Save gruu values. if (contact.hasParam('temp-gruu')) { this._ua.contact.temp_gruu = contact .getParam('temp-gruu') .replace(/"/g, ''); } if (contact.hasParam('pub-gruu')) { this._ua.contact.pub_gruu = contact .getParam('pub-gruu') .replace(/"/g, ''); } if (!this._registered) { this._registered = true; this._ua.registered({ response }); } 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 = Number(response.getHeader('min-expires')); if (this._expires < MIN_REGISTER_EXPIRES) { this._expires = MIN_REGISTER_EXPIRES; } // Assure register re-try with new expire. this._registering = false; // Attempt the registration again immediately. this.register(); } else { // This response MUST contain a Min-Expires header field. logger.debug('423 response received for REGISTER without Min-Expires'); this._registrationFailure(response, JsSIP_C.causes.SIP_FAILURE_CODE); } break; } default: { const cause = Utils.sipErrorCause(response.status_code); this._registrationFailure(response, cause); } } }, }); this._registering = true; request_sender.send(); } unregister(options = {}) { if (!this._registered) { logger.debug('already unregistered'); return; } this._registered = false; // Clear the registration timer. if (this._registrationTimer !== null) { clearTimeout(this._registrationTimer); this._registrationTimer = null; } const extraHeaders = Utils.cloneArray(this._extraHeaders); if (options.all) { extraHeaders.push(`Contact: *${this._extraContactParams}`); } else { extraHeaders.push(`Contact: ${this._contact};expires=0${this._extraContactParams}`); } extraHeaders.push('Expires: 0'); const request = new SIPMessage.OutgoingRequest(JsSIP_C.REGISTER, this._registrar, this._ua, { to_uri: this._to_uri, call_id: this._call_id, cseq: (this._cseq += 1), }, extraHeaders); const request_sender = new RequestSender(this._ua, request, { onRequestTimeout: () => { this._unregistered(null, JsSIP_C.causes.REQUEST_TIMEOUT); }, onTransportError: () => { this._unregistered(null, JsSIP_C.causes.CONNECTION_ERROR); }, // Increase the CSeq on authentication. onAuthenticated: () => { this._cseq += 1; }, onReceiveResponse: response => { 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._unregistered(response); break; } default: { const cause = Utils.sipErrorCause(response.status_code); this._unregistered(response, cause); } } }, }); request_sender.send(); } close() { if (this._registered) { this.unregister(); } } onTransportClosed() { this._registering = false; if (this._registrationTimer !== null) { clearTimeout(this._registrationTimer); this._registrationTimer = null; } if (this._registered) { this._registered = false; this._ua.unregistered({}); } } _registrationFailure(response, cause) { this._registering = false; this._ua.registrationFailed({ response: response || null, cause, }); if (this._registered) { this._registered = false; this._ua.unregistered({ response: response || null, cause, }); } } _unregistered(response, cause) { this._registering = false; this._registered = false; this._ua.unregistered({ response: response || null, cause: cause || null, }); } };