jssip
Version:
The Javascript SIP library
307 lines (306 loc) • 13.3 kB
JavaScript
;
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,
});
}
};