UNPKG

sip.js

Version:

A SIP library for JavaScript

949 lines 51.2 kB
import { Grammar } from "../grammar/grammar.js"; import { C } from "../core/messages/methods/constants.js"; import { SignalingState } from "../core/session/session.js"; import { getReasonPhrase, newTag } from "../core/messages/utils.js"; import { Session } from "./session.js"; import { SessionState } from "./session-state.js"; import { SIPExtension } from "./user-agent-options.js"; /** * An inviter offers to establish a {@link Session} (outgoing INVITE). * @public */ export class Inviter extends Session { /** * Constructs a new instance of the `Inviter` class. * @param userAgent - User agent. See {@link UserAgent} for details. * @param targetURI - Request URI identifying the target of the message. * @param options - Options bucket. See {@link InviterOptions} for details. */ constructor(userAgent, targetURI, options = {}) { super(userAgent, options); /** True if dispose() has been called. */ this.disposed = false; /** True if early media use is enabled. */ this.earlyMedia = false; /** The early media session description handlers. */ this.earlyMediaSessionDescriptionHandlers = new Map(); /** True if cancel() was called. */ this.isCanceled = false; /** True if initial INVITE without SDP. */ this.inviteWithoutSdp = false; this.logger = userAgent.getLogger("sip.Inviter"); // Early media this.earlyMedia = options.earlyMedia !== undefined ? options.earlyMedia : this.earlyMedia; // From tag this.fromTag = newTag(); // Invite without SDP this.inviteWithoutSdp = options.inviteWithoutSdp !== undefined ? options.inviteWithoutSdp : this.inviteWithoutSdp; // Inviter options (could do better copying these options) const inviterOptions = Object.assign({}, options); inviterOptions.params = Object.assign({}, options.params); // Anonymous call const anonymous = options.anonymous || false; // Contact const contact = userAgent.contact.toString({ anonymous, // Do not add ;ob in initial forming dialog requests if the // registration over the current connection got a GRUU URI. outbound: anonymous ? !userAgent.contact.tempGruu : !userAgent.contact.pubGruu }); // FIXME: TODO: We should not be parsing URIs here as if it fails we have to throw an exception // which is not something we want our constructor to do. URIs should be passed in as params. // URIs if (anonymous && userAgent.configuration.uri) { inviterOptions.params.fromDisplayName = "Anonymous"; inviterOptions.params.fromUri = "sip:anonymous@anonymous.invalid"; } let fromURI = userAgent.userAgentCore.configuration.aor; if (inviterOptions.params.fromUri) { fromURI = typeof inviterOptions.params.fromUri === "string" ? Grammar.URIParse(inviterOptions.params.fromUri) : inviterOptions.params.fromUri; } if (!fromURI) { throw new TypeError("Invalid from URI: " + inviterOptions.params.fromUri); } let toURI = targetURI; if (inviterOptions.params.toUri) { toURI = typeof inviterOptions.params.toUri === "string" ? Grammar.URIParse(inviterOptions.params.toUri) : inviterOptions.params.toUri; } if (!toURI) { throw new TypeError("Invalid to URI: " + inviterOptions.params.toUri); } // Params const messageOptions = Object.assign({}, inviterOptions.params); messageOptions.fromTag = this.fromTag; // Extra headers const extraHeaders = (inviterOptions.extraHeaders || []).slice(); if (anonymous && userAgent.configuration.uri) { extraHeaders.push("P-Preferred-Identity: " + userAgent.configuration.uri.toString()); extraHeaders.push("Privacy: id"); } extraHeaders.push("Contact: " + contact); extraHeaders.push("Allow: " + ["ACK", "CANCEL", "INVITE", "MESSAGE", "BYE", "OPTIONS", "INFO", "NOTIFY", "REFER"].toString()); if (userAgent.configuration.sipExtension100rel === SIPExtension.Required) { extraHeaders.push("Require: 100rel"); } if (userAgent.configuration.sipExtensionReplaces === SIPExtension.Required) { extraHeaders.push("Require: replaces"); } inviterOptions.extraHeaders = extraHeaders; // Body const body = undefined; // Make initial outgoing request message this.outgoingRequestMessage = userAgent.userAgentCore.makeOutgoingRequestMessage(C.INVITE, targetURI, fromURI, toURI, messageOptions, extraHeaders, body); // Session parent properties this._contact = contact; this._referralInviterOptions = inviterOptions; this._renderbody = options.renderbody; this._rendertype = options.rendertype; // Modifiers and options for initial INVITE transaction if (options.sessionDescriptionHandlerModifiers) { this.sessionDescriptionHandlerModifiers = options.sessionDescriptionHandlerModifiers; } if (options.sessionDescriptionHandlerOptions) { this.sessionDescriptionHandlerOptions = options.sessionDescriptionHandlerOptions; } // Modifiers and options for re-INVITE transactions if (options.sessionDescriptionHandlerModifiersReInvite) { this.sessionDescriptionHandlerModifiersReInvite = options.sessionDescriptionHandlerModifiersReInvite; } if (options.sessionDescriptionHandlerOptionsReInvite) { this.sessionDescriptionHandlerOptionsReInvite = options.sessionDescriptionHandlerOptionsReInvite; } // Identifier this._id = this.outgoingRequestMessage.callId + this.fromTag; // Add to the user agent's session collection. this.userAgent._sessions[this._id] = this; } /** * Destructor. */ dispose() { // Only run through this once. It can and does get called multiple times // depending on the what the sessions state is when first called. // For example, if called when "establishing" it will be called again // at least once when the session transitions to "terminated". // Regardless, running through this more than once is pointless. if (this.disposed) { return Promise.resolve(); } this.disposed = true; // Dispose of early dialog media this.disposeEarlyMedia(); // If the final response for the initial INVITE not yet been received, cancel it switch (this.state) { case SessionState.Initial: return this.cancel().then(() => super.dispose()); case SessionState.Establishing: return this.cancel().then(() => super.dispose()); case SessionState.Established: return super.dispose(); case SessionState.Terminating: return super.dispose(); case SessionState.Terminated: return super.dispose(); default: throw new Error("Unknown state."); } } /** * Initial outgoing INVITE request message body. */ get body() { return this.outgoingRequestMessage.body; } /** * The identity of the local user. */ get localIdentity() { return this.outgoingRequestMessage.from; } /** * The identity of the remote user. */ get remoteIdentity() { return this.outgoingRequestMessage.to; } /** * Initial outgoing INVITE request message. */ get request() { return this.outgoingRequestMessage; } /** * Cancels the INVITE request. * * @remarks * Sends a CANCEL request. * Resolves once the response sent, otherwise rejects. * * After sending a CANCEL request the expectation is that a 487 final response * will be received for the INVITE. However a 200 final response to the INVITE * may nonetheless arrive (it's a race between the CANCEL reaching the UAS before * the UAS sends a 200) in which case an ACK & BYE will be sent. The net effect * is that this method will terminate the session regardless of the race. * @param options - Options bucket. */ cancel(options = {}) { this.logger.log("Inviter.cancel"); // validate state if (this.state !== SessionState.Initial && this.state !== SessionState.Establishing) { const error = new Error(`Invalid session state ${this.state}`); this.logger.error(error.message); return Promise.reject(error); } // flag canceled this.isCanceled = true; // transition state this.stateTransition(SessionState.Terminating); // helper function function getCancelReason(code, reason) { if ((code && code < 200) || code > 699) { throw new TypeError("Invalid statusCode: " + code); } else if (code) { const cause = code; const text = getReasonPhrase(code) || reason; return "SIP;cause=" + cause + ';text="' + text + '"'; } } if (this.outgoingInviteRequest) { // the CANCEL may not be respected by peer(s), so don't transition to terminated let cancelReason; if (options.statusCode && options.reasonPhrase) { cancelReason = getCancelReason(options.statusCode, options.reasonPhrase); } this.outgoingInviteRequest.cancel(cancelReason, options); } else { this.logger.warn("Canceled session before INVITE was sent"); this.stateTransition(SessionState.Terminated); } return Promise.resolve(); } /** * Sends the INVITE request. * * @remarks * TLDR... * 1) Only one offer/answer exchange permitted during initial INVITE. * 2) No "early media" if the initial offer is in an INVITE (default behavior). * 3) If "early media" and the initial offer is in an INVITE, no INVITE forking. * * 1) Only one offer/answer exchange permitted during initial INVITE. * * Our implementation replaces the following bullet point... * * o After having sent or received an answer to the first offer, the * UAC MAY generate subsequent offers in requests based on rules * specified for that method, but only if it has received answers * to any previous offers, and has not sent any offers to which it * hasn't gotten an answer. * https://tools.ietf.org/html/rfc3261#section-13.2.1 * * ...with... * * o After having sent or received an answer to the first offer, the * UAC MUST NOT generate subsequent offers in requests based on rules * specified for that method. * * ...which in combination with this bullet point... * * o 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 * * ...ensures that EXACTLY ONE offer/answer exchange will occur * during an initial out of dialog INVITE request made by our UAC. * * * 2) No "early media" if the initial offer is in an INVITE (default behavior). * * While our implementation adheres to the following bullet point... * * o 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 * * We have made the following implementation decision with regard to early media... * * o If the initial offer is in the INVITE, the answer from the * UAS back to the UAC will establish a media session only * only after the final 2xx response to that INVITE is received. * * The reason for this decision is rooted in a restriction currently * inherent in WebRTC. Specifically, while a SIP INVITE request with an * initial offer may fork resulting in more than one provisional answer, * there is currently no easy/good way to to "fork" an offer generated * by a peer connection. In particular, a WebRTC offer currently may only * be matched with one answer and we have no good way to know which * "provisional answer" is going to be the "final answer". So we have * decided to punt and not create any "early media" sessions in this case. * * The upshot is that if you want "early media", you must not put the * initial offer in the INVITE. Instead, force the UAS to provide the * initial offer by sending an INVITE without an offer. In the WebRTC * case this allows us to create a unique peer connection with a unique * answer for every provisional offer with "early media" on all of them. * * * 3) If "early media" and the initial offer is in an INVITE, no INVITE forking. * * The default behavior may be altered and "early media" utilized if the * initial offer is in the an INVITE by setting the `earlyMedia` options. * However in that case the INVITE request MUST NOT fork. This allows for * "early media" in environments where the forking behavior of the SIP * servers being utilized is configured to disallow forking. */ invite(options = {}) { this.logger.log("Inviter.invite"); // validate state if (this.state !== SessionState.Initial) { // re-invite return super.invite(options); } // Modifiers and options for initial INVITE transaction if (options.sessionDescriptionHandlerModifiers) { this.sessionDescriptionHandlerModifiers = options.sessionDescriptionHandlerModifiers; } if (options.sessionDescriptionHandlerOptions) { this.sessionDescriptionHandlerOptions = options.sessionDescriptionHandlerOptions; } // just send an INVITE with no sdp... if (options.withoutSdp || this.inviteWithoutSdp) { if (this._renderbody && this._rendertype) { this.outgoingRequestMessage.body = { contentType: this._rendertype, body: this._renderbody }; } // transition state this.stateTransition(SessionState.Establishing); return Promise.resolve(this.sendInvite(options)); } // get an offer and send it in an INVITE const offerOptions = { sessionDescriptionHandlerModifiers: this.sessionDescriptionHandlerModifiers, sessionDescriptionHandlerOptions: this.sessionDescriptionHandlerOptions }; return this.getOffer(offerOptions) .then((body) => { this.outgoingRequestMessage.body = { body: body.content, contentType: body.contentType }; // transition state this.stateTransition(SessionState.Establishing); return this.sendInvite(options); }) .catch((error) => { this.logger.log(error.message); // It's possible we are already terminated, // so don't throw trying to transition again. if (this.state !== SessionState.Terminated) { this.stateTransition(SessionState.Terminated); } throw error; }); } /** * 13.2.1 Creating the Initial INVITE * * Since the initial INVITE represents a request outside of a dialog, * its construction follows the procedures of Section 8.1.1. Additional * processing is required for the specific case of INVITE. * * An Allow header field (Section 20.5) SHOULD be present in the INVITE. * It indicates what methods can be invoked within a dialog, on the UA * sending the INVITE, for the duration of the dialog. For example, a * UA capable of receiving INFO requests within a dialog [34] SHOULD * include an Allow header field listing the INFO method. * * A Supported header field (Section 20.37) SHOULD be present in the * INVITE. It enumerates all the extensions understood by the UAC. * * An Accept (Section 20.1) header field MAY be present in the INVITE. * It indicates which Content-Types are acceptable to the UA, in both * the response received by it, and in any subsequent requests sent to * it within dialogs established by the INVITE. The Accept header field * is especially useful for indicating support of various session * description formats. * * The UAC MAY add an Expires header field (Section 20.19) to limit the * validity of the invitation. If the time indicated in the Expires * header field is reached and no final answer for the INVITE has been * received, the UAC core SHOULD generate a CANCEL request for the * INVITE, as per Section 9. * * A UAC MAY also find it useful to add, among others, Subject (Section * 20.36), Organization (Section 20.25) and User-Agent (Section 20.41) * header fields. They all contain information related to the INVITE. * * The UAC MAY choose to add a message body to the INVITE. Section * 8.1.1.10 deals with how to construct the header fields -- Content- * Type among others -- needed to describe the message body. * * https://tools.ietf.org/html/rfc3261#section-13.2.1 */ sendInvite(options = {}) { // There are special rules for message bodies that contain a session // description - their corresponding Content-Disposition is "session". // SIP uses an offer/answer model where one UA sends a session // description, called the offer, which contains a proposed description // of the session. The offer indicates the desired communications means // (audio, video, games), parameters of those means (such as codec // types) and addresses for receiving media from the answerer. The // other UA responds with another session description, called the // answer, which indicates which communications means are accepted, the // parameters that apply to those means, and addresses for receiving // media from the offerer. An offer/answer exchange is within the // context of a dialog, so that if a SIP INVITE results in multiple // dialogs, each is a separate offer/answer exchange. The offer/answer // model defines restrictions on when offers and answers can be made // (for example, you cannot make a new offer while one is in progress). // This results in restrictions on where the offers and answers can // appear in SIP messages. In this specification, offers and answers // can only appear in INVITE requests and responses, and ACK. The usage // of offers and answers is further restricted. For the initial INVITE // transaction, the rules are: // // o The initial offer MUST be in either an INVITE or, if not there, // in the first reliable non-failure message from the UAS back to // the UAC. In this specification, that is the final 2xx // response. // // o 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. // // o If the initial offer is in the first reliable non-failure // message from the UAS back to UAC, the answer MUST be in the // acknowledgement for that message (in this specification, ACK // for a 2xx response). // // o After having sent or received an answer to the first offer, the // UAC MAY generate subsequent offers in requests based on rules // specified for that method, but only if it has received answers // to any previous offers, and has not sent any offers to which it // hasn't gotten an answer. // // o 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 // 5 The Offer/Answer Model and PRACK // // RFC 3261 describes guidelines for the sets of messages in which // offers and answers [3] can appear. Based on those guidelines, this // extension provides additional opportunities for offer/answer // exchanges. // If the INVITE contained an offer, the UAS MAY generate an answer in a // reliable provisional response (assuming these are supported by the // UAC). That results in the establishment of the session before // completion of the call. Similarly, if a reliable provisional // response is the first reliable message sent back to the UAC, and the // INVITE did not contain an offer, one MUST appear in that reliable // provisional response. // If the UAC receives a reliable provisional response with an offer // (this would occur if the UAC sent an INVITE without an offer, in // which case the first reliable provisional response will contain the // offer), it MUST generate an answer in the PRACK. If the UAC receives // a reliable provisional response with an answer, it MAY generate an // additional offer in the PRACK. If the UAS receives a PRACK with an // offer, it MUST place the answer in the 2xx to the PRACK. // Once an answer has been sent or received, the UA SHOULD establish the // session based on the parameters of the offer and answer, even if the // original INVITE itself has not been responded to. // If the UAS had placed a session description in any reliable // provisional response that is unacknowledged when the INVITE is // accepted, the UAS MUST delay sending the 2xx until the provisional // response is acknowledged. Otherwise, the reliability of the 1xx // cannot be guaranteed, and reliability is needed for proper operation // of the offer/answer exchange. // All user agents that support this extension MUST support all // offer/answer exchanges that are possible based on the rules in // Section 13.2 of RFC 3261, based on the existence of INVITE and PRACK // as requests, and 2xx and reliable 1xx as non-failure reliable // responses. // // https://tools.ietf.org/html/rfc3262#section-5 //// // The Offer/Answer Model Implementation // // The offer/answer model is straight forward, but one MUST READ the specifications... // // 13.2.1 Creating the Initial INVITE (paragraph 8 in particular) // https://tools.ietf.org/html/rfc3261#section-13.2.1 // // 5 The Offer/Answer Model and PRACK // https://tools.ietf.org/html/rfc3262#section-5 // // Session Initiation Protocol (SIP) Usage of the Offer/Answer Model // https://tools.ietf.org/html/rfc6337 //// //// // TODO: The Offer/Answer Model Implementation // // Currently if `earlyMedia` is enabled and the INVITE request forks, // the session is terminated if the early dialog does not match the // confirmed dialog. This restriction make sense in a WebRTC environment, // but there are other environments where this restriction does not hold. // // So while we currently cannot make the offer in INVITE+forking+webrtc // case work, we propose doing the following... // // OPTION 1 // - add a `earlyMediaForking` option and // - require SDH.setDescription() to be callable multiple times. // // OPTION 2 // 1) modify SDH Factory to provide an initial offer without giving us the SDH, and then... // 2) stick that offer in the initial INVITE, and when 183 with initial answer is received... // 3) ask SDH Factory if it supports "earlyRemoteAnswer" // a) if true, ask SDH Factory to createSDH(localOffer).then((sdh) => sdh.setDescription(remoteAnswer) // b) if false, defer getting a SDH until 2xx response is received // // Our supplied WebRTC SDH will default to behavior 3b which works in forking environment (without) // early media if initial offer is in the INVITE). We will, however, provide an "inviteWillNotFork" // option which if set to "true" will have our supplied WebRTC SDH behave in the 3a manner. // That will result in // - early media working with initial offer in the INVITE, and... // - if the INVITE forks, the session terminating with an ERROR that reads like // "You set 'inviteWillNotFork' to true but the INVITE forked. You can't eat your cake, and have it too." // - furthermore, we accept that users will report that error to us as "bug" regardless // // So, SDH Factory is going to end up with a new interface along the lines of... // // interface SessionDescriptionHandlerFactory { // makeLocalOffer(): Promise<ContentTypeAndBody>; // makeSessionDescriptionHandler( // initialOffer: ContentTypeAndBody, offerType: "local" | "remote" // ): Promise<SessionDescriptionHandler>; // supportsEarlyRemoteAnswer: boolean; // supportsContentType(contentType: string): boolean; // getDescription(description: ContentTypeAndBody): Promise<ContentTypeAndBody> // setDescription(description: ContentTypeAndBody): Promise<void> // } //// // Send the INVITE request. this.outgoingInviteRequest = this.userAgent.userAgentCore.invite(this.outgoingRequestMessage, { onAccept: (inviteResponse) => { // Our transaction layer is "non-standard" in that it will only // pass us a 2xx response once per branch, so there is no need to // worry about dealing with 2xx retransmissions. However, we can // and do still get 2xx responses for multiple branches (when an // INVITE is forked) which may create multiple confirmed dialogs. // Herein we are acking and sending a bye to any confirmed dialogs // which arrive beyond the first one. This is the desired behavior // for most applications (but certainly not all). // If we already received a confirmed dialog, ack & bye this additional confirmed session. if (this.dialog) { this.logger.log("Additional confirmed dialog, sending ACK and BYE"); this.ackAndBye(inviteResponse); // We do NOT transition state in this case (this is an "extra" dialog) return; } // If the user requested cancellation, ack & bye this session. if (this.isCanceled) { this.logger.log("Canceled session accepted, sending ACK and BYE"); this.ackAndBye(inviteResponse); this.stateTransition(SessionState.Terminated); return; } this.notifyReferer(inviteResponse); this.onAccept(inviteResponse) .then(() => { this.disposeEarlyMedia(); }) .catch(() => { this.disposeEarlyMedia(); }) .then(() => { if (options.requestDelegate && options.requestDelegate.onAccept) { options.requestDelegate.onAccept(inviteResponse); } }); }, onProgress: (inviteResponse) => { // If the user requested cancellation, ignore response. if (this.isCanceled) { return; } this.notifyReferer(inviteResponse); this.onProgress(inviteResponse) .catch(() => { this.disposeEarlyMedia(); }) .then(() => { if (options.requestDelegate && options.requestDelegate.onProgress) { options.requestDelegate.onProgress(inviteResponse); } }); }, onRedirect: (inviteResponse) => { this.notifyReferer(inviteResponse); this.onRedirect(inviteResponse); if (options.requestDelegate && options.requestDelegate.onRedirect) { options.requestDelegate.onRedirect(inviteResponse); } }, onReject: (inviteResponse) => { this.notifyReferer(inviteResponse); this.onReject(inviteResponse); if (options.requestDelegate && options.requestDelegate.onReject) { options.requestDelegate.onReject(inviteResponse); } }, onTrying: (inviteResponse) => { this.notifyReferer(inviteResponse); this.onTrying(inviteResponse); if (options.requestDelegate && options.requestDelegate.onTrying) { options.requestDelegate.onTrying(inviteResponse); } } }); return this.outgoingInviteRequest; } disposeEarlyMedia() { this.earlyMediaSessionDescriptionHandlers.forEach((sessionDescriptionHandler) => { sessionDescriptionHandler.close(); }); this.earlyMediaSessionDescriptionHandlers.clear(); } notifyReferer(response) { if (!this._referred) { return; } if (!(this._referred instanceof Session)) { throw new Error("Referred session not instance of session"); } if (!this._referred.dialog) { return; } if (!response.message.statusCode) { throw new Error("Status code undefined."); } if (!response.message.reasonPhrase) { throw new Error("Reason phrase undefined."); } const statusCode = response.message.statusCode; const reasonPhrase = response.message.reasonPhrase; const body = `SIP/2.0 ${statusCode} ${reasonPhrase}`.trim(); const outgoingNotifyRequest = this._referred.dialog.notify(undefined, { extraHeaders: ["Event: refer", "Subscription-State: terminated"], body: { contentDisposition: "render", contentType: "message/sipfrag", content: body } }); // The implicit subscription created by a REFER is the same as a // subscription created with a SUBSCRIBE request. The agent issuing the // REFER can terminate this subscription prematurely by unsubscribing // using the mechanisms described in [2]. Terminating a subscription, // either by explicitly unsubscribing or rejecting NOTIFY, is not an // indication that the referenced request should be withdrawn or // abandoned. // https://tools.ietf.org/html/rfc3515#section-2.4.4 // FIXME: TODO: This should be done in a subscribe dialog to satisfy the above. // If the notify is rejected, stop sending NOTIFY requests. outgoingNotifyRequest.delegate = { onReject: () => { this._referred = undefined; } }; } /** * Handle final response to initial INVITE. * @param inviteResponse - 2xx response. */ onAccept(inviteResponse) { this.logger.log("Inviter.onAccept"); // validate state if (this.state !== SessionState.Establishing) { this.logger.error(`Accept received while in state ${this.state}, dropping response`); return Promise.reject(new Error(`Invalid session state ${this.state}`)); } const response = inviteResponse.message; const session = inviteResponse.session; // Ported behavior. if (response.hasHeader("P-Asserted-Identity")) { this._assertedIdentity = Grammar.nameAddrHeaderParse(response.getHeader("P-Asserted-Identity")); } // We have a confirmed dialog. session.delegate = { onAck: (ackRequest) => this.onAckRequest(ackRequest), onBye: (byeRequest) => this.onByeRequest(byeRequest), onInfo: (infoRequest) => this.onInfoRequest(infoRequest), onInvite: (inviteRequest) => this.onInviteRequest(inviteRequest), onMessage: (messageRequest) => this.onMessageRequest(messageRequest), onNotify: (notifyRequest) => this.onNotifyRequest(notifyRequest), onPrack: (prackRequest) => this.onPrackRequest(prackRequest), onRefer: (referRequest) => this.onReferRequest(referRequest) }; this._dialog = session; switch (session.signalingState) { case SignalingState.Initial: // INVITE without offer, so MUST have offer at this point, so invalid state. this.logger.error("Received 2xx response to INVITE without a session description"); this.ackAndBye(inviteResponse, 400, "Missing session description"); this.stateTransition(SessionState.Terminated); return Promise.reject(new Error("Bad Media Description")); case SignalingState.HaveLocalOffer: // INVITE with offer, so MUST have answer at this point, so invalid state. this.logger.error("Received 2xx response to INVITE without a session description"); this.ackAndBye(inviteResponse, 400, "Missing session description"); this.stateTransition(SessionState.Terminated); return Promise.reject(new Error("Bad Media Description")); case SignalingState.HaveRemoteOffer: { // INVITE without offer, received offer in 2xx, so MUST send answer in ACK. if (!this._dialog.offer) { throw new Error(`Session offer undefined in signaling state ${this._dialog.signalingState}.`); } const options = { sessionDescriptionHandlerModifiers: this.sessionDescriptionHandlerModifiers, sessionDescriptionHandlerOptions: this.sessionDescriptionHandlerOptions }; return this.setOfferAndGetAnswer(this._dialog.offer, options) .then((body) => { inviteResponse.ack({ body }); this.stateTransition(SessionState.Established); }) .catch((error) => { this.ackAndBye(inviteResponse, 488, "Invalid session description"); this.stateTransition(SessionState.Terminated); throw error; }); } case SignalingState.Stable: { // If INVITE without offer and we have already completed the initial exchange. if (this.earlyMediaSessionDescriptionHandlers.size > 0) { const sdh = this.earlyMediaSessionDescriptionHandlers.get(session.id); if (!sdh) { throw new Error("Session description handler undefined."); } this.setSessionDescriptionHandler(sdh); this.earlyMediaSessionDescriptionHandlers.delete(session.id); inviteResponse.ack(); this.stateTransition(SessionState.Established); return Promise.resolve(); } // If INVITE with offer and we used an "early" answer in a provisional response for media if (this.earlyMediaDialog) { // If early media dialog doesn't match confirmed dialog, we must unfortunately fail. // This limitation stems from how WebRTC currently implements its offer/answer model. // There are details elsewhere, but in short a WebRTC offer cannot be forked. if (this.earlyMediaDialog !== session) { if (this.earlyMedia) { const message = "You have set the 'earlyMedia' option to 'true' which requires that your INVITE requests " + "do not fork and yet this INVITE request did in fact fork. Consequentially and not surprisingly " + "the end point which accepted the INVITE (confirmed dialog) does not match the end point with " + "which early media has been setup (early dialog) and thus this session is unable to proceed. " + "In accordance with the SIP specifications, the SIP servers your end point is connected to " + "determine if an INVITE forks and the forking behavior of those servers cannot be controlled " + "by this library. If you wish to use early media with this library you must configure those " + "servers accordingly. Alternatively you may set the 'earlyMedia' to 'false' which will allow " + "this library to function with any INVITE requests which do fork."; this.logger.error(message); } const error = new Error("Early media dialog does not equal confirmed dialog, terminating session"); this.logger.error(error.message); this.ackAndBye(inviteResponse, 488, "Not Acceptable Here"); this.stateTransition(SessionState.Terminated); return Promise.reject(error); } // Otherwise we are good to go. inviteResponse.ack(); this.stateTransition(SessionState.Established); return Promise.resolve(); } // If INVITE with offer and we have been waiting till now to apply the answer. const answer = session.answer; if (!answer) { throw new Error("Answer is undefined."); } const options = { sessionDescriptionHandlerModifiers: this.sessionDescriptionHandlerModifiers, sessionDescriptionHandlerOptions: this.sessionDescriptionHandlerOptions }; return this.setAnswer(answer, options) .then(() => { // This session has completed an initial offer/answer exchange... let ackOptions; if (this._renderbody && this._rendertype) { ackOptions = { body: { contentDisposition: "render", contentType: this._rendertype, content: this._renderbody } }; } inviteResponse.ack(ackOptions); this.stateTransition(SessionState.Established); }) .catch((error) => { this.logger.error(error.message); this.ackAndBye(inviteResponse, 488, "Not Acceptable Here"); this.stateTransition(SessionState.Terminated); throw error; }); } case SignalingState.Closed: // Dialog has terminated. return Promise.reject(new Error("Terminated.")); default: throw new Error("Unknown session signaling state."); } } /** * Handle provisional response to initial INVITE. * @param inviteResponse - 1xx response. */ onProgress(inviteResponse) { var _a; this.logger.log("Inviter.onProgress"); // validate state if (this.state !== SessionState.Establishing) { this.logger.error(`Progress received while in state ${this.state}, dropping response`); return Promise.reject(new Error(`Invalid session state ${this.state}`)); } if (!this.outgoingInviteRequest) { throw new Error("Outgoing INVITE request undefined."); } const response = inviteResponse.message; const session = inviteResponse.session; // Ported - Set assertedIdentity. if (response.hasHeader("P-Asserted-Identity")) { this._assertedIdentity = Grammar.nameAddrHeaderParse(response.getHeader("P-Asserted-Identity")); } // 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 = response.getHeader("require"); const rseqHeader = response.getHeader("rseq"); const rseq = requireHeader && requireHeader.includes("100rel") && rseqHeader ? Number(rseqHeader) : undefined; const responseReliable = !!rseq; const extraHeaders = []; if (responseReliable) { extraHeaders.push("RAck: " + response.getHeader("rseq") + " " + response.getHeader("cseq")); } switch (session.signalingState) { case SignalingState.Initial: // INVITE without offer and session still has no offer (and no answer). if (responseReliable) { // Similarly, if a reliable provisional // response is the first reliable message sent back to the UAC, and the // INVITE did not contain an offer, one MUST appear in that reliable // provisional response. // https://tools.ietf.org/html/rfc3262#section-5 this.logger.warn("First reliable provisional response received MUST contain an offer when INVITE does not contain an offer."); // FIXME: Known popular UA's currently end up here... inviteResponse.prack({ extraHeaders }); } return Promise.resolve(); case SignalingState.HaveLocalOffer: // INVITE with offer and session only has that initial local offer. if (responseReliable) { inviteResponse.prack({ extraHeaders }); } return Promise.resolve(); case SignalingState.HaveRemoteOffer: if (!responseReliable) { // The initial offer MUST be in either an INVITE or, if not there, // in the first reliable non-failure message from the UAS back to // the UAC. // https://tools.ietf.org/html/rfc3261#section-13.2.1 // According to Section 13.2.1 of [RFC3261], 'The first reliable // non-failure message' must have an offer if there is no offer in the // INVITE request. This means that the User Agent (UA) that receives // the INVITE request without an offer must include an offer in the // first reliable response with 100rel extension. If no reliable // provisional response has been sent, the User Agent Server (UAS) must // include an offer when sending 2xx response. // https://tools.ietf.org/html/rfc6337#section-2.2 this.logger.warn("Non-reliable provisional response MUST NOT contain an initial offer, discarding response."); return Promise.resolve(); } { // If the initial offer is in the first reliable non-failure // message from the UAS back to UAC, the answer MUST be in the // acknowledgement for that message const sdh = this.sessionDescriptionHandlerFactory(this, this.userAgent.configuration.sessionDescriptionHandlerFactoryOptions || {}); if ((_a = this.delegate) === null || _a === void 0 ? void 0 : _a.onSessionDescriptionHandler) { this.delegate.onSessionDescriptionHandler(sdh, true); } this.earlyMediaSessionDescriptionHandlers.set(session.id, sdh); return sdh .setDescription(response.body, this.sessionDescriptionHandlerOptions, this.sessionDescriptionHandlerModifiers) .then(() => sdh.getDescription(this.sessionDescriptionHandlerOptions, this.sessionDescriptionHandlerModifiers)) .then((description) => { const body = { contentDisposition: "session", contentType: description.contentType, content: description.body }; inviteResponse.prack({ extraHeaders, body }); }) .catch((error) => { this.stateTransition(SessionState.Terminated); throw error; }); } case SignalingState.Stable: // This session has completed an initial offer/answer exchange, so... // - INVITE with SDP and this provisional response MAY be reliable // - INVITE without SDP and this provisional response MAY be reliable if (responseReliable) { inviteResponse.prack({ extraHeaders }); } if (this.earlyMedia && !this.earlyMediaDialog) { this.earlyMediaDialog = session; const answer = session.answer; if (!answer) { throw new Error("Answer is undefined."); } const options = { sessionDescriptionHandlerModifiers: this.sessionDescriptionHandlerModifiers, sessionDescriptionHandlerOptions: this.sessionDescriptionHandlerOptions }; return this.setAnswer(answer, options).catch((error) => { this.stateTransition(SessionState.Terminated); throw error; }); } return Promise.resolve(); case SignalingState.Closed: // Dialog has terminated. return Promise.reject(new Error("Terminated.")); default: throw new Error("Unknown session signaling state."); } } /** * Handle final response to initial INVITE. * @param inviteResponse - 3xx response. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars onRedirect(inviteResponse) { this.logger.log("Inviter.onRedirect"); // validate state if (this.state !== SessionState.Establishing && this.state !== SessionState.Terminating) { this.logger.error(`Redirect received while in st