UNPKG

sip.js

Version:

A SIP library for JavaScript

377 lines (376 loc) 18.4 kB
import { C } from "../messages/methods/constants.js"; import { Timers } from "../timers.js"; import { ServerTransaction } from "./server-transaction.js"; import { TransactionState } from "./transaction-state.js"; /** * INVITE Server Transaction. * @remarks * https://tools.ietf.org/html/rfc3261#section-17.2.1 * @public */ export class InviteServerTransaction extends ServerTransaction { /** * Constructor. * Upon construction, a "100 Trying" reply will be immediately sent. * After construction the transaction will be in the "proceeding" state and the transaction * `id` will equal the branch parameter set in the Via header of the incoming request. * https://tools.ietf.org/html/rfc3261#section-17.2.1 * @param request - Incoming INVITE request from the transport. * @param transport - The transport. * @param user - The transaction user. */ constructor(request, transport, user) { super(request, transport, user, TransactionState.Proceeding, "sip.transaction.ist"); } /** * Destructor. */ dispose() { this.stopProgressExtensionTimer(); if (this.H) { clearTimeout(this.H); this.H = undefined; } if (this.I) { clearTimeout(this.I); this.I = undefined; } if (this.L) { clearTimeout(this.L); this.L = undefined; } super.dispose(); } /** Transaction kind. Deprecated. */ get kind() { return "ist"; } /** * Receive requests from transport matching this transaction. * @param request - Request matching this transaction. */ receiveRequest(request) { switch (this.state) { case TransactionState.Proceeding: // If a request retransmission is received while in the "Proceeding" state, the most // recent provisional response that was received from the TU MUST be passed to the // transport layer for retransmission. // https://tools.ietf.org/html/rfc3261#section-17.2.1 if (request.method === C.INVITE) { if (this.lastProvisionalResponse) { this.send(this.lastProvisionalResponse).catch((error) => { this.logTransportError(error, "Failed to send retransmission of provisional response."); }); } return; } break; case TransactionState.Accepted: // While in the "Accepted" state, any retransmissions of the INVITE // received will match this transaction state machine and will be // absorbed by the machine without changing its state. These // retransmissions are not passed onto the TU. // https://tools.ietf.org/html/rfc6026#section-7.1 if (request.method === C.INVITE) { return; } break; case TransactionState.Completed: // Furthermore, while in the "Completed" state, if a request retransmission is // received, the server SHOULD pass the response to the transport for retransmission. // https://tools.ietf.org/html/rfc3261#section-17.2.1 if (request.method === C.INVITE) { if (!this.lastFinalResponse) { throw new Error("Last final response undefined."); } this.send(this.lastFinalResponse).catch((error) => { this.logTransportError(error, "Failed to send retransmission of final response."); }); return; } // If an ACK is received while the server transaction is in the "Completed" state, // the server transaction MUST transition to the "Confirmed" state. // https://tools.ietf.org/html/rfc3261#section-17.2.1 if (request.method === C.ACK) { this.stateTransition(TransactionState.Confirmed); return; } break; case TransactionState.Confirmed: // The purpose of the "Confirmed" state is to absorb any additional ACK messages that arrive, // triggered from retransmissions of the final response. // https://tools.ietf.org/html/rfc3261#section-17.2.1 if (request.method === C.INVITE || request.method === C.ACK) { return; } break; case TransactionState.Terminated: // For good measure absorb any additional messages that arrive (should not happen). if (request.method === C.INVITE || request.method === C.ACK) { return; } break; default: throw new Error(`Invalid state ${this.state}`); } const message = `INVITE server transaction received unexpected ${request.method} request while in state ${this.state}.`; this.logger.warn(message); return; } /** * Receive responses from TU for this transaction. * @param statusCode - Status code of response. * @param response - Response. */ receiveResponse(statusCode, response) { if (statusCode < 100 || statusCode > 699) { throw new Error(`Invalid status code ${statusCode}`); } switch (this.state) { case TransactionState.Proceeding: // The TU passes any number of provisional responses to the server // transaction. So long as the server transaction is in the // "Proceeding" state, each of these MUST be passed to the transport // layer for transmission. They are not sent reliably by the // transaction layer (they are not retransmitted by it) and do not cause // a change in the state of the server transaction. // https://tools.ietf.org/html/rfc3261#section-17.2.1 if (statusCode >= 100 && statusCode <= 199) { this.lastProvisionalResponse = response; // Start the progress extension timer only for a non-100 provisional response. if (statusCode > 100) { this.startProgressExtensionTimer(); // FIXME: remove } this.send(response).catch((error) => { this.logTransportError(error, "Failed to send 1xx response."); }); return; } // If, while in the "Proceeding" state, the TU passes a 2xx response // to the server transaction, the server transaction MUST pass this // response to the transport layer for transmission. It is not // retransmitted by the server transaction; retransmissions of 2xx // responses are handled by the TU. The server transaction MUST then // transition to the "Accepted" state. // https://tools.ietf.org/html/rfc6026#section-8.5 if (statusCode >= 200 && statusCode <= 299) { this.lastFinalResponse = response; this.stateTransition(TransactionState.Accepted); this.send(response).catch((error) => { this.logTransportError(error, "Failed to send 2xx response."); }); return; } // While in the "Proceeding" state, if the TU passes a response with // status code from 300 to 699 to the server transaction, the response // MUST be passed to the transport layer for transmission, and the state // machine MUST enter the "Completed" state. // https://tools.ietf.org/html/rfc3261#section-17.2.1 if (statusCode >= 300 && statusCode <= 699) { this.lastFinalResponse = response; this.stateTransition(TransactionState.Completed); this.send(response).catch((error) => { this.logTransportError(error, "Failed to send non-2xx final response."); }); return; } break; case TransactionState.Accepted: // While in the "Accepted" state, if the TU passes a 2xx response, // the server transaction MUST pass the response to the transport layer for transmission. // https://tools.ietf.org/html/rfc6026#section-8.7 if (statusCode >= 200 && statusCode <= 299) { this.send(response).catch((error) => { this.logTransportError(error, "Failed to send 2xx response."); }); return; } break; case TransactionState.Completed: break; case TransactionState.Confirmed: break; case TransactionState.Terminated: break; default: throw new Error(`Invalid state ${this.state}`); } const message = `INVITE server transaction received unexpected ${statusCode} response from TU while in state ${this.state}.`; this.logger.error(message); throw new Error(message); } /** * Retransmit the last 2xx response. This is a noop if not in the "accepted" state. */ retransmitAcceptedResponse() { if (this.state === TransactionState.Accepted && this.lastFinalResponse) { this.send(this.lastFinalResponse).catch((error) => { this.logTransportError(error, "Failed to send 2xx response."); }); } } /** * First, the procedures in [4] are followed, which attempt to deliver the response to a backup. * If those should all fail, based on the definition of failure in [4], the server transaction SHOULD * inform the TU that a failure has occurred, and MUST remain in the current state. * https://tools.ietf.org/html/rfc6026#section-8.8 */ onTransportError(error) { if (this.user.onTransportError) { this.user.onTransportError(error); } } /** For logging. */ typeToString() { return "INVITE server transaction"; } /** * Execute a state transition. * @param newState - New state. */ stateTransition(newState) { // Assert valid state transitions. const invalidStateTransition = () => { throw new Error(`Invalid state transition from ${this.state} to ${newState}`); }; switch (newState) { case TransactionState.Proceeding: invalidStateTransition(); break; case TransactionState.Accepted: case TransactionState.Completed: if (this.state !== TransactionState.Proceeding) { invalidStateTransition(); } break; case TransactionState.Confirmed: if (this.state !== TransactionState.Completed) { invalidStateTransition(); } break; case TransactionState.Terminated: if (this.state !== TransactionState.Accepted && this.state !== TransactionState.Completed && this.state !== TransactionState.Confirmed) { invalidStateTransition(); } break; default: invalidStateTransition(); } // On any state transition, stop resending provisional responses this.stopProgressExtensionTimer(); // The purpose of the "Accepted" state is to absorb retransmissions of an accepted INVITE request. // Any such retransmissions are absorbed entirely within the server transaction. // They are not passed up to the TU since any downstream UAS cores that accepted the request have // taken responsibility for reliability and will already retransmit their 2xx responses if necessary. // https://tools.ietf.org/html/rfc6026#section-8.7 if (newState === TransactionState.Accepted) { this.L = setTimeout(() => this.timerL(), Timers.TIMER_L); } // When the "Completed" state is entered, timer H MUST be set to fire in 64*T1 seconds for all transports. // Timer H determines when the server transaction abandons retransmitting the response. // If an ACK is received while the server transaction is in the "Completed" state, // the server transaction MUST transition to the "Confirmed" state. // https://tools.ietf.org/html/rfc3261#section-17.2.1 if (newState === TransactionState.Completed) { // FIXME: Missing timer G for unreliable transports. this.H = setTimeout(() => this.timerH(), Timers.TIMER_H); } // The purpose of the "Confirmed" state is to absorb any additional ACK messages that arrive, // triggered from retransmissions of the final response. When this state is entered, timer I // is set to fire in T4 seconds for unreliable transports, and zero seconds for reliable // transports. Once timer I fires, the server MUST transition to the "Terminated" state. // https://tools.ietf.org/html/rfc3261#section-17.2.1 if (newState === TransactionState.Confirmed) { // FIXME: This timer is not getting set correctly for unreliable transports. this.I = setTimeout(() => this.timerI(), Timers.TIMER_I); } // Once the transaction is in the "Terminated" state, it MUST be destroyed immediately. // https://tools.ietf.org/html/rfc6026#section-8.7 if (newState === TransactionState.Terminated) { this.dispose(); } // Update state. this.setState(newState); } /** * FIXME: UAS Provisional Retransmission Timer. See RFC 3261 Section 13.3.1.1 * This is in the wrong place. This is not a transaction level thing. It's a UAS level thing. */ startProgressExtensionTimer() { // Start the progress extension timer only for the first non-100 provisional response. if (this.progressExtensionTimer === undefined) { this.progressExtensionTimer = setInterval(() => { this.logger.debug(`Progress extension timer expired for INVITE server transaction ${this.id}.`); if (!this.lastProvisionalResponse) { throw new Error("Last provisional response undefined."); } this.send(this.lastProvisionalResponse).catch((error) => { this.logTransportError(error, "Failed to send retransmission of provisional response."); }); }, Timers.PROVISIONAL_RESPONSE_INTERVAL); } } /** * FIXME: UAS Provisional Retransmission Timer id. See RFC 3261 Section 13.3.1.1 * This is in the wrong place. This is not a transaction level thing. It's a UAS level thing. */ stopProgressExtensionTimer() { if (this.progressExtensionTimer !== undefined) { clearInterval(this.progressExtensionTimer); this.progressExtensionTimer = undefined; } } /** * While in the "Proceeding" state, if the TU passes a response with status code * from 300 to 699 to the server transaction, the response MUST be passed to the * transport layer for transmission, and the state machine MUST enter the "Completed" state. * For unreliable transports, timer G is set to fire in T1 seconds, and is not set to fire for * reliable transports. If timer G fires, the response is passed to the transport layer once * more for retransmission, and timer G is set to fire in MIN(2*T1, T2) seconds. From then on, * when timer G fires, the response is passed to the transport again for transmission, and * timer G is reset with a value that doubles, unless that value exceeds T2, in which case * it is reset with the value of T2. * https://tools.ietf.org/html/rfc3261#section-17.2.1 */ timerG() { // TODO } /** * If timer H fires while in the "Completed" state, it implies that the ACK was never received. * In this case, the server transaction MUST transition to the "Terminated" state, and MUST * indicate to the TU that a transaction failure has occurred. * https://tools.ietf.org/html/rfc3261#section-17.2.1 */ timerH() { this.logger.debug(`Timer H expired for INVITE server transaction ${this.id}.`); if (this.state === TransactionState.Completed) { this.logger.warn("ACK to negative final response was never received, terminating transaction."); this.stateTransition(TransactionState.Terminated); } } /** * Once timer I fires, the server MUST transition to the "Terminated" state. * https://tools.ietf.org/html/rfc3261#section-17.2.1 */ timerI() { this.logger.debug(`Timer I expired for INVITE server transaction ${this.id}.`); this.stateTransition(TransactionState.Terminated); } /** * When Timer L fires and the state machine is in the "Accepted" state, the machine MUST * transition to the "Terminated" state. Once the transaction is in the "Terminated" state, * it MUST be destroyed immediately. Timer L reflects the amount of time the server * transaction could receive 2xx responses for retransmission from the * TU while it is waiting to receive an ACK. * https://tools.ietf.org/html/rfc6026#section-7.1 * https://tools.ietf.org/html/rfc6026#section-8.7 */ timerL() { this.logger.debug(`Timer L expired for INVITE server transaction ${this.id}.`); if (this.state === TransactionState.Accepted) { this.stateTransition(TransactionState.Terminated); } } }