import { TransportError } from "../exceptions"; import { IncomingRequestMessage } from "../messages"; import { Timers } from "../timers"; import { Transport } from "../transport"; import { ServerTransaction } from "./server-transaction"; import { TransactionState } from "./transaction-state"; import { ServerTransactionUser } from "./transaction-user"; /** * Non-INVITE Server Transaction. * @remarks * https://tools.ietf.org/html/rfc3261#section-17.2.2 * @public */ export class NonInviteServerTransaction extends ServerTransaction { private lastResponse: string | undefined; private J: any | undefined; /** * Constructor. * After construction the transaction will be in the "trying": 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.2 * @param request - Incoming Non-INVITE request from the transport. * @param transport - The transport. * @param user - The transaction user. */ constructor(request: IncomingRequestMessage, transport: Transport, user: ServerTransactionUser) { super( request, transport, user, TransactionState.Trying, "sip.transaction.nist" ); } /** * Destructor. */ public dispose(): void { if (this.J) { clearTimeout(this.J); this.J = undefined; } super.dispose(); } /** Transaction kind. Deprecated. */ get kind(): string { return "nist"; } /** * Receive requests from transport matching this transaction. * @param request - Request matching this transaction. */ public receiveRequest(request: IncomingRequestMessage): void { switch (this.state) { case TransactionState.Trying: // Once in the "Trying" state, any further request retransmissions are discarded. // https://tools.ietf.org/html/rfc3261#section-17.2.2 break; case TransactionState.Proceeding: // If a retransmission of the request is received while in the "Proceeding" state, // the most recently sent provisional response MUST be passed to the transport layer for retransmission. // https://tools.ietf.org/html/rfc3261#section-17.2.2 if (!this.lastResponse) { throw new Error("Last response undefined."); } this.send(this.lastResponse).catch((error: TransportError) => { this.logTransportError(error, "Failed to send retransmission of provisional response."); }); break; case TransactionState.Completed: // While in the "Completed" state, the server transaction MUST pass the final response to the transport // layer for retransmission whenever a retransmission of the request is received. Any other final responses // passed by the TU to the server transaction MUST be discarded while in the "Completed" state. // https://tools.ietf.org/html/rfc3261#section-17.2.2 if (!this.lastResponse) { throw new Error("Last response undefined."); } this.send(this.lastResponse).catch((error: TransportError) => { this.logTransportError(error, "Failed to send retransmission of final response."); }); break; case TransactionState.Terminated: break; default: throw new Error(`Invalid state ${this.state}`); } } /** * Receive responses from TU for this transaction. * @param statusCode - Status code of repsonse. 101-199 not allowed per RFC 4320. * @param response - Response to send. */ public receiveResponse(statusCode: number, response: string): void { if (statusCode < 100 || statusCode > 699) { throw new Error(`Invalid status code ${statusCode}`); } // An SIP element MUST NOT send any provisional response with a // Status-Code other than 100 to a non-INVITE request. // An SIP element MUST NOT respond to a non-INVITE request with a // Status-Code of 100 over any unreliable transport, such as UDP, // before the amount of time it takes a client transaction's Timer E to be reset to T2. // An SIP element MAY respond to a non-INVITE request with a // Status-Code of 100 over a reliable transport at any time. // https://tools.ietf.org/html/rfc4320#section-4.1 if (statusCode > 100 && statusCode <= 199) { throw new Error("Provisional response other than 100 not allowed."); } switch (this.state) { case TransactionState.Trying: // While in the "Trying" state, if the TU passes a provisional response // to the server transaction, the server transaction MUST enter the "Proceeding" state. // The response MUST be passed to the transport layer for transmission. // https://tools.ietf.org/html/rfc3261#section-17.2.2 this.lastResponse = response; if (statusCode >= 100 && statusCode < 200) { this.stateTransition(TransactionState.Proceeding); this.send(response).catch((error: TransportError) => { this.logTransportError(error, "Failed to send provisional response."); }); return; } if (statusCode >= 200 && statusCode <= 699) { this.stateTransition(TransactionState.Completed); this.send(response).catch((error: TransportError) => { this.logTransportError(error, "Failed to send final response."); }); return; } break; case TransactionState.Proceeding: // Any further provisional responses that are received from the TU while // in the "Proceeding" state MUST be passed to the transport layer for transmission. // If the TU passes a final response (status codes 200-699) to the server while in // the "Proceeding" state, the transaction MUST enter the "Completed" state, and // the response MUST be passed to the transport layer for transmission. // https://tools.ietf.org/html/rfc3261#section-17.2.2 this.lastResponse = response; if (statusCode >= 200 && statusCode <= 699) { this.stateTransition(TransactionState.Completed); this.send(response).catch((error: TransportError) => { this.logTransportError(error, "Failed to send final response."); }); return; } break; case TransactionState.Completed: // Any other final responses passed by the TU to the server // transaction MUST be discarded while in the "Completed" state. // https://tools.ietf.org/html/rfc3261#section-17.2.2 return; case TransactionState.Terminated: break; default: throw new Error(`Invalid state ${this.state}`); } const message = `Non-INVITE server transaction received unexpected ${statusCode} response from TU while in state ${this.state}.`; this.logger.error(message); throw new Error(message); } /** * 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 SHOULD transition to the terminated state. * https://tools.ietf.org/html/rfc3261#section-17.2.4 */ protected onTransportError(error: Error): void { if (this.user.onTransportError) { this.user.onTransportError(error); } this.stateTransition(TransactionState.Terminated, true); } /** For logging. */ protected typeToString(): string { return "non-INVITE server transaction"; } private stateTransition(newState: TransactionState, dueToTransportError = false): void { // Assert valid state transitions. const invalidStateTransition = () => { throw new Error(`Invalid state transition from ${this.state} to ${newState}`); }; switch (newState) { case TransactionState.Trying: invalidStateTransition(); break; case TransactionState.Proceeding: if (this.state !== TransactionState.Trying) { invalidStateTransition(); } break; case TransactionState.Completed: if (this.state !== TransactionState.Trying && this.state !== TransactionState.Proceeding) { invalidStateTransition(); } break; case TransactionState.Terminated: if (this.state !== TransactionState.Proceeding && this.state !== TransactionState.Completed) { if (!dueToTransportError) { invalidStateTransition(); } } break; default: invalidStateTransition(); } // When the server transaction enters the "Completed" state, it MUST set Timer J to fire // in 64*T1 seconds for unreliable transports, and zero seconds for reliable transports. // https://tools.ietf.org/html/rfc3261#section-17.2.2 if (newState === TransactionState.Completed) { this.J = setTimeout(() => this.timer_J(), Timers.TIMER_J); } // The server transaction MUST be destroyed the instant it enters the "Terminated" state. // https://tools.ietf.org/html/rfc3261#section-17.2.2 if (newState === TransactionState.Terminated) { this.dispose(); } this.setState(newState); } /** * The server transaction remains in this state until Timer J fires, * at which point it MUST transition to the "Terminated" state. * https://tools.ietf.org/html/rfc3261#section-17.2.2 */ private timer_J(): void { this.logger.debug(`Timer J expired for NON-INVITE server transaction ${this.id}.`); if (this.state === TransactionState.Completed) { this.stateTransition(TransactionState.Terminated); } } }