UNPKG

sip.js

Version:

A SIP library for JavaScript

365 lines (364 loc) 22.5 kB
import { Dialog } from "../dialogs/dialog.js"; import { SessionDialog } from "../dialogs/session-dialog.js"; import { SignalingState } from "../session/session.js"; import { InviteClientTransaction } from "../transactions/invite-client-transaction.js"; import { TransactionState } from "../transactions/transaction-state.js"; import { UserAgentClient } from "./user-agent-client.js"; /** * INVITE UAC. * @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.2 UAC Processing * https://tools.ietf.org/html/rfc3261#section-13.2 * @public */ export class InviteUserAgentClient extends UserAgentClient { constructor(core, message, delegate) { super(InviteClientTransaction, core, message, delegate); this.confirmedDialogAcks = new Map(); this.confirmedDialogs = new Map(); this.earlyDialogs = new Map(); this.delegate = delegate; } dispose() { // The UAC core considers the INVITE transaction completed 64*T1 seconds // after the reception of the first 2xx response. At this point all the // early dialogs that have not transitioned to established dialogs are // terminated. Once the INVITE transaction is considered completed by // the UAC core, no more new 2xx responses are expected to arrive. // // If, after acknowledging any 2xx response to an INVITE, the UAC does // not want to continue with that dialog, then the UAC MUST terminate // the dialog by sending a BYE request as described in Section 15. // https://tools.ietf.org/html/rfc3261#section-13.2.2.4 this.earlyDialogs.forEach((earlyDialog) => earlyDialog.dispose()); this.earlyDialogs.clear(); super.dispose(); } /** * Special case for transport error while sending ACK. * @param error - Transport error */ onTransportError(error) { if (this.transaction.state === TransactionState.Calling) { return super.onTransportError(error); } // If not in 'calling' state, the transport error occurred while sending an ACK. this.logger.error(error.message); this.logger.error("User agent client request transport error while sending ACK."); } /** * Once the INVITE has been passed to the INVITE client transaction, the * UAC waits for responses for the INVITE. * https://tools.ietf.org/html/rfc3261#section-13.2.2 * @param incomingResponse - Incoming response to INVITE request. */ receiveResponse(message) { if (!this.authenticationGuard(message)) { return; } const statusCode = message.statusCode ? message.statusCode.toString() : ""; if (!statusCode) { throw new Error("Response status code undefined."); } switch (true) { case /^100$/.test(statusCode): if (this.delegate && this.delegate.onTrying) { this.delegate.onTrying({ message }); } return; case /^1[0-9]{2}$/.test(statusCode): // Zero, one or multiple provisional responses may arrive before one or // more final responses are received. Provisional responses for an // INVITE request can create "early dialogs". If a provisional response // has a tag in the To field, and if the dialog ID of the response does // not match an existing dialog, one is constructed using the procedures // defined in Section 12.1.2. // // The early dialog will only be needed if the UAC needs to send a // request to its peer within the dialog before the initial INVITE // transaction completes. Header fields present in a provisional // response are applicable as long as the dialog is in the early state // (for example, an Allow header field in a provisional response // contains the methods that can be used in the dialog while this is in // the early state). // https://tools.ietf.org/html/rfc3261#section-13.2.2.1 { // Dialogs are created through the generation of non-failure responses // to requests with specific methods. Within this specification, only // 2xx and 101-199 responses with a To tag, where the request was // INVITE, will establish a dialog. A dialog established by a non-final // response to a request is in the "early" state and it is called an // early dialog. // https://tools.ietf.org/html/rfc3261#section-12.1 // Provisional without to tag, no dialog to create. if (!message.toTag) { this.logger.warn("Non-100 1xx INVITE response received without a to tag, dropping."); return; } // 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 // Provisional without Contact header field, malformed response. const contact = message.parseHeader("contact"); if (!contact) { this.logger.error("Non-100 1xx INVITE response received without a Contact header field, dropping."); return; } // Compute dialog state. const dialogState = Dialog.initialDialogStateForUserAgentClient(this.message, message); // Have existing early dialog or create a new one. let earlyDialog = this.earlyDialogs.get(dialogState.id); if (!earlyDialog) { const transaction = this.transaction; if (!(transaction instanceof InviteClientTransaction)) { throw new Error("Transaction not instance of InviteClientTransaction."); } earlyDialog = new SessionDialog(transaction, this.core, dialogState); this.earlyDialogs.set(earlyDialog.id, earlyDialog); } // Guard against out of order reliable provisional responses. // Note that this is where the rseq tracking is done. if (!earlyDialog.reliableSequenceGuard(message)) { this.logger.warn("1xx INVITE reliable response received out of order or is a retransmission, dropping."); return; } // If the initial offer is in an INVITE, the answer MUST be in a // reliable non-failure message from UAS back to UAC which is // correlated to that INVITE. For this specification, that is // only the final 2xx response to that INVITE. That same exact // answer MAY also be placed in any provisional responses sent // prior to the answer. The UAC MUST treat the first session // description it receives as the answer, and MUST ignore any // session descriptions in subsequent responses to the initial // INVITE. // https://tools.ietf.org/html/rfc3261#section-13.2.1 if (earlyDialog.signalingState === SignalingState.Initial || earlyDialog.signalingState === SignalingState.HaveLocalOffer) { earlyDialog.signalingStateTransition(message); } // Pass response to delegate. const session = earlyDialog; if (this.delegate && this.delegate.onProgress) { this.delegate.onProgress({ message, session, prack: (options) => { const outgoingPrackRequest = session.prack(undefined, options); return outgoingPrackRequest; } }); } } return; case /^2[0-9]{2}$/.test(statusCode): // Multiple 2xx responses may arrive at the UAC for a single INVITE // request due to a forking proxy. Each response is distinguished by // the tag parameter in the To header field, and each represents a // distinct dialog, with a distinct dialog identifier. // // 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. // https://tools.ietf.org/html/rfc3261#section-13.2.2.4 { // Dialogs are created through the generation of non-failure responses // to requests with specific methods. Within this specification, only // 2xx and 101-199 responses with a To tag, where the request was // INVITE, will establish a dialog. A dialog established by a non-final // response to a request is in the "early" state and it is called an // early dialog. // https://tools.ietf.org/html/rfc3261#section-12.1 // Final without to tag, malformed response. if (!message.toTag) { this.logger.error("2xx INVITE response received without a to tag, dropping."); return; } // 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 // Final without Contact header field, malformed response. const contact = message.parseHeader("contact"); if (!contact) { this.logger.error("2xx INVITE response received without a Contact header field, dropping."); return; } // Compute dialog state. const dialogState = Dialog.initialDialogStateForUserAgentClient(this.message, message); // NOTE: Currently our transaction layer is caching the 2xx ACKs and // handling retransmissions of the ACK which is an approach which is // not to spec. In any event, this block is intended to provide a to // spec implementation of ACK retransmissions, but it should not be // hit currently. let dialog = this.confirmedDialogs.get(dialogState.id); if (dialog) { // Once the ACK has been constructed, the procedures of [4] are used to // determine the destination address, port and transport. However, the // request is passed to the transport layer directly for transmission, // rather than a client transaction. This is because the UAC core // handles retransmissions of the ACK, not the transaction layer. The // ACK MUST be passed to the client transport every time a // retransmission of the 2xx final response that triggered the ACK // arrives. // https://tools.ietf.org/html/rfc3261#section-13.2.2.4 const outgoingAckRequest = this.confirmedDialogAcks.get(dialogState.id); if (outgoingAckRequest) { const transaction = this.transaction; if (!(transaction instanceof InviteClientTransaction)) { throw new Error("Client transaction not instance of InviteClientTransaction."); } transaction.ackResponse(outgoingAckRequest.message); } else { // If still waiting for an ACK, drop the retransmission of the 2xx final response. } return; } // 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. // https://tools.ietf.org/html/rfc3261#section-13.2.2.4 dialog = this.earlyDialogs.get(dialogState.id); if (dialog) { dialog.confirm(); dialog.recomputeRouteSet(message); this.earlyDialogs.delete(dialog.id); this.confirmedDialogs.set(dialog.id, dialog); } else { const transaction = this.transaction; if (!(transaction instanceof InviteClientTransaction)) { throw new Error("Transaction not instance of InviteClientTransaction."); } dialog = new SessionDialog(transaction, this.core, dialogState); this.confirmedDialogs.set(dialog.id, dialog); } // If the initial offer is in an INVITE, the answer MUST be in a // reliable non-failure message from UAS back to UAC which is // correlated to that INVITE. For this specification, that is // only the final 2xx response to that INVITE. That same exact // answer MAY also be placed in any provisional responses sent // prior to the answer. The UAC MUST treat the first session // description it receives as the answer, and MUST ignore any // session descriptions in subsequent responses to the initial // INVITE. // https://tools.ietf.org/html/rfc3261#section-13.2.1 if (dialog.signalingState === SignalingState.Initial || dialog.signalingState === SignalingState.HaveLocalOffer) { dialog.signalingStateTransition(message); } // Session Initiated! :) const session = dialog; // 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 if (this.delegate && this.delegate.onAccept) { this.delegate.onAccept({ message, session, ack: (options) => { const outgoingAckRequest = session.ack(options); this.confirmedDialogAcks.set(session.id, outgoingAckRequest); return outgoingAckRequest; } }); } else { const outgoingAckRequest = session.ack(); this.confirmedDialogAcks.set(session.id, outgoingAckRequest); } } return; case /^3[0-9]{2}$/.test(statusCode): // 12.3 Termination of a Dialog // // Independent of the method, if a request outside of a dialog generates // a non-2xx final response, any early dialogs created through // provisional responses to that request are terminated. The mechanism // for terminating confirmed dialogs is method specific. In this // specification, the BYE method terminates a session and the dialog // associated with it. See Section 15 for details. // https://tools.ietf.org/html/rfc3261#section-12.3 // All early dialogs are considered terminated upon reception of the // non-2xx final response. // // After having received the non-2xx final response the UAC core // considers the INVITE transaction completed. The INVITE client // transaction handles the generation of ACKs for the response (see // Section 17). // https://tools.ietf.org/html/rfc3261#section-13.2.2.3 this.earlyDialogs.forEach((earlyDialog) => earlyDialog.dispose()); this.earlyDialogs.clear(); // A 3xx response may contain one or more Contact header field values // providing new addresses where the callee might be reachable. // Depending on the status code of the 3xx response (see Section 21.3), // the UAC MAY choose to try those new addresses. // https://tools.ietf.org/html/rfc3261#section-13.2.2.2 if (this.delegate && this.delegate.onRedirect) { this.delegate.onRedirect({ message }); } return; case /^[4-6][0-9]{2}$/.test(statusCode): // 12.3 Termination of a Dialog // // Independent of the method, if a request outside of a dialog generates // a non-2xx final response, any early dialogs created through // provisional responses to that request are terminated. The mechanism // for terminating confirmed dialogs is method specific. In this // specification, the BYE method terminates a session and the dialog // associated with it. See Section 15 for details. // https://tools.ietf.org/html/rfc3261#section-12.3 // All early dialogs are considered terminated upon reception of the // non-2xx final response. // // After having received the non-2xx final response the UAC core // considers the INVITE transaction completed. The INVITE client // transaction handles the generation of ACKs for the response (see // Section 17). // https://tools.ietf.org/html/rfc3261#section-13.2.2.3 this.earlyDialogs.forEach((earlyDialog) => earlyDialog.dispose()); this.earlyDialogs.clear(); // A single non-2xx final response may be received for the INVITE. 4xx, // 5xx and 6xx responses may contain a Contact header field value // indicating the location where additional information about the error // can be found. Subsequent final responses (which would only arrive // under error conditions) MUST be ignored. // https://tools.ietf.org/html/rfc3261#section-13.2.2.3 if (this.delegate && this.delegate.onReject) { this.delegate.onReject({ message }); } return; default: throw new Error(`Invalid status code ${statusCode}`); } throw new Error(`Executing what should be an unreachable code path receiving ${statusCode} response.`); } }