sip.js
Version:
A SIP library for JavaScript
904 lines (903 loc) • 48 kB
JavaScript
import { NameAddrHeader } from "../../grammar/name-addr-header.js";
import { getBody, isBody } from "../messages/body.js";
import { C } from "../messages/methods/constants.js";
import { IncomingRequestMessage } from "../messages/incoming-request-message.js";
import { IncomingResponseMessage } from "../messages/incoming-response-message.js";
import { OutgoingRequestMessage } from "../messages/outgoing-request-message.js";
import { SessionState } from "../session/session.js";
import { SignalingState } from "../session/session.js";
import { Timers } from "../timers.js";
import { InviteClientTransaction } from "../transactions/invite-client-transaction.js";
import { InviteServerTransaction } from "../transactions/invite-server-transaction.js";
import { TransactionState } from "../transactions/transaction-state.js";
import { ByeUserAgentClient } from "../user-agents/bye-user-agent-client.js";
import { ByeUserAgentServer } from "../user-agents/bye-user-agent-server.js";
import { InfoUserAgentClient } from "../user-agents/info-user-agent-client.js";
import { InfoUserAgentServer } from "../user-agents/info-user-agent-server.js";
import { MessageUserAgentClient } from "../user-agents/message-user-agent-client.js";
import { MessageUserAgentServer } from "../user-agents/message-user-agent-server.js";
import { NotifyUserAgentClient } from "../user-agents/notify-user-agent-client.js";
import { NotifyUserAgentServer } from "../user-agents/notify-user-agent-server.js";
import { PrackUserAgentClient } from "../user-agents/prack-user-agent-client.js";
import { PrackUserAgentServer } from "../user-agents/prack-user-agent-server.js";
import { ReInviteUserAgentClient } from "../user-agents/re-invite-user-agent-client.js";
import { ReInviteUserAgentServer } from "../user-agents/re-invite-user-agent-server.js";
import { ReferUserAgentClient } from "../user-agents/refer-user-agent-client.js";
import { ReferUserAgentServer } from "../user-agents/refer-user-agent-server.js";
import { Dialog } from "./dialog.js";
/**
* Session Dialog.
* @public
*/
export class SessionDialog extends Dialog {
constructor(initialTransaction, core, state, delegate) {
super(core, state);
this.initialTransaction = initialTransaction;
/** The state of the offer/answer exchange. */
this._signalingState = SignalingState.Initial;
/** True if waiting for an ACK to the initial transaction 2xx (UAS only). */
this.ackWait = false;
/** True if processing an ACK to the initial transaction 2xx (UAS only). */
this.ackProcessing = false;
this.delegate = delegate;
if (initialTransaction instanceof InviteServerTransaction) {
// If we're created by an invite server transaction, we're
// going to be waiting for an ACK if are to be confirmed.
this.ackWait = true;
}
// If we're confirmed upon creation start the retransmitting whatever
// the 2xx final response was that confirmed us into existence.
if (!this.early) {
this.start2xxRetransmissionTimer();
}
this.signalingStateTransition(initialTransaction.request);
this.logger = core.loggerFactory.getLogger("sip.invite-dialog");
this.logger.log(`INVITE dialog ${this.id} constructed`);
}
dispose() {
super.dispose();
this._signalingState = SignalingState.Closed;
this._offer = undefined;
this._answer = undefined;
if (this.invite2xxTimer) {
clearTimeout(this.invite2xxTimer);
this.invite2xxTimer = undefined;
}
// The UAS MUST still respond to any pending requests received for that
// dialog. It is RECOMMENDED that a 487 (Request Terminated) response
// be generated to those pending requests.
// https://tools.ietf.org/html/rfc3261#section-15.1.2
// TODO:
// this.userAgentServers.forEach((uas) => uas.reply(487));
this.logger.log(`INVITE dialog ${this.id} destroyed`);
}
// FIXME: Need real state machine
get sessionState() {
if (this.early) {
return SessionState.Early;
}
else if (this.ackWait) {
return SessionState.AckWait;
}
else if (this._signalingState === SignalingState.Closed) {
return SessionState.Terminated;
}
else {
return SessionState.Confirmed;
}
}
/** The state of the offer/answer exchange. */
get signalingState() {
return this._signalingState;
}
/** The current offer. Undefined unless signaling state HaveLocalOffer, HaveRemoteOffer, of Stable. */
get offer() {
return this._offer;
}
/** The current answer. Undefined unless signaling state Stable. */
get answer() {
return this._answer;
}
/** Confirm the dialog. Only matters if dialog is currently early. */
confirm() {
// When we're confirmed start the retransmitting whatever
// the 2xx final response that may have confirmed us.
if (this.early) {
this.start2xxRetransmissionTimer();
}
super.confirm();
}
/** Re-confirm the dialog. Only matters if handling re-INVITE request. */
reConfirm() {
// When we're confirmed start the retransmitting whatever
// the 2xx final response that may have confirmed us.
if (this.reinviteUserAgentServer) {
this.startReInvite2xxRetransmissionTimer();
}
}
/**
* The UAC core MUST generate an ACK request for each 2xx received from
* the transaction layer. The header fields of the ACK are constructed
* in the same way as for any request sent within a dialog (see Section
* 12) with the exception of the CSeq and the header fields related to
* authentication. The sequence number of the CSeq header field MUST be
* the same as the INVITE being acknowledged, but the CSeq method MUST
* be ACK. The ACK MUST contain the same credentials as the INVITE. If
* the 2xx contains an offer (based on the rules above), the ACK MUST
* carry an answer in its body. If the offer in the 2xx response is not
* acceptable, the UAC core MUST generate a valid answer in the ACK and
* then send a BYE immediately.
* https://tools.ietf.org/html/rfc3261#section-13.2.2.4
* @param options - ACK options bucket.
*/
ack(options = {}) {
this.logger.log(`INVITE dialog ${this.id} sending ACK request`);
let transaction;
if (this.reinviteUserAgentClient) {
// We're sending ACK for a re-INVITE
if (!(this.reinviteUserAgentClient.transaction instanceof InviteClientTransaction)) {
throw new Error("Transaction not instance of InviteClientTransaction.");
}
transaction = this.reinviteUserAgentClient.transaction;
this.reinviteUserAgentClient = undefined;
}
else {
// We're sending ACK for the initial INVITE
if (!(this.initialTransaction instanceof InviteClientTransaction)) {
throw new Error("Initial transaction not instance of InviteClientTransaction.");
}
transaction = this.initialTransaction;
}
const message = this.createOutgoingRequestMessage(C.ACK, {
cseq: transaction.request.cseq,
extraHeaders: options.extraHeaders,
body: options.body
});
transaction.ackResponse(message); // See InviteClientTransaction for details.
this.signalingStateTransition(message);
return { message };
}
/**
* Terminating a Session
*
* This section describes the procedures for terminating a session
* established by SIP. The state of the session and the state of the
* dialog are very closely related. When a session is initiated with an
* INVITE, each 1xx or 2xx response from a distinct UAS creates a
* dialog, and if that response completes the offer/answer exchange, it
* also creates a session. As a result, each session is "associated"
* with a single dialog - the one which resulted in its creation. If an
* initial INVITE generates a non-2xx final response, that terminates
* all sessions (if any) and all dialogs (if any) that were created
* through responses to the request. By virtue of completing the
* transaction, a non-2xx final response also prevents further sessions
* from being created as a result of the INVITE. The BYE request is
* used to terminate a specific session or attempted session. In this
* case, the specific session is the one with the peer UA on the other
* side of the dialog. When a BYE is received on a dialog, any session
* associated with that dialog SHOULD terminate. A UA MUST NOT send a
* BYE outside of a dialog. The caller's UA MAY send a BYE for either
* confirmed or early dialogs, and the callee's UA MAY send a BYE on
* confirmed dialogs, but MUST NOT send a BYE on early dialogs.
*
* However, the callee's UA MUST NOT send a BYE on a confirmed dialog
* until it has received an ACK for its 2xx response or until the server
* transaction times out. If no SIP extensions have defined other
* application layer states associated with the dialog, the BYE also
* terminates the dialog.
*
* https://tools.ietf.org/html/rfc3261#section-15
* FIXME: Make these proper Exceptions...
* @param options - BYE options bucket.
* @returns
* Throws `Error` if callee's UA attempts a BYE on an early dialog.
* Throws `Error` if callee's UA attempts a BYE on a confirmed dialog
* while it's waiting on the ACK for its 2xx response.
*/
bye(delegate, options) {
this.logger.log(`INVITE dialog ${this.id} sending BYE request`);
// The caller's UA MAY send a BYE for either
// confirmed or early dialogs, and the callee's UA MAY send a BYE on
// confirmed dialogs, but MUST NOT send a BYE on early dialogs.
//
// However, the callee's UA MUST NOT send a BYE on a confirmed dialog
// until it has received an ACK for its 2xx response or until the server
// transaction times out.
// https://tools.ietf.org/html/rfc3261#section-15
if (this.initialTransaction instanceof InviteServerTransaction) {
if (this.early) {
// FIXME: TODO: This should throw a proper exception.
throw new Error("UAS MUST NOT send a BYE on early dialogs.");
}
if (this.ackWait && this.initialTransaction.state !== TransactionState.Terminated) {
// FIXME: TODO: This should throw a proper exception.
throw new Error("UAS MUST NOT send a BYE on a confirmed dialog " +
"until it has received an ACK for its 2xx response " +
"or until the server transaction times out.");
}
}
// A BYE request is constructed as would any other request within a
// dialog, as described in Section 12.
//
// Once the BYE is constructed, the UAC core creates a new non-INVITE
// client transaction, and passes it the BYE request. The UAC MUST
// consider the session terminated (and therefore stop sending or
// listening for media) as soon as the BYE request is passed to the
// client transaction. If the response for the BYE is a 481
// (Call/Transaction Does Not Exist) or a 408 (Request Timeout) or no
// response at all is received for the BYE (that is, a timeout is
// returned by the client transaction), the UAC MUST consider the
// session and the dialog terminated.
// https://tools.ietf.org/html/rfc3261#section-15.1.1
return new ByeUserAgentClient(this, delegate, options);
}
/**
* An INFO request can be associated with an Info Package (see
* Section 5), or associated with a legacy INFO usage (see Section 2).
*
* The construction of the INFO request is the same as any other
* non-target refresh request within an existing invite dialog usage as
* described in Section 12.2 of RFC 3261.
* https://tools.ietf.org/html/rfc6086#section-4.2.1
* @param options - Options bucket.
*/
info(delegate, options) {
this.logger.log(`INVITE dialog ${this.id} sending INFO request`);
if (this.early) {
// FIXME: TODO: This should throw a proper exception.
throw new Error("Dialog not confirmed.");
}
return new InfoUserAgentClient(this, delegate, options);
}
/**
* Modifying an Existing Session
*
* A successful INVITE request (see Section 13) establishes both a
* dialog between two user agents and a session using the offer-answer
* model. Section 12 explains how to modify an existing dialog using a
* target refresh request (for example, changing the remote target URI
* of the dialog). This section describes how to modify the actual
* session. This modification can involve changing addresses or ports,
* adding a media stream, deleting a media stream, and so on. This is
* accomplished by sending a new INVITE request within the same dialog
* that established the session. An INVITE request sent within an
* existing dialog is known as a re-INVITE.
*
* Note that a single re-INVITE can modify the dialog and the
* parameters of the session at the same time.
*
* Either the caller or callee can modify an existing session.
* https://tools.ietf.org/html/rfc3261#section-14
* @param options - Options bucket
*/
invite(delegate, options) {
this.logger.log(`INVITE dialog ${this.id} sending INVITE request`);
if (this.early) {
// FIXME: TODO: This should throw a proper exception.
throw new Error("Dialog not confirmed.");
}
// Note that a UAC MUST NOT initiate a new INVITE transaction within a
// dialog while another INVITE transaction is in progress in either
// direction.
//
// 1. If there is an ongoing INVITE client transaction, the TU MUST
// wait until the transaction reaches the completed or terminated
// state before initiating the new INVITE.
//
// 2. If there is an ongoing INVITE server transaction, the TU MUST
// wait until the transaction reaches the confirmed or terminated
// state before initiating the new INVITE.
//
// However, a UA MAY initiate a regular transaction while an INVITE
// transaction is in progress. A UA MAY also initiate an INVITE
// transaction while a regular transaction is in progress.
// https://tools.ietf.org/html/rfc3261#section-14.1
if (this.reinviteUserAgentClient) {
// FIXME: TODO: This should throw a proper exception.
throw new Error("There is an ongoing re-INVITE client transaction.");
}
if (this.reinviteUserAgentServer) {
// FIXME: TODO: This should throw a proper exception.
throw new Error("There is an ongoing re-INVITE server transaction.");
}
return new ReInviteUserAgentClient(this, delegate, options);
}
/**
* A UAC MAY associate a MESSAGE request with an existing dialog. If a
* MESSAGE request is sent within a dialog, it is "associated" with any
* media session or sessions associated with that dialog.
* https://tools.ietf.org/html/rfc3428#section-4
* @param options - Options bucket.
*/
message(delegate, options) {
this.logger.log(`INVITE dialog ${this.id} sending MESSAGE request`);
if (this.early) {
// FIXME: TODO: This should throw a proper exception.
throw new Error("Dialog not confirmed.");
}
const message = this.createOutgoingRequestMessage(C.MESSAGE, options);
return new MessageUserAgentClient(this.core, message, delegate);
}
/**
* The NOTIFY mechanism defined in [2] MUST be used to inform the agent
* sending the REFER of the status of the reference.
* https://tools.ietf.org/html/rfc3515#section-2.4.4
* @param options - Options bucket.
*/
notify(delegate, options) {
this.logger.log(`INVITE dialog ${this.id} sending NOTIFY request`);
if (this.early) {
// FIXME: TODO: This should throw a proper exception.
throw new Error("Dialog not confirmed.");
}
return new NotifyUserAgentClient(this, delegate, options);
}
/**
* Assuming the response is to be transmitted reliably, the UAC MUST
* create a new request with method PRACK. This request is sent within
* the dialog associated with the provisional response (indeed, the
* provisional response may have created the dialog). PRACK requests
* MAY contain bodies, which are interpreted according to their type and
* disposition.
* https://tools.ietf.org/html/rfc3262#section-4
* @param options - Options bucket.
*/
prack(delegate, options) {
this.logger.log(`INVITE dialog ${this.id} sending PRACK request`);
return new PrackUserAgentClient(this, delegate, options);
}
/**
* REFER is a SIP request and is constructed as defined in [1]. A REFER
* request MUST contain exactly one Refer-To header field value.
* https://tools.ietf.org/html/rfc3515#section-2.4.1
* @param options - Options bucket.
*/
refer(delegate, options) {
this.logger.log(`INVITE dialog ${this.id} sending REFER request`);
if (this.early) {
// FIXME: TODO: This should throw a proper exception.
throw new Error("Dialog not confirmed.");
}
// FIXME: TODO: Validate Refer-To header field value.
return new ReferUserAgentClient(this, delegate, options);
}
/**
* Requests sent within a dialog, as any other requests, are atomic. If
* a particular request is accepted by the UAS, all the state changes
* associated with it are performed. If the request is rejected, none
* of the state changes are performed.
* https://tools.ietf.org/html/rfc3261#section-12.2.2
* @param message - Incoming request message within this dialog.
*/
receiveRequest(message) {
this.logger.log(`INVITE dialog ${this.id} received ${message.method} request`);
// Response retransmissions cease when an ACK request for the
// response is received. This is independent of whatever transport
// protocols are used to send the response.
// https://tools.ietf.org/html/rfc6026#section-8.1
if (message.method === C.ACK) {
// If ackWait is true, then this is the ACK to the initial INVITE,
// otherwise this is an ACK to an in dialog INVITE. In either case,
// guard to make sure the sequence number of the ACK matches the INVITE.
if (this.ackWait) {
if (this.initialTransaction instanceof InviteClientTransaction) {
this.logger.warn(`INVITE dialog ${this.id} received unexpected ${message.method} request, dropping.`);
return;
}
if (this.initialTransaction.request.cseq !== message.cseq) {
this.logger.warn(`INVITE dialog ${this.id} received unexpected ${message.method} request, dropping.`);
return;
}
// Update before the delegate has a chance to handle the
// message as delegate may callback into this dialog.
this.ackWait = false;
}
else {
if (!this.reinviteUserAgentServer) {
this.logger.warn(`INVITE dialog ${this.id} received unexpected ${message.method} request, dropping.`);
return;
}
if (this.reinviteUserAgentServer.transaction.request.cseq !== message.cseq) {
this.logger.warn(`INVITE dialog ${this.id} received unexpected ${message.method} request, dropping.`);
return;
}
this.reinviteUserAgentServer = undefined;
}
this.signalingStateTransition(message);
if (this.delegate && this.delegate.onAck) {
const promiseOrVoid = this.delegate.onAck({ message });
if (promiseOrVoid instanceof Promise) {
this.ackProcessing = true; // make sure this is always reset to false
promiseOrVoid.then(() => (this.ackProcessing = false)).catch(() => (this.ackProcessing = false));
}
}
return;
}
// Request within a dialog out of sequence guard.
// https://tools.ietf.org/html/rfc3261#section-12.2.2
if (!this.sequenceGuard(message)) {
this.logger.log(`INVITE dialog ${this.id} rejected out of order ${message.method} request.`);
return;
}
// Request within a dialog common processing.
// https://tools.ietf.org/html/rfc3261#section-12.2.2
super.receiveRequest(message);
// Handle various INVITE related cross-over, glare and race conditions
if (message.method === C.INVITE) {
// Hopefully this message is helpful...
const warning = () => {
const reason = this.ackWait ? "waiting for initial ACK" : "processing initial ACK";
this.logger.warn(`INVITE dialog ${this.id} received re-INVITE while ${reason}`);
let msg = "RFC 5407 suggests the following to avoid this race condition... ";
msg += " Note: Implementation issues are outside the scope of this document,";
msg += " but the following tip is provided for avoiding race conditions of";
msg += " this type. The caller can delay sending re-INVITE F6 for some period";
msg += " of time (2 seconds, perhaps), after which the caller can reasonably";
msg += " assume that its ACK has been received. Implementors can decouple the";
msg += " actions of the user (e.g., pressing the hold button) from the actions";
msg += " of the protocol (the sending of re-INVITE F6), so that the UA can";
msg += " behave like this. In this case, it is the implementor's choice as to";
msg += " how long to wait. In most cases, such an implementation may be";
msg += " useful to prevent the type of race condition shown in this section.";
msg += " This document expresses no preference about whether or not they";
msg += " should wait for an ACK to be delivered. After considering the impact";
msg += " on user experience, implementors should decide whether or not to wait";
msg += " for a while, because the user experience depends on the";
msg += " implementation and has no direct bearing on protocol behavior.";
this.logger.warn(msg);
return; // drop re-INVITE request message
};
// A UAS that receives a second INVITE before it sends the final
// response to a first INVITE with a lower CSeq sequence number on the
// same dialog MUST return a 500 (Server Internal Error) response to the
// second INVITE and MUST include a Retry-After header field with a
// randomly chosen value of between 0 and 10 seconds.
// https://tools.ietf.org/html/rfc3261#section-14.2
const retryAfter = Math.floor(Math.random() * 10) + 1;
const extraHeaders = [`Retry-After: ${retryAfter}`];
// There may be ONLY ONE offer/answer negotiation in progress for a
// single dialog at any point in time. Section 4 explains how to ensure
// this.
// https://tools.ietf.org/html/rfc6337#section-2.2
if (this.ackProcessing) {
// UAS-IsI: While an INVITE server transaction is incomplete or ACK
// transaction associated with an offer/answer is incomplete,
// a UA must reject another INVITE request with a 500
// response.
// https://tools.ietf.org/html/rfc6337#section-4.3
this.core.replyStateless(message, { statusCode: 500, extraHeaders });
warning();
return;
}
// 3.1.4. Callee Receives re-INVITE (Established State) While in the
// Moratorium State (Case 1)
// https://tools.ietf.org/html/rfc5407#section-3.1.4
// 3.1.5. Callee Receives re-INVITE (Established State) While in the
// Moratorium State (Case 2)
// https://tools.ietf.org/html/rfc5407#section-3.1.5
if (this.ackWait && this.signalingState !== SignalingState.Stable) {
// This scenario is basically the same as that of Section 3.1.4, but
// differs in sending an offer in the 200 and an answer in the ACK. In
// contrast to the previous case, the offer in the 200 (F3) and the
// offer in the re-INVITE (F6) collide with each other.
//
// Bob sends a 491 to the re-INVITE (F6) since he is not able to
// properly handle a new request until he receives an answer. (Note:
// 500 with a Retry-After header may be returned if the 491 response is
// understood to indicate request collision. However, 491 is
// recommended here because 500 applies to so many cases that it is
// difficult to determine what the real problem was.)
// https://tools.ietf.org/html/rfc5407#section-3.1.5
// UAS-IsI: While an INVITE server transaction is incomplete or ACK
// transaction associated with an offer/answer is incomplete,
// a UA must reject another INVITE request with a 500
// response.
// https://tools.ietf.org/html/rfc6337#section-4.3
this.core.replyStateless(message, { statusCode: 500, extraHeaders });
warning();
return;
}
// A UAS that receives a second INVITE before it sends the final
// response to a first INVITE with a lower CSeq sequence number on the
// same dialog MUST return a 500 (Server Internal Error) response to the
// second INVITE and MUST include a Retry-After header field with a
// randomly chosen value of between 0 and 10 seconds.
// https://tools.ietf.org/html/rfc3261#section-14.2
if (this.reinviteUserAgentServer) {
this.core.replyStateless(message, { statusCode: 500, extraHeaders });
return;
}
// A UAS that receives an INVITE on a dialog while an INVITE it had sent
// on that dialog is in progress MUST return a 491 (Request Pending)
// response to the received INVITE.
// https://tools.ietf.org/html/rfc3261#section-14.2
if (this.reinviteUserAgentClient) {
this.core.replyStateless(message, { statusCode: 491 });
return;
}
}
// Requests within a dialog MAY contain Record-Route and Contact header
// fields. However, these requests do not cause the dialog's route set
// to be modified, although they may modify the remote target URI.
// Specifically, requests that are not target refresh requests do not
// modify the dialog's remote target URI, and requests that are target
// refresh requests do. For dialogs that have been established with an
// INVITE, the only target refresh request defined is re-INVITE (see
// Section 14). Other extensions may define different target refresh
// requests for dialogs established in other ways.
//
// Note that an ACK is NOT a target refresh request.
//
// Target refresh requests only update the dialog's remote target URI,
// and not the route set formed from the Record-Route. Updating the
// latter would introduce severe backwards compatibility problems with
// RFC 2543-compliant systems.
// https://tools.ietf.org/html/rfc3261#section-15
if (message.method === C.INVITE) {
// FIXME: parser needs to be typed...
const contact = message.parseHeader("contact");
if (!contact) {
// TODO: Review to make sure this will never happen
throw new Error("Contact undefined.");
}
if (!(contact instanceof NameAddrHeader)) {
throw new Error("Contact not instance of NameAddrHeader.");
}
this.dialogState.remoteTarget = contact.uri;
}
// Switch on method and then delegate.
switch (message.method) {
case C.BYE:
// A UAS core receiving a BYE request for an existing dialog MUST follow
// the procedures of Section 12.2.2 to process the request. Once done,
// the UAS SHOULD terminate the session (and therefore stop sending and
// listening for media). The only case where it can elect not to are
// multicast sessions, where participation is possible even if the other
// participant in the dialog has terminated its involvement in the
// session. Whether or not it ends its participation on the session,
// the UAS core MUST generate a 2xx response to the BYE, and MUST pass
// that to the server transaction for transmission.
//
// The UAS MUST still respond to any pending requests received for that
// dialog. It is RECOMMENDED that a 487 (Request Terminated) response
// be generated to those pending requests.
// https://tools.ietf.org/html/rfc3261#section-15.1.2
{
const uas = new ByeUserAgentServer(this, message);
this.delegate && this.delegate.onBye ? this.delegate.onBye(uas) : uas.accept();
this.dispose();
}
break;
case C.INFO:
// If a UA receives an INFO request associated with an Info Package that
// the UA has not indicated willingness to receive, the UA MUST send a
// 469 (Bad Info Package) response (see Section 11.6), which contains a
// Recv-Info header field with Info Packages for which the UA is willing
// to receive INFO requests.
{
const uas = new InfoUserAgentServer(this, message);
this.delegate && this.delegate.onInfo
? this.delegate.onInfo(uas)
: uas.reject({
statusCode: 469,
extraHeaders: ["Recv-Info:"]
});
}
break;
case C.INVITE:
// If the new session description is not acceptable, the UAS can reject
// it by returning a 488 (Not Acceptable Here) response for the re-
// INVITE. This response SHOULD include a Warning header field.
// https://tools.ietf.org/html/rfc3261#section-14.2
{
const uas = new ReInviteUserAgentServer(this, message);
this.signalingStateTransition(message);
this.delegate && this.delegate.onInvite ? this.delegate.onInvite(uas) : uas.reject({ statusCode: 488 }); // TODO: Warning header field.
}
break;
case C.MESSAGE:
{
const uas = new MessageUserAgentServer(this.core, message);
this.delegate && this.delegate.onMessage ? this.delegate.onMessage(uas) : uas.accept();
}
break;
case C.NOTIFY:
// https://tools.ietf.org/html/rfc3515#section-2.4.4
{
const uas = new NotifyUserAgentServer(this, message);
this.delegate && this.delegate.onNotify ? this.delegate.onNotify(uas) : uas.accept();
}
break;
case C.PRACK:
// https://tools.ietf.org/html/rfc3262#section-4
{
const uas = new PrackUserAgentServer(this, message);
this.delegate && this.delegate.onPrack ? this.delegate.onPrack(uas) : uas.accept();
}
break;
case C.REFER:
// https://tools.ietf.org/html/rfc3515#section-2.4.2
{
const uas = new ReferUserAgentServer(this, message);
this.delegate && this.delegate.onRefer ? this.delegate.onRefer(uas) : uas.reject();
}
break;
default:
{
this.logger.log(`INVITE dialog ${this.id} received unimplemented ${message.method} request`);
this.core.replyStateless(message, { statusCode: 501 });
}
break;
}
}
/**
* Guard against out of order reliable provisional responses and retransmissions.
* Returns false if the response should be discarded, otherwise true.
* @param message - Incoming response message within this dialog.
*/
reliableSequenceGuard(message) {
const statusCode = message.statusCode;
if (!statusCode) {
throw new Error("Status code undefined");
}
if (statusCode > 100 && statusCode < 200) {
// If a provisional response is received for an initial request, and
// that response contains a Require header field containing the option
// tag 100rel, the response is to be sent reliably. If the response is
// a 100 (Trying) (as opposed to 101 to 199), this option tag MUST be
// ignored, and the procedures below MUST NOT be used.
// https://tools.ietf.org/html/rfc3262#section-4
const requireHeader = message.getHeader("require");
const rseqHeader = message.getHeader("rseq");
const rseq = requireHeader && requireHeader.includes("100rel") && rseqHeader ? Number(rseqHeader) : undefined;
if (rseq) {
// Handling of subsequent reliable provisional responses for the same
// initial request follows the same rules as above, with the following
// difference: reliable provisional responses are guaranteed to be in
// order. As a result, if the UAC receives another reliable provisional
// response to the same request, and its RSeq value is not one higher
// than the value of the sequence number, that response MUST NOT be
// acknowledged with a PRACK, and MUST NOT be processed further by the
// UAC. An implementation MAY discard the response, or MAY cache the
// response in the hopes of receiving the missing responses.
// https://tools.ietf.org/html/rfc3262#section-4
if (this.rseq && this.rseq + 1 !== rseq) {
return false;
}
// Once a reliable provisional response is received, retransmissions of
// that response MUST be discarded. A response is a retransmission when
// its dialog ID, CSeq, and RSeq match the original response. The UAC
// MUST maintain a sequence number that indicates the most recently
// received in-order reliable provisional response for the initial
// request. This sequence number MUST be maintained until a final
// response is received for the initial request. Its value MUST be
// initialized to the RSeq header field in the first reliable
// provisional response received for the initial request.
// https://tools.ietf.org/html/rfc3262#section-4
this.rseq = this.rseq ? this.rseq + 1 : rseq;
}
}
return true;
}
/**
* If not in a stable signaling state, rollback to prior stable signaling state.
*/
signalingStateRollback() {
if (this._signalingState === SignalingState.HaveLocalOffer ||
this.signalingState === SignalingState.HaveRemoteOffer) {
if (this._rollbackOffer && this._rollbackAnswer) {
this._signalingState = SignalingState.Stable;
this._offer = this._rollbackOffer;
this._answer = this._rollbackAnswer;
}
}
}
/**
* Update the signaling state of the dialog.
* @param message - The message to base the update off of.
*/
signalingStateTransition(message) {
const body = getBody(message);
// No body, no session. No, woman, no cry.
if (!body || body.contentDisposition !== "session") {
return;
}
// We've got an existing offer and answer which we may wish to rollback to
if (this._signalingState === SignalingState.Stable) {
this._rollbackOffer = this._offer;
this._rollbackAnswer = this._answer;
}
// We're in UAS role, receiving incoming request with session description
if (message instanceof IncomingRequestMessage) {
switch (this._signalingState) {
case SignalingState.Initial:
case SignalingState.Stable:
this._signalingState = SignalingState.HaveRemoteOffer;
this._offer = body;
this._answer = undefined;
break;
case SignalingState.HaveLocalOffer:
this._signalingState = SignalingState.Stable;
this._answer = body;
break;
case SignalingState.HaveRemoteOffer:
// You cannot make a new offer while one is in progress.
// https://tools.ietf.org/html/rfc3261#section-13.2.1
// FIXME: What to do here?
break;
case SignalingState.Closed:
break;
default:
throw new Error("Unexpected signaling state.");
}
}
// We're in UAC role, receiving incoming response with session description
if (message instanceof IncomingResponseMessage) {
switch (this._signalingState) {
case SignalingState.Initial:
case SignalingState.Stable:
this._signalingState = SignalingState.HaveRemoteOffer;
this._offer = body;
this._answer = undefined;
break;
case SignalingState.HaveLocalOffer:
this._signalingState = SignalingState.Stable;
this._answer = body;
break;
case SignalingState.HaveRemoteOffer:
// You cannot make a new offer while one is in progress.
// https://tools.ietf.org/html/rfc3261#section-13.2.1
// FIXME: What to do here?
break;
case SignalingState.Closed:
break;
default:
throw new Error("Unexpected signaling state.");
}
}
// We're in UAC role, sending outgoing request with session description
if (message instanceof OutgoingRequestMessage) {
switch (this._signalingState) {
case SignalingState.Initial:
case SignalingState.Stable:
this._signalingState = SignalingState.HaveLocalOffer;
this._offer = body;
this._answer = undefined;
break;
case SignalingState.HaveLocalOffer:
// You cannot make a new offer while one is in progress.
// https://tools.ietf.org/html/rfc3261#section-13.2.1
// FIXME: What to do here?
break;
case SignalingState.HaveRemoteOffer:
this._signalingState = SignalingState.Stable;
this._answer = body;
break;
case SignalingState.Closed:
break;
default:
throw new Error("Unexpected signaling state.");
}
}
// We're in UAS role, sending outgoing response with session description
if (isBody(message)) {
switch (this._signalingState) {
case SignalingState.Initial:
case SignalingState.Stable:
this._signalingState = SignalingState.HaveLocalOffer;
this._offer = body;
this._answer = undefined;
break;
case SignalingState.HaveLocalOffer:
// You cannot make a new offer while one is in progress.
// https://tools.ietf.org/html/rfc3261#section-13.2.1
// FIXME: What to do here?
break;
case SignalingState.HaveRemoteOffer:
this._signalingState = SignalingState.Stable;
this._answer = body;
break;
case SignalingState.Closed:
break;
default:
throw new Error("Unexpected signaling state.");
}
}
}
start2xxRetransmissionTimer() {
if (this.initialTransaction instanceof InviteServerTransaction) {
const transaction = this.initialTransaction;
// Once the response has been constructed, it is passed to the INVITE
// server transaction. In order to ensure reliable end-to-end
// transport of the response, it is necessary to periodically pass
// the response directly to the transport until the ACK arrives. The
// 2xx response is passed to the transport with an interval that
// starts at T1 seconds and doubles for each retransmission until it
// reaches T2 seconds (T1 and T2 are defined in Section 17).
// Response retransmissions cease when an ACK request for the
// response is received. This is independent of whatever transport
// protocols are used to send the response.
// https://tools.ietf.org/html/rfc6026#section-8.1
let timeout = Timers.T1;
const retransmission = () => {
if (!this.ackWait) {
this.invite2xxTimer = undefined;
return;
}
this.logger.log("No ACK for 2xx response received, attempting retransmission");
transaction.retransmitAcceptedResponse();
timeout = Math.min(timeout * 2, Timers.T2);
this.invite2xxTimer = setTimeout(retransmission, timeout);
};
this.invite2xxTimer = setTimeout(retransmission, timeout);
// If the server retransmits the 2xx response for 64*T1 seconds without
// receiving an ACK, the dialog is confirmed, but the session SHOULD be
// terminated. This is accomplished with a BYE, as described in Section 15.
// https://tools.ietf.org/html/rfc3261#section-13.3.1.4
const stateChanged = () => {
if (transaction.state === TransactionState.Terminated) {
transaction.removeStateChangeListener(stateChanged);
if (this.invite2xxTimer) {
clearTimeout(this.invite2xxTimer);
this.invite2xxTimer = undefined;
}
if (this.ackWait) {
if (this.delegate && this.delegate.onAckTimeout) {
this.delegate.onAckTimeout();
}
else {
this.bye();
}
}
}
};
transaction.addStateChangeListener(stateChanged);
}
}
// FIXME: Refactor
startReInvite2xxRetransmissionTimer() {
if (this.reinviteUserAgentServer && this.reinviteUserAgentServer.transaction instanceof InviteServerTransaction) {
const transaction = this.reinviteUserAgentServer.transaction;
// Once the response has been constructed, it is passed to the INVITE
// server transaction. In order to ensure reliable end-to-end
// transport of the response, it is necessary to periodically pass
// the response directly to the transport until the ACK arrives. The
// 2xx response is passed to the transport with an interval that
// starts at T1 seconds and doubles for each retransmission until it
// reaches T2 seconds (T1 and T2 are defined in Section 17).
// Response retransmissions cease when an ACK request for the
// response is received. This is independent of whatever transport
// protocols are used to send the response.
// https://tools.ietf.org/html/rfc6026#section-8.1
let timeout = Timers.T1;
const retransmission = () => {
if (!this.reinviteUserAgentServer) {
this.invite2xxTimer = undefined;
return;
}
this.logger.log("No ACK for 2xx response received, attempting retransmission");
transaction.retransmitAcceptedResponse();
timeout = Math.min(timeout * 2, Timers.T2);
this.invite2xxTimer = setTimeout(retransmission, timeout);
};
this.invite2xxTimer = setTimeout(retransmission, timeout);
// If the server retransmits the 2xx response for 64*T1 seconds without
// receiving an ACK, the dialog is confirmed, but the session SHOULD be
// terminated. This is accomplished with a BYE, as described in Section 15.
// https://tools.ietf.org/html/rfc3261#section-13.3.1.4
const stateChanged = () => {
if (transaction.state === TransactionState.Terminated) {
transaction.removeStateChangeListener(stateChanged);
if (this.invite2xxTimer) {
clearTimeout(this.invite2xxTimer);
this.invite2xxTimer = undefined;
}
if (this.reinviteUserAgentServer) {
// FIXME: TODO: What to do here
}
}
};
transaction.addStateChangeListener(stateChanged);
}
}
}