jssip
Version:
the Javascript SIP library
416 lines (340 loc) • 10.8 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 = this._extraHeaders.slice();
extraHeaders.push(`Contact: \
${this._contact};expires=${this._expires}${this._extraContactParams}`);
extraHeaders.push(`Expires: ${this._expires}`);
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._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;
// 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 = this._extraHeaders.slice();
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
});
}
};