sip.js
Version:
A SIP library for JavaScript
234 lines (233 loc) • 13 kB
JavaScript
import { Dialog } from "../dialogs/dialog.js";
import { SessionDialog } from "../dialogs/session-dialog.js";
import { TransactionStateError } from "../exceptions/transaction-state-error.js";
import { SignalingState } from "../session/session.js";
import { InviteServerTransaction } from "../transactions/invite-server-transaction.js";
import { AllowedMethods } from "../user-agent-core/allowed-methods.js";
import { UserAgentServer } from "./user-agent-server.js";
/**
* INVITE UAS.
* @remarks
* 13 Initiating a Session
* https://tools.ietf.org/html/rfc3261#section-13
* 13.1 Overview
* https://tools.ietf.org/html/rfc3261#section-13.1
* 13.3 UAS Processing
* https://tools.ietf.org/html/rfc3261#section-13.3
* @public
*/
export class InviteUserAgentServer extends UserAgentServer {
constructor(core, message, delegate) {
super(InviteServerTransaction, core, message, delegate);
this.core = core;
}
dispose() {
if (this.earlyDialog) {
this.earlyDialog.dispose();
}
super.dispose();
}
/**
* 13.3.1.4 The INVITE is Accepted
* The UAS core generates a 2xx response. This response establishes a
* dialog, and therefore follows the procedures of Section 12.1.1 in
* addition to those of Section 8.2.6.
* https://tools.ietf.org/html/rfc3261#section-13.3.1.4
* @param options - Accept options bucket.
*/
accept(options = { statusCode: 200 }) {
if (!this.acceptable) {
throw new TransactionStateError(`${this.message.method} not acceptable in state ${this.transaction.state}.`);
}
// This response establishes a dialog...
// https://tools.ietf.org/html/rfc3261#section-13.3.1.4
if (!this.confirmedDialog) {
if (this.earlyDialog) {
this.earlyDialog.confirm();
this.confirmedDialog = this.earlyDialog;
this.earlyDialog = undefined;
}
else {
const transaction = this.transaction;
if (!(transaction instanceof InviteServerTransaction)) {
throw new Error("Transaction not instance of InviteClientTransaction.");
}
const state = Dialog.initialDialogStateForUserAgentServer(this.message, this.toTag);
this.confirmedDialog = new SessionDialog(transaction, this.core, state);
}
}
// When a UAS responds to a request with a response that establishes a
// dialog (such as a 2xx to INVITE), the UAS MUST copy all Record-Route
// header field values from the request into the response (including the
// URIs, URI parameters, and any Record-Route header field parameters,
// whether they are known or unknown to the UAS) and MUST maintain the
// order of those values. The UAS MUST add a Contact header field to
// the response. The Contact header field contains an address where the
// UAS would like to be contacted for subsequent requests in the dialog
// (which includes the ACK for a 2xx response in the case of an INVITE).
// Generally, the host portion of this URI is the IP address or FQDN of
// the host. The URI provided in the Contact header field MUST be a SIP
// or SIPS URI. If the request that initiated the dialog contained a
// SIPS URI in the Request-URI or in the top Record-Route header field
// value, if there was any, or the Contact header field if there was no
// Record-Route header field, the Contact header field in the response
// MUST be a SIPS URI. The URI SHOULD have global scope (that is, the
// same URI can be used in messages outside this dialog). The same way,
// the scope of the URI in the Contact header field of the INVITE is not
// limited to this dialog either. It can therefore be used in messages
// to the UAC even outside this dialog.
// https://tools.ietf.org/html/rfc3261#section-12.1.1
const recordRouteHeader = this.message.getHeaders("record-route").map((header) => `Record-Route: ${header}`);
const contactHeader = `Contact: ${this.core.configuration.contact.toString()}`;
// A 2xx response to an INVITE SHOULD contain the Allow header field and
// the Supported header field, and MAY contain the Accept header field.
// Including these header fields allows the UAC to determine the
// features and extensions supported by the UAS for the duration of the
// call, without probing.
// https://tools.ietf.org/html/rfc3261#section-13.3.1.4
// FIXME: TODO: This should not be hard coded.
const allowHeader = "Allow: " + AllowedMethods.toString();
// FIXME: TODO: Supported header (see reply())
// FIXME: TODO: Accept header
// If the INVITE request contained an offer, and the UAS had not yet
// sent an answer, the 2xx MUST contain an answer. If the INVITE did
// not contain an offer, the 2xx MUST contain an offer if the UAS had
// not yet sent an offer.
// https://tools.ietf.org/html/rfc3261#section-13.3.1.4
if (!options.body) {
if (this.confirmedDialog.signalingState === SignalingState.Stable) {
options.body = this.confirmedDialog.answer; // resend the answer sent in provisional response
}
else if (this.confirmedDialog.signalingState === SignalingState.Initial ||
this.confirmedDialog.signalingState === SignalingState.HaveRemoteOffer) {
throw new Error("Response must have a body.");
}
}
options.statusCode = options.statusCode || 200;
options.extraHeaders = options.extraHeaders || [];
options.extraHeaders = options.extraHeaders.concat(recordRouteHeader);
options.extraHeaders.push(allowHeader);
options.extraHeaders.push(contactHeader);
const response = super.accept(options);
const session = this.confirmedDialog;
const result = Object.assign(Object.assign({}, response), { session });
// Update dialog signaling state
if (options.body) {
// Once the UAS has sent or received an answer to the initial
// offer, it MUST NOT generate subsequent offers in any responses
// to the initial INVITE. This means that a UAS based on this
// specification alone can never generate subsequent offers until
// completion of the initial transaction.
// https://tools.ietf.org/html/rfc3261#section-13.2.1
if (this.confirmedDialog.signalingState !== SignalingState.Stable) {
this.confirmedDialog.signalingStateTransition(options.body);
}
}
return result;
}
/**
* 13.3.1.1 Progress
* If the UAS is not able to answer the invitation immediately, it can
* choose to indicate some kind of progress to the UAC (for example, an
* indication that a phone is ringing). This is accomplished with a
* provisional response between 101 and 199. These provisional
* responses establish early dialogs and therefore follow the procedures
* of Section 12.1.1 in addition to those of Section 8.2.6. A UAS MAY
* send as many provisional responses as it likes. Each of these MUST
* indicate the same dialog ID. However, these will not be delivered
* reliably.
*
* If the UAS desires an extended period of time to answer the INVITE,
* it will need to ask for an "extension" in order to prevent proxies
* from canceling the transaction. A proxy has the option of canceling
* a transaction when there is a gap of 3 minutes between responses in a
* transaction. To prevent cancellation, the UAS MUST send a non-100
* provisional response at every minute, to handle the possibility of
* lost provisional responses.
* https://tools.ietf.org/html/rfc3261#section-13.3.1.1
* @param options - Progress options bucket.
*/
progress(options = { statusCode: 180 }) {
if (!this.progressable) {
throw new TransactionStateError(`${this.message.method} not progressable in state ${this.transaction.state}.`);
}
// This response establishes a dialog...
// https://tools.ietf.org/html/rfc3261#section-13.3.1.4
if (!this.earlyDialog) {
const transaction = this.transaction;
if (!(transaction instanceof InviteServerTransaction)) {
throw new Error("Transaction not instance of InviteClientTransaction.");
}
const state = Dialog.initialDialogStateForUserAgentServer(this.message, this.toTag, true);
this.earlyDialog = new SessionDialog(transaction, this.core, state);
}
// When a UAS responds to a request with a response that establishes a
// dialog (such as a 2xx to INVITE), the UAS MUST copy all Record-Route
// header field values from the request into the response (including the
// URIs, URI parameters, and any Record-Route header field parameters,
// whether they are known or unknown to the UAS) and MUST maintain the
// order of those values. The UAS MUST add a Contact header field to
// the response. The Contact header field contains an address where the
// UAS would like to be contacted for subsequent requests in the dialog
// (which includes the ACK for a 2xx response in the case of an INVITE).
// Generally, the host portion of this URI is the IP address or FQDN of
// the host. The URI provided in the Contact header field MUST be a SIP
// or SIPS URI. If the request that initiated the dialog contained a
// SIPS URI in the Request-URI or in the top Record-Route header field
// value, if there was any, or the Contact header field if there was no
// Record-Route header field, the Contact header field in the response
// MUST be a SIPS URI. The URI SHOULD have global scope (that is, the
// same URI can be used in messages outside this dialog). The same way,
// the scope of the URI in the Contact header field of the INVITE is not
// limited to this dialog either. It can therefore be used in messages
// to the UAC even outside this dialog.
// https://tools.ietf.org/html/rfc3261#section-12.1.1
const recordRouteHeader = this.message.getHeaders("record-route").map((header) => `Record-Route: ${header}`);
const contactHeader = `Contact: ${this.core.configuration.contact}`;
options.extraHeaders = options.extraHeaders || [];
options.extraHeaders = options.extraHeaders.concat(recordRouteHeader);
options.extraHeaders.push(contactHeader);
const response = super.progress(options);
const session = this.earlyDialog;
const result = Object.assign(Object.assign({}, response), { session });
// Update dialog signaling state
if (options.body) {
// Once the UAS has sent or received an answer to the initial
// offer, it MUST NOT generate subsequent offers in any responses
// to the initial INVITE. This means that a UAS based on this
// specification alone can never generate subsequent offers until
// completion of the initial transaction.
// https://tools.ietf.org/html/rfc3261#section-13.2.1
if (this.earlyDialog.signalingState !== SignalingState.Stable) {
this.earlyDialog.signalingStateTransition(options.body);
}
}
return result;
}
/**
* 13.3.1.2 The INVITE is Redirected
* If the UAS decides to redirect the call, a 3xx response is sent. A
* 300 (Multiple Choices), 301 (Moved Permanently) or 302 (Moved
* Temporarily) response SHOULD contain a Contact header field
* containing one or more URIs of new addresses to be tried. The
* response is passed to the INVITE server transaction, which will deal
* with its retransmissions.
* https://tools.ietf.org/html/rfc3261#section-13.3.1.2
* @param contacts - Contacts to redirect to.
* @param options - Redirect options bucket.
*/
redirect(contacts, options = { statusCode: 302 }) {
return super.redirect(contacts, options);
}
/**
* 13.3.1.3 The INVITE is Rejected
* A common scenario occurs when the callee is currently not willing or
* able to take additional calls at this end system. A 486 (Busy Here)
* SHOULD be returned in such a scenario.
* https://tools.ietf.org/html/rfc3261#section-13.3.1.3
* @param options - Reject options bucket.
*/
reject(options = { statusCode: 486 }) {
return super.reject(options);
}
}