jssip
Version:
the Javascript SIP library
321 lines (272 loc) • 8.01 kB
JavaScript
const Logger = require('./Logger');
const SIPMessage = require('./SIPMessage');
const JsSIP_C = require('./Constants');
const Transactions = require('./Transactions');
const Dialog_RequestSender = require('./Dialog/RequestSender');
const Utils = require('./Utils');
const logger = new Logger('Dialog');
const C = {
// Dialog states.
STATUS_EARLY : 1,
STATUS_CONFIRMED : 2,
STATUS_TERMINATED : 3
};
// RFC 3261 12.1.
module.exports = class Dialog
{
// Expose C object.
static get C()
{
return C;
}
constructor(owner, message, type, state = C.STATUS_CONFIRMED)
{
this._owner = owner;
this._ua = owner._ua;
this._uac_pending_reply = false;
this._uas_pending_reply = false;
if (!message.hasHeader('contact'))
{
return {
error : 'unable to create a Dialog without Contact header field'
};
}
if (message instanceof SIPMessage.IncomingResponse)
{
state = (message.status_code < 200) ? C.STATUS_EARLY : C.STATUS_CONFIRMED;
}
const contact = message.parseHeader('contact');
// RFC 3261 12.1.1.
if (type === 'UAS')
{
this._id = {
call_id : message.call_id,
local_tag : message.to_tag,
remote_tag : message.from_tag,
toString()
{
return this.call_id + this.local_tag + this.remote_tag;
}
};
this._state = state;
this._remote_seqnum = message.cseq;
this._local_uri = message.parseHeader('to').uri;
this._remote_uri = message.parseHeader('from').uri;
this._remote_target = contact.uri;
this._route_set = message.getHeaders('record-route');
this._ack_seqnum = this._remote_seqnum;
}
// RFC 3261 12.1.2.
else if (type === 'UAC')
{
this._id = {
call_id : message.call_id,
local_tag : message.from_tag,
remote_tag : message.to_tag,
toString()
{
return this.call_id + this.local_tag + this.remote_tag;
}
};
this._state = state;
this._local_seqnum = message.cseq;
this._local_uri = message.parseHeader('from').uri;
this._remote_uri = message.parseHeader('to').uri;
this._remote_target = contact.uri;
this._route_set = message.getHeaders('record-route').reverse();
this._ack_seqnum = null;
}
this._ua.newDialog(this);
logger.debug(`new ${type} dialog created with status ${this._state === C.STATUS_EARLY ? 'EARLY': 'CONFIRMED'}`);
}
get id()
{
return this._id;
}
get local_seqnum()
{
return this._local_seqnum;
}
set local_seqnum(num)
{
this._local_seqnum = num;
}
get owner()
{
return this._owner;
}
get uac_pending_reply()
{
return this._uac_pending_reply;
}
set uac_pending_reply(pending)
{
this._uac_pending_reply = pending;
}
get uas_pending_reply()
{
return this._uas_pending_reply;
}
isTerminated()
{
return this._status === C.STATUS_TERMINATED;
}
update(message, type)
{
this._state = C.STATUS_CONFIRMED;
logger.debug(`dialog ${this._id.toString()} changed to CONFIRMED state`);
if (type === 'UAC')
{
// RFC 3261 13.2.2.4.
this._route_set = message.getHeaders('record-route').reverse();
}
}
terminate()
{
logger.debug(`dialog ${this._id.toString()} deleted`);
this._ua.destroyDialog(this);
this._state = C.STATUS_TERMINATED;
}
sendRequest(method, options = {})
{
const extraHeaders = Utils.cloneArray(options.extraHeaders);
const eventHandlers = Utils.cloneObject(options.eventHandlers);
const body = options.body || null;
const request = this._createRequest(method, extraHeaders, body);
// Increase the local CSeq on authentication.
eventHandlers.onAuthenticated = () =>
{
this._local_seqnum += 1;
};
const request_sender = new Dialog_RequestSender(this, request, eventHandlers);
request_sender.send();
// Return the instance of OutgoingRequest.
return request;
}
receiveRequest(request)
{
// Check in-dialog request.
if (!this._checkInDialogRequest(request))
{
return;
}
// ACK received. Cleanup this._ack_seqnum.
if (request.method === JsSIP_C.ACK && this._ack_seqnum !== null)
{
this._ack_seqnum = null;
}
// INVITE received. Set this._ack_seqnum.
else if (request.method === JsSIP_C.INVITE)
{
this._ack_seqnum = request.cseq;
}
this._owner.receiveRequest(request);
}
// RFC 3261 12.2.1.1.
_createRequest(method, extraHeaders, body)
{
extraHeaders = Utils.cloneArray(extraHeaders);
if (!this._local_seqnum) { this._local_seqnum = Math.floor(Math.random() * 10000); }
const cseq = (method === JsSIP_C.CANCEL || method === JsSIP_C.ACK) ?
this._local_seqnum :
this._local_seqnum += 1;
const request = new SIPMessage.OutgoingRequest(
method,
this._remote_target,
this._ua, {
'cseq' : cseq,
'call_id' : this._id.call_id,
'from_uri' : this._local_uri,
'from_tag' : this._id.local_tag,
'to_uri' : this._remote_uri,
'to_tag' : this._id.remote_tag,
'route_set' : this._route_set
}, extraHeaders, body);
return request;
}
// RFC 3261 12.2.2.
_checkInDialogRequest(request)
{
if (!this._remote_seqnum)
{
this._remote_seqnum = request.cseq;
}
else if (request.cseq < this._remote_seqnum)
{
if (request.method === JsSIP_C.ACK)
{
// We are not expecting any ACK with lower seqnum than the current one.
// Or this is not the ACK we are waiting for.
if (this._ack_seqnum === null || request.cseq !== this._ack_seqnum)
{
return false;
}
}
else
{
request.reply(500);
return false;
}
}
else if (request.cseq > this._remote_seqnum)
{
this._remote_seqnum = request.cseq;
}
// RFC3261 14.2 Modifying an Existing Session -UAS BEHAVIOR-.
if (request.method === JsSIP_C.INVITE ||
(request.method === JsSIP_C.UPDATE && request.body))
{
if (this._uac_pending_reply === true)
{
request.reply(491);
}
else if (this._uas_pending_reply === true)
{
const retryAfter = (Math.random() * 10 | 0) + 1;
request.reply(500, null, [ `Retry-After:${retryAfter}` ]);
return false;
}
else
{
this._uas_pending_reply = true;
const stateChanged = () =>
{
if (request.server_transaction.state === Transactions.C.STATUS_ACCEPTED ||
request.server_transaction.state === Transactions.C.STATUS_COMPLETED ||
request.server_transaction.state === Transactions.C.STATUS_TERMINATED)
{
request.server_transaction.removeListener('stateChanged', stateChanged);
this._uas_pending_reply = false;
}
};
request.server_transaction.on('stateChanged', stateChanged);
}
// RFC3261 12.2.2 Replace the dialog`s remote target URI if the request is accepted.
if (request.hasHeader('contact'))
{
request.server_transaction.on('stateChanged', () =>
{
if (request.server_transaction.state === Transactions.C.STATUS_ACCEPTED)
{
this._remote_target = request.parseHeader('contact').uri;
}
});
}
}
else if (request.method === JsSIP_C.NOTIFY)
{
// RFC6665 3.2 Replace the dialog`s remote target URI if the request is accepted.
if (request.hasHeader('contact'))
{
request.server_transaction.on('stateChanged', () =>
{
if (request.server_transaction.state === Transactions.C.STATUS_COMPLETED)
{
this._remote_target = request.parseHeader('contact').uri;
}
});
}
}
return true;
}
};