sip.js
Version:
A SIP library for JavaScript
534 lines (533 loc) • 27 kB
JavaScript
import { NameAddrHeader } from "../../grammar/name-addr-header.js";
import { C } from "../messages/methods/constants.js";
/**
* Dialog.
* @remarks
* A key concept for a user agent is that of a dialog. A dialog
* represents a peer-to-peer SIP relationship between two user agents
* that persists for some time. The dialog facilitates sequencing of
* messages between the user agents and proper routing of requests
* between both of them. The dialog represents a context in which to
* interpret SIP messages.
* https://tools.ietf.org/html/rfc3261#section-12
* @public
*/
export class Dialog {
/**
* Dialog constructor.
* @param core - User agent core.
* @param dialogState - Initial dialog state.
*/
constructor(core, dialogState) {
this.core = core;
this.dialogState = dialogState;
this.core.dialogs.set(this.id, this);
}
/**
* When a UAC receives a response that establishes a dialog, it
* constructs the state of the dialog. This state MUST be maintained
* for the duration of the dialog.
* https://tools.ietf.org/html/rfc3261#section-12.1.2
* @param outgoingRequestMessage - Outgoing request message for dialog.
* @param incomingResponseMessage - Incoming response message creating dialog.
*/
static initialDialogStateForUserAgentClient(outgoingRequestMessage, incomingResponseMessage) {
// If the request was sent over TLS, and the Request-URI contained a
// SIPS URI, the "secure" flag is set to TRUE.
// https://tools.ietf.org/html/rfc3261#section-12.1.2
const secure = false; // FIXME: Currently no support for TLS.
// The route set MUST be set to the list of URIs in the Record-Route
// header field from the response, taken in reverse order and preserving
// all URI parameters. If no Record-Route header field is present in
// the response, the route set MUST be set to the empty set. This route
// set, even if empty, overrides any pre-existing route set for future
// requests in this dialog. The remote target MUST be set to the URI
// from the Contact header field of the response.
// https://tools.ietf.org/html/rfc3261#section-12.1.2
const routeSet = incomingResponseMessage.getHeaders("record-route").reverse();
// 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.
// https://tools.ietf.org/html/rfc3261#section-12.1.1
const contact = incomingResponseMessage.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.");
}
const remoteTarget = contact.uri;
// The local sequence number MUST be set to the value of the sequence
// number in the CSeq header field of the request. The remote sequence
// number MUST be empty (it is established when the remote UA sends a
// request within the dialog). The call identifier component of the
// dialog ID MUST be set to the value of the Call-ID in the request.
// The local tag component of the dialog ID MUST be set to the tag in
// the From field in the request, and the remote tag component of the
// dialog ID MUST be set to the tag in the To field of the response. A
// UAC MUST be prepared to receive a response without a tag in the To
// field, in which case the tag is considered to have a value of null.
//
// This is to maintain backwards compatibility with RFC 2543, which
// did not mandate To tags.
//
// https://tools.ietf.org/html/rfc3261#section-12.1.2
const localSequenceNumber = outgoingRequestMessage.cseq;
const remoteSequenceNumber = undefined;
const callId = outgoingRequestMessage.callId;
const localTag = outgoingRequestMessage.fromTag;
const remoteTag = incomingResponseMessage.toTag;
if (!callId) {
// TODO: Review to make sure this will never happen
throw new Error("Call id undefined.");
}
if (!localTag) {
// TODO: Review to make sure this will never happen
throw new Error("From tag undefined.");
}
if (!remoteTag) {
// TODO: Review to make sure this will never happen
throw new Error("To tag undefined."); // FIXME: No backwards compatibility with RFC 2543
}
// The remote URI MUST be set to the URI in the To field, and the local
// URI MUST be set to the URI in the From field.
// https://tools.ietf.org/html/rfc3261#section-12.1.2
if (!outgoingRequestMessage.from) {
// TODO: Review to make sure this will never happen
throw new Error("From undefined.");
}
if (!outgoingRequestMessage.to) {
// TODO: Review to make sure this will never happen
throw new Error("To undefined.");
}
const localURI = outgoingRequestMessage.from.uri;
const remoteURI = outgoingRequestMessage.to.uri;
// A dialog can also be in the "early" state, which occurs when it is
// created with a provisional response, and then transition to the
// "confirmed" state when a 2xx final response arrives.
// https://tools.ietf.org/html/rfc3261#section-12
if (!incomingResponseMessage.statusCode) {
throw new Error("Incoming response status code undefined.");
}
const early = incomingResponseMessage.statusCode < 200 ? true : false;
const dialogState = {
id: callId + localTag + remoteTag,
early,
callId,
localTag,
remoteTag,
localSequenceNumber,
remoteSequenceNumber,
localURI,
remoteURI,
remoteTarget,
routeSet,
secure
};
return dialogState;
}
/**
* The UAS then constructs the state of the dialog. This state MUST be
* maintained for the duration of the dialog.
* https://tools.ietf.org/html/rfc3261#section-12.1.1
* @param incomingRequestMessage - Incoming request message creating dialog.
* @param toTag - Tag in the To field in the response to the incoming request.
*/
static initialDialogStateForUserAgentServer(incomingRequestMessage, toTag, early = false) {
// If the request arrived over TLS, and the Request-URI contained a SIPS
// URI, the "secure" flag is set to TRUE.
// https://tools.ietf.org/html/rfc3261#section-12.1.1
const secure = false; // FIXME: Currently no support for TLS.
// The route set MUST be set to the list of URIs in the Record-Route
// header field from the request, taken in order and preserving all URI
// parameters. If no Record-Route header field is present in the
// request, the route set MUST be set to the empty set. This route set,
// even if empty, overrides any pre-existing route set for future
// requests in this dialog. The remote target MUST be set to the URI
// from the Contact header field of the request.
// https://tools.ietf.org/html/rfc3261#section-12.1.1
const routeSet = incomingRequestMessage.getHeaders("record-route");
const contact = incomingRequestMessage.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.");
}
const remoteTarget = contact.uri;
// The remote sequence number MUST be set to the value of the sequence
// number in the CSeq header field of the request. The local sequence
// number MUST be empty. The call identifier component of the dialog ID
// MUST be set to the value of the Call-ID in the request. The local
// tag component of the dialog ID MUST be set to the tag in the To field
// in the response to the request (which always includes a tag), and the
// remote tag component of the dialog ID MUST be set to the tag from the
// From field in the request. A UAS MUST be prepared to receive a
// request without a tag in the From field, in which case the tag is
// considered to have a value of null.
//
// This is to maintain backwards compatibility with RFC 2543, which
// did not mandate From tags.
//
// https://tools.ietf.org/html/rfc3261#section-12.1.1
const remoteSequenceNumber = incomingRequestMessage.cseq;
const localSequenceNumber = undefined;
const callId = incomingRequestMessage.callId;
const localTag = toTag;
const remoteTag = incomingRequestMessage.fromTag;
// The remote URI MUST be set to the URI in the From field, and the
// local URI MUST be set to the URI in the To field.
// https://tools.ietf.org/html/rfc3261#section-12.1.1
const remoteURI = incomingRequestMessage.from.uri;
const localURI = incomingRequestMessage.to.uri;
const dialogState = {
id: callId + localTag + remoteTag,
early,
callId,
localTag,
remoteTag,
localSequenceNumber,
remoteSequenceNumber,
localURI,
remoteURI,
remoteTarget,
routeSet,
secure
};
return dialogState;
}
/** Destructor. */
dispose() {
this.core.dialogs.delete(this.id);
}
/**
* A dialog is identified at each UA with a dialog ID, which consists of
* a Call-ID value, a local tag and a remote tag. The dialog ID at each
* UA involved in the dialog is not the same. Specifically, the local
* tag at one UA is identical to the remote tag at the peer UA. The
* tags are opaque tokens that facilitate the generation of unique
* dialog IDs.
* https://tools.ietf.org/html/rfc3261#section-12
*/
get id() {
return this.dialogState.id;
}
/**
* A dialog can also be in the "early" state, which occurs when it is
* created with a provisional response, and then it transition to the
* "confirmed" state when a 2xx final response received or is sent.
*
* Note: RFC 3261 is concise on when a dialog is "confirmed", but it
* can be a point of confusion if an INVITE dialog is "confirmed" after
* a 2xx is sent or after receiving the ACK for the 2xx response.
* With careful reading it can be inferred a dialog is always is
* "confirmed" when the 2xx is sent (regardless of type of dialog).
* However a INVITE dialog does have additional considerations
* when it is confirmed but an ACK has not yet been received (in
* particular with regard to a callee sending BYE requests).
*/
get early() {
return this.dialogState.early;
}
/** Call identifier component of the dialog id. */
get callId() {
return this.dialogState.callId;
}
/** Local tag component of the dialog id. */
get localTag() {
return this.dialogState.localTag;
}
/** Remote tag component of the dialog id. */
get remoteTag() {
return this.dialogState.remoteTag;
}
/** Local sequence number (used to order requests from the UA to its peer). */
get localSequenceNumber() {
return this.dialogState.localSequenceNumber;
}
/** Remote sequence number (used to order requests from its peer to the UA). */
get remoteSequenceNumber() {
return this.dialogState.remoteSequenceNumber;
}
/** Local URI. */
get localURI() {
return this.dialogState.localURI;
}
/** Remote URI. */
get remoteURI() {
return this.dialogState.remoteURI;
}
/** Remote target. */
get remoteTarget() {
return this.dialogState.remoteTarget;
}
/**
* Route set, which is an ordered list of URIs. The route set is the
* list of servers that need to be traversed to send a request to the peer.
*/
get routeSet() {
return this.dialogState.routeSet;
}
/**
* If the request was sent over TLS, and the Request-URI contained
* a SIPS URI, the "secure" flag is set to true. *NOT IMPLEMENTED*
*/
get secure() {
return this.dialogState.secure;
}
/** The user agent core servicing this dialog. */
get userAgentCore() {
return this.core;
}
/** Confirm the dialog. Only matters if dialog is currently early. */
confirm() {
this.dialogState.early = false;
}
/**
* 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.
*
* Note that some requests, such as INVITEs, affect several pieces of
* state.
*
* https://tools.ietf.org/html/rfc3261#section-12.2.2
* @param message - Incoming request message within this dialog.
*/
receiveRequest(message) {
// ACK guard.
// By convention, the handling of ACKs is the responsibility
// the particular dialog implementation. For example, see SessionDialog.
// Furthermore, ACKs have same sequence number as the associated INVITE.
if (message.method === C.ACK) {
return;
}
// If the remote sequence number was not empty, but the sequence number
// of the request is lower than the remote sequence number, the request
// is out of order and MUST be rejected with a 500 (Server Internal
// Error) response. If the remote sequence number was not empty, and
// the sequence number of the request is greater than the remote
// sequence number, the request is in order. It is possible for the
// CSeq sequence number to be higher than the remote sequence number by
// more than one. This is not an error condition, and a UAS SHOULD be
// prepared to receive and process requests with CSeq values more than
// one higher than the previous received request. The UAS MUST then set
// the remote sequence number to the value of the sequence number in the
// CSeq header field value in the request.
//
// If a proxy challenges a request generated by the UAC, the UAC has
// to resubmit the request with credentials. The resubmitted request
// will have a new CSeq number. The UAS will never see the first
// request, and thus, it will notice a gap in the CSeq number space.
// Such a gap does not represent any error condition.
//
// https://tools.ietf.org/html/rfc3261#section-12.2.2
if (this.remoteSequenceNumber) {
if (message.cseq <= this.remoteSequenceNumber) {
throw new Error("Out of sequence in dialog request. Did you forget to call sequenceGuard()?");
}
this.dialogState.remoteSequenceNumber = message.cseq;
}
// If the remote sequence number is empty, it MUST be set to the value
// of the sequence number in the CSeq header field value in the request.
// https://tools.ietf.org/html/rfc3261#section-12.2.2
if (!this.remoteSequenceNumber) {
this.dialogState.remoteSequenceNumber = message.cseq;
}
// When a UAS receives a target refresh request, it MUST replace the
// dialog's remote target URI with the URI from the Contact header field
// in that request, if present.
// https://tools.ietf.org/html/rfc3261#section-12.2.2
// Note: "target refresh request" processing delegated to sub-class.
}
/**
* If the dialog identifier in the 2xx response matches the dialog
* identifier of an existing dialog, the dialog MUST be transitioned to
* the "confirmed" state, and the route set for the dialog MUST be
* recomputed based on the 2xx response using the procedures of Section
* 12.2.1.2. Otherwise, a new dialog in the "confirmed" state MUST be
* constructed using the procedures of Section 12.1.2.
*
* Note that the only piece of state that is recomputed is the route
* set. Other pieces of state such as the highest sequence numbers
* (remote and local) sent within the dialog are not recomputed. The
* route set only is recomputed for backwards compatibility. RFC
* 2543 did not mandate mirroring of the Record-Route header field in
* a 1xx, only 2xx. However, we cannot update the entire state of
* the dialog, since mid-dialog requests may have been sent within
* the early dialog, modifying the sequence numbers, for example.
*
* https://tools.ietf.org/html/rfc3261#section-13.2.2.4
*/
recomputeRouteSet(message) {
this.dialogState.routeSet = message.getHeaders("record-route").reverse();
}
/**
* A request within a dialog is constructed by using many of the
* components of the state stored as part of the dialog.
* https://tools.ietf.org/html/rfc3261#section-12.2.1.1
* @param method - Outgoing request method.
*/
createOutgoingRequestMessage(method, options) {
// The URI in the To field of the request MUST be set to the remote URI
// from the dialog state. The tag in the To header field of the request
// MUST be set to the remote tag of the dialog ID. The From URI of the
// request MUST be set to the local URI from the dialog state. The tag
// in the From header field of the request MUST be set to the local tag
// of the dialog ID. If the value of the remote or local tags is null,
// the tag parameter MUST be omitted from the To or From header fields,
// respectively.
//
// Usage of the URI from the To and From fields in the original
// request within subsequent requests is done for backwards
// compatibility with RFC 2543, which used the URI for dialog
// identification. In this specification, only the tags are used for
// dialog identification. It is expected that mandatory reflection
// of the original To and From URI in mid-dialog requests will be
// deprecated in a subsequent revision of this specification.
// https://tools.ietf.org/html/rfc3261#section-12.2.1.1
const toUri = this.remoteURI;
const toTag = this.remoteTag;
const fromUri = this.localURI;
const fromTag = this.localTag;
// The Call-ID of the request MUST be set to the Call-ID of the dialog.
// Requests within a dialog MUST contain strictly monotonically
// increasing and contiguous CSeq sequence numbers (increasing-by-one)
// in each direction (excepting ACK and CANCEL of course, whose numbers
// equal the requests being acknowledged or cancelled). Therefore, if
// the local sequence number is not empty, the value of the local
// sequence number MUST be incremented by one, and this value MUST be
// placed into the CSeq header field. If the local sequence number is
// empty, an initial value MUST be chosen using the guidelines of
// Section 8.1.1.5. The method field in the CSeq header field value
// MUST match the method of the request.
// https://tools.ietf.org/html/rfc3261#section-12.2.1.1
const callId = this.callId;
let cseq;
if (options && options.cseq) {
cseq = options.cseq;
}
else if (!this.dialogState.localSequenceNumber) {
cseq = this.dialogState.localSequenceNumber = 1; // https://tools.ietf.org/html/rfc3261#section-8.1.1.5
}
else {
cseq = this.dialogState.localSequenceNumber += 1;
}
// The UAC uses the remote target and route set to build the Request-URI
// and Route header field of the request.
//
// If the route set is empty, the UAC MUST place the remote target URI
// into the Request-URI. The UAC MUST NOT add a Route header field to
// the request.
//
// If the route set is not empty, and the first URI in the route set
// contains the lr parameter (see Section 19.1.1), the UAC MUST place
// the remote target URI into the Request-URI and MUST include a Route
// header field containing the route set values in order, including all
// parameters.
//
// If the route set is not empty, and its first URI does not contain the
// lr parameter, the UAC MUST place the first URI from the route set
// into the Request-URI, stripping any parameters that are not allowed
// in a Request-URI. The UAC MUST add a Route header field containing
// the remainder of the route set values in order, including all
// parameters. The UAC MUST then place the remote target URI into the
// Route header field as the last value.
// https://tools.ietf.org/html/rfc3261#section-12.2.1.1
// The lr parameter, when present, indicates that the element
// responsible for this resource implements the routing mechanisms
// specified in this document. This parameter will be used in the
// URIs proxies place into Record-Route header field values, and
// may appear in the URIs in a pre-existing route set.
//
// This parameter is used to achieve backwards compatibility with
// systems implementing the strict-routing mechanisms of RFC 2543
// and the rfc2543bis drafts up to bis-05. An element preparing
// to send a request based on a URI not containing this parameter
// can assume the receiving element implements strict-routing and
// reformat the message to preserve the information in the
// Request-URI.
// https://tools.ietf.org/html/rfc3261#section-19.1.1
// NOTE: Not backwards compatible with RFC 2543 (no support for strict-routing).
const ruri = this.remoteTarget;
const routeSet = this.routeSet;
const extraHeaders = options && options.extraHeaders;
const body = options && options.body;
// The relative order of header fields with different field names is not
// significant. However, it is RECOMMENDED that header fields which are
// needed for proxy processing (Via, Route, Record-Route, Proxy-Require,
// Max-Forwards, and Proxy-Authorization, for example) appear towards
// the top of the message to facilitate rapid parsing.
// https://tools.ietf.org/html/rfc3261#section-7.3.1
const message = this.userAgentCore.makeOutgoingRequestMessage(method, ruri, fromUri, toUri, {
callId,
cseq,
fromTag,
toTag,
routeSet
}, extraHeaders, body);
return message;
}
/**
* Increment the local sequence number by one.
* It feels like this should be protected, but the current authentication handling currently
* needs this to keep the dialog in sync when "auto re-sends" request messages.
* @internal
*/
incrementLocalSequenceNumber() {
if (!this.dialogState.localSequenceNumber) {
throw new Error("Local sequence number undefined.");
}
this.dialogState.localSequenceNumber += 1;
}
/**
* If the remote sequence number was not empty, but the sequence number
* of the request is lower than the remote sequence number, the request
* is out of order and MUST be rejected with a 500 (Server Internal
* Error) response.
* https://tools.ietf.org/html/rfc3261#section-12.2.2
* @param request - Incoming request to guard.
* @returns True if the program execution is to continue in the branch in question.
* Otherwise a 500 Server Internal Error was stateless sent and request processing must stop.
*/
sequenceGuard(message) {
// ACK guard.
// By convention, handling of unexpected ACKs is responsibility
// the particular dialog implementation. For example, see SessionDialog.
// Furthermore, we cannot reply to an "out of sequence" ACK.
if (message.method === C.ACK) {
return true;
}
// Note: We are rejecting on "less than or equal to" the remote
// sequence number (excepting ACK whose numbers equal the requests
// being acknowledged or cancelled), which is the correct thing to
// do in our case. The only time a request with the same sequence number
// will show up here if is a) it is a very late retransmission of a
// request we already handled or b) it is a different request with the
// same sequence number which would be violation of the standard.
// Request retransmissions are absorbed by the transaction layer,
// so any request with a duplicate sequence number getting here
// would have to be a retransmission after the transaction terminated
// or a broken request (with unique via branch value).
// Requests within a dialog MUST contain strictly monotonically
// increasing and contiguous CSeq sequence numbers (increasing-by-one)
// in each direction (excepting ACK and CANCEL of course, whose numbers
// equal the requests being acknowledged or cancelled). Therefore, if
// the local sequence number is not empty, the value of the local
// sequence number MUST be incremented by one, and this value MUST be
// placed into the CSeq header field.
// https://tools.ietf.org/html/rfc3261#section-12.2.1.1
if (this.remoteSequenceNumber && message.cseq <= this.remoteSequenceNumber) {
this.core.replyStateless(message, { statusCode: 500 });
return false;
}
return true;
}
}