jssip
Version:
The Javascript SIP library
620 lines (619 loc) • 21.3 kB
JavaScript
"use strict";
const EventEmitter = require('events').EventEmitter;
const Logger = require('./Logger');
const JsSIP_C = require('./Constants');
const SIPMessage = require('./SIPMessage');
const Timers = require('./Timers');
const loggernict = new Logger('NonInviteClientTransaction');
const loggerict = new Logger('InviteClientTransaction');
const loggeract = new Logger('AckClientTransaction');
const loggernist = new Logger('NonInviteServerTransaction');
const loggerist = new Logger('InviteServerTransaction');
const C = {
// Transaction states.
STATUS_TRYING: 1,
STATUS_PROCEEDING: 2,
STATUS_CALLING: 3,
STATUS_ACCEPTED: 4,
STATUS_COMPLETED: 5,
STATUS_TERMINATED: 6,
STATUS_CONFIRMED: 7,
// Transaction types.
NON_INVITE_CLIENT: 'nict',
NON_INVITE_SERVER: 'nist',
INVITE_CLIENT: 'ict',
INVITE_SERVER: 'ist',
};
class NonInviteClientTransaction extends EventEmitter {
constructor(ua, transport, request, eventHandlers) {
super();
this.type = C.NON_INVITE_CLIENT;
this.id = `z9hG4bK${Math.floor(Math.random() * 10000000)}`;
this.ua = ua;
this.transport = transport;
this.request = request;
this.eventHandlers = eventHandlers;
let via = `SIP/2.0/${transport.via_transport}`;
via += ` ${ua.configuration.via_host};branch=${this.id}`;
this.request.setHeader('via', via);
this.ua.newTransaction(this);
}
get C() {
return C;
}
stateChanged(state) {
this.state = state;
this.emit('stateChanged');
}
send() {
this.stateChanged(C.STATUS_TRYING);
this.F = setTimeout(() => {
this.timer_F();
}, Timers.TIMER_F);
if (!this.transport.send(this.request)) {
this.onTransportError();
}
}
onTransportError() {
loggernict.debug(`transport error occurred, deleting transaction ${this.id}`);
clearTimeout(this.F);
clearTimeout(this.K);
this.stateChanged(C.STATUS_TERMINATED);
this.ua.destroyTransaction(this);
this.eventHandlers.onTransportError();
}
timer_F() {
loggernict.debug(`Timer F expired for transaction ${this.id}`);
this.stateChanged(C.STATUS_TERMINATED);
this.ua.destroyTransaction(this);
this.eventHandlers.onRequestTimeout();
}
timer_K() {
this.stateChanged(C.STATUS_TERMINATED);
this.ua.destroyTransaction(this);
}
receiveResponse(response) {
const status_code = response.status_code;
if (status_code < 200) {
switch (this.state) {
case C.STATUS_TRYING:
case C.STATUS_PROCEEDING: {
this.stateChanged(C.STATUS_PROCEEDING);
this.eventHandlers.onReceiveResponse(response);
break;
}
}
}
else {
switch (this.state) {
case C.STATUS_TRYING:
case C.STATUS_PROCEEDING: {
this.stateChanged(C.STATUS_COMPLETED);
clearTimeout(this.F);
if (status_code === 408) {
this.eventHandlers.onRequestTimeout();
}
else {
this.eventHandlers.onReceiveResponse(response);
}
this.K = setTimeout(() => {
this.timer_K();
}, Timers.TIMER_K);
break;
}
case C.STATUS_COMPLETED: {
break;
}
}
}
}
}
class InviteClientTransaction extends EventEmitter {
constructor(ua, transport, request, eventHandlers) {
super();
this.type = C.INVITE_CLIENT;
this.id = `z9hG4bK${Math.floor(Math.random() * 10000000)}`;
this.ua = ua;
this.transport = transport;
this.request = request;
this.eventHandlers = eventHandlers;
request.transaction = this;
let via = `SIP/2.0/${transport.via_transport}`;
via += ` ${ua.configuration.via_host};branch=${this.id}`;
this.request.setHeader('via', via);
this.ua.newTransaction(this);
}
get C() {
return C;
}
stateChanged(state) {
this.state = state;
this.emit('stateChanged');
}
send() {
this.stateChanged(C.STATUS_CALLING);
this.B = setTimeout(() => {
this.timer_B();
}, Timers.TIMER_B);
if (!this.transport.send(this.request)) {
this.onTransportError();
}
}
onTransportError() {
clearTimeout(this.B);
clearTimeout(this.D);
clearTimeout(this.M);
if (this.state !== C.STATUS_ACCEPTED) {
loggerict.debug(`transport error occurred, deleting transaction ${this.id}`);
this.eventHandlers.onTransportError();
}
this.stateChanged(C.STATUS_TERMINATED);
this.ua.destroyTransaction(this);
}
// RFC 6026 7.2.
timer_M() {
loggerict.debug(`Timer M expired for transaction ${this.id}`);
if (this.state === C.STATUS_ACCEPTED) {
clearTimeout(this.B);
this.stateChanged(C.STATUS_TERMINATED);
this.ua.destroyTransaction(this);
}
}
// RFC 3261 17.1.1.
timer_B() {
loggerict.debug(`Timer B expired for transaction ${this.id}`);
if (this.state === C.STATUS_CALLING) {
this.stateChanged(C.STATUS_TERMINATED);
this.ua.destroyTransaction(this);
this.eventHandlers.onRequestTimeout();
}
}
timer_D() {
loggerict.debug(`Timer D expired for transaction ${this.id}`);
clearTimeout(this.B);
this.stateChanged(C.STATUS_TERMINATED);
this.ua.destroyTransaction(this);
}
sendACK(response) {
const ack = new SIPMessage.OutgoingRequest(JsSIP_C.ACK, this.request.ruri, this.ua, {
route_set: this.request.getHeaders('route'),
call_id: this.request.getHeader('call-id'),
cseq: this.request.cseq,
});
ack.setHeader('from', this.request.getHeader('from'));
ack.setHeader('via', this.request.getHeader('via'));
ack.setHeader('to', response.getHeader('to'));
this.D = setTimeout(() => {
this.timer_D();
}, Timers.TIMER_D);
this.transport.send(ack);
}
cancel(reason) {
// Send only if a provisional response (>100) has been received.
if (this.state !== C.STATUS_PROCEEDING) {
return;
}
const cancel = new SIPMessage.OutgoingRequest(JsSIP_C.CANCEL, this.request.ruri, this.ua, {
route_set: this.request.getHeaders('route'),
call_id: this.request.getHeader('call-id'),
cseq: this.request.cseq,
});
cancel.setHeader('from', this.request.getHeader('from'));
cancel.setHeader('via', this.request.getHeader('via'));
cancel.setHeader('to', this.request.getHeader('to'));
if (reason) {
cancel.setHeader('reason', reason);
}
this.transport.send(cancel);
}
receiveResponse(response) {
const status_code = response.status_code;
if (status_code >= 100 && status_code <= 199) {
switch (this.state) {
case C.STATUS_CALLING: {
this.stateChanged(C.STATUS_PROCEEDING);
this.eventHandlers.onReceiveResponse(response);
break;
}
case C.STATUS_PROCEEDING: {
this.eventHandlers.onReceiveResponse(response);
break;
}
}
}
else if (status_code >= 200 && status_code <= 299) {
switch (this.state) {
case C.STATUS_CALLING:
case C.STATUS_PROCEEDING: {
this.stateChanged(C.STATUS_ACCEPTED);
this.M = setTimeout(() => {
this.timer_M();
}, Timers.TIMER_M);
this.eventHandlers.onReceiveResponse(response);
break;
}
case C.STATUS_ACCEPTED: {
this.eventHandlers.onReceiveResponse(response);
break;
}
}
}
else if (status_code >= 300 && status_code <= 699) {
switch (this.state) {
case C.STATUS_CALLING:
case C.STATUS_PROCEEDING: {
this.stateChanged(C.STATUS_COMPLETED);
this.sendACK(response);
this.eventHandlers.onReceiveResponse(response);
break;
}
case C.STATUS_COMPLETED: {
this.sendACK(response);
break;
}
}
}
}
}
class AckClientTransaction extends EventEmitter {
constructor(ua, transport, request, eventHandlers) {
super();
this.id = `z9hG4bK${Math.floor(Math.random() * 10000000)}`;
this.transport = transport;
this.request = request;
this.eventHandlers = eventHandlers;
let via = `SIP/2.0/${transport.via_transport}`;
via += ` ${ua.configuration.via_host};branch=${this.id}`;
this.request.setHeader('via', via);
}
get C() {
return C;
}
send() {
if (!this.transport.send(this.request)) {
this.onTransportError();
}
}
onTransportError() {
loggeract.debug(`transport error occurred for transaction ${this.id}`);
this.eventHandlers.onTransportError();
}
}
class NonInviteServerTransaction extends EventEmitter {
constructor(ua, transport, request) {
super();
this.type = C.NON_INVITE_SERVER;
this.id = request.via_branch;
this.ua = ua;
this.transport = transport;
this.request = request;
this.last_response = '';
request.server_transaction = this;
this.state = C.STATUS_TRYING;
ua.newTransaction(this);
}
get C() {
return C;
}
stateChanged(state) {
this.state = state;
this.emit('stateChanged');
}
timer_J() {
loggernist.debug(`Timer J expired for transaction ${this.id}`);
this.stateChanged(C.STATUS_TERMINATED);
this.ua.destroyTransaction(this);
}
onTransportError() {
if (!this.transportError) {
this.transportError = true;
loggernist.debug(`transport error occurred, deleting transaction ${this.id}`);
clearTimeout(this.J);
this.stateChanged(C.STATUS_TERMINATED);
this.ua.destroyTransaction(this);
}
}
receiveResponse(status_code, response, onSuccess, onFailure) {
if (status_code === 100) {
/* RFC 4320 4.1
* 'A SIP element MUST NOT
* send any provisional response with a
* Status-Code other than 100 to a non-INVITE request.'
*/
switch (this.state) {
case C.STATUS_TRYING: {
this.stateChanged(C.STATUS_PROCEEDING);
if (!this.transport.send(response)) {
this.onTransportError();
}
break;
}
case C.STATUS_PROCEEDING: {
this.last_response = response;
if (!this.transport.send(response)) {
this.onTransportError();
if (onFailure) {
onFailure();
}
}
else if (onSuccess) {
onSuccess();
}
break;
}
}
}
else if (status_code >= 200 && status_code <= 699) {
switch (this.state) {
case C.STATUS_TRYING:
case C.STATUS_PROCEEDING: {
this.stateChanged(C.STATUS_COMPLETED);
this.last_response = response;
this.J = setTimeout(() => {
this.timer_J();
}, Timers.TIMER_J);
if (!this.transport.send(response)) {
this.onTransportError();
if (onFailure) {
onFailure();
}
}
else if (onSuccess) {
onSuccess();
}
break;
}
case C.STATUS_COMPLETED: {
break;
}
}
}
}
}
class InviteServerTransaction extends EventEmitter {
constructor(ua, transport, request) {
super();
this.type = C.INVITE_SERVER;
this.id = request.via_branch;
this.ua = ua;
this.transport = transport;
this.request = request;
this.last_response = '';
request.server_transaction = this;
this.state = C.STATUS_PROCEEDING;
ua.newTransaction(this);
this.resendProvisionalTimer = null;
request.reply(100);
}
get C() {
return C;
}
stateChanged(state) {
this.state = state;
this.emit('stateChanged');
}
timer_H() {
loggerist.debug(`Timer H expired for transaction ${this.id}`);
if (this.state === C.STATUS_COMPLETED) {
loggerist.debug('ACK not received, dialog will be terminated');
}
this.stateChanged(C.STATUS_TERMINATED);
this.ua.destroyTransaction(this);
}
timer_I() {
this.stateChanged(C.STATUS_TERMINATED);
this.ua.destroyTransaction(this);
}
// RFC 6026 7.1.
timer_L() {
loggerist.debug(`Timer L expired for transaction ${this.id}`);
if (this.state === C.STATUS_ACCEPTED) {
this.stateChanged(C.STATUS_TERMINATED);
this.ua.destroyTransaction(this);
}
}
onTransportError() {
if (!this.transportError) {
this.transportError = true;
loggerist.debug(`transport error occurred, deleting transaction ${this.id}`);
if (this.resendProvisionalTimer !== null) {
clearInterval(this.resendProvisionalTimer);
this.resendProvisionalTimer = null;
}
clearTimeout(this.L);
clearTimeout(this.H);
clearTimeout(this.I);
this.stateChanged(C.STATUS_TERMINATED);
this.ua.destroyTransaction(this);
}
}
resend_provisional() {
if (!this.transport.send(this.last_response)) {
this.onTransportError();
}
}
// INVITE Server Transaction RFC 3261 17.2.1.
receiveResponse(status_code, response, onSuccess, onFailure) {
if (status_code >= 100 && status_code <= 199) {
switch (this.state) {
case C.STATUS_PROCEEDING: {
if (!this.transport.send(response)) {
this.onTransportError();
}
this.last_response = response;
break;
}
}
}
if (status_code > 100 &&
status_code <= 199 &&
this.state === C.STATUS_PROCEEDING) {
// Trigger the resendProvisionalTimer only for the first non 100 provisional response.
if (this.resendProvisionalTimer === null) {
this.resendProvisionalTimer = setInterval(() => {
this.resend_provisional();
}, Timers.PROVISIONAL_RESPONSE_INTERVAL);
}
}
else if (status_code >= 200 && status_code <= 299) {
switch (this.state) {
case C.STATUS_PROCEEDING: {
this.stateChanged(C.STATUS_ACCEPTED);
this.last_response = response;
this.L = setTimeout(() => {
this.timer_L();
}, Timers.TIMER_L);
if (this.resendProvisionalTimer !== null) {
clearInterval(this.resendProvisionalTimer);
this.resendProvisionalTimer = null;
}
}
// falls through
case C.STATUS_ACCEPTED: {
// Note that this point will be reached for proceeding this.state also.
if (!this.transport.send(response)) {
this.onTransportError();
if (onFailure) {
onFailure();
}
}
else if (onSuccess) {
onSuccess();
}
break;
}
}
}
else if (status_code >= 300 && status_code <= 699) {
switch (this.state) {
case C.STATUS_PROCEEDING: {
if (this.resendProvisionalTimer !== null) {
clearInterval(this.resendProvisionalTimer);
this.resendProvisionalTimer = null;
}
if (!this.transport.send(response)) {
this.onTransportError();
if (onFailure) {
onFailure();
}
}
else {
this.stateChanged(C.STATUS_COMPLETED);
this.H = setTimeout(() => {
this.timer_H();
}, Timers.TIMER_H);
if (onSuccess) {
onSuccess();
}
}
break;
}
}
}
}
}
/**
* INVITE:
* _true_ if retransmission
* _false_ new request
*
* ACK:
* _true_ ACK to non2xx response
* _false_ ACK must be passed to TU (accepted state)
* ACK to 2xx response
*
* CANCEL:
* _true_ no matching invite transaction
* _false_ matching invite transaction and no final response sent
*
* OTHER:
* _true_ retransmission
* _false_ new request
*/
function checkTransaction({ _transactions }, request) {
let tr;
switch (request.method) {
case JsSIP_C.INVITE: {
tr = _transactions.ist[request.via_branch];
if (tr) {
switch (tr.state) {
case C.STATUS_PROCEEDING: {
tr.transport.send(tr.last_response);
break;
}
// RFC 6026 7.1 Invite retransmission.
// Received while in C.STATUS_ACCEPTED state. Absorb it.
case C.STATUS_ACCEPTED: {
break;
}
}
return true;
}
break;
}
case JsSIP_C.ACK: {
tr = _transactions.ist[request.via_branch];
// RFC 6026 7.1.
if (tr) {
if (tr.state === C.STATUS_ACCEPTED) {
return false;
}
else if (tr.state === C.STATUS_COMPLETED) {
tr.state = C.STATUS_CONFIRMED;
tr.I = setTimeout(() => {
tr.timer_I();
}, Timers.TIMER_I);
return true;
}
}
// ACK to 2XX Response.
else {
return false;
}
break;
}
case JsSIP_C.CANCEL: {
tr = _transactions.ist[request.via_branch];
if (tr) {
request.reply_sl(200);
if (tr.state === C.STATUS_PROCEEDING) {
return false;
}
else {
return true;
}
}
else {
request.reply_sl(481);
return true;
}
}
default: {
// Non-INVITE Server Transaction RFC 3261 17.2.2.
tr = _transactions.nist[request.via_branch];
if (tr) {
switch (tr.state) {
case C.STATUS_TRYING: {
break;
}
case C.STATUS_PROCEEDING:
case C.STATUS_COMPLETED: {
tr.transport.send(tr.last_response);
break;
}
}
return true;
}
break;
}
}
}
module.exports = {
C,
NonInviteClientTransaction,
InviteClientTransaction,
AckClientTransaction,
NonInviteServerTransaction,
InviteServerTransaction,
checkTransaction,
};