UNPKG

sip.js

Version:

A SIP library for JavaScript

312 lines (311 loc) 14.7 kB
import { C } from "../messages/methods/constants.js"; import { IncomingResponseMessage } from "../messages/incoming-response-message.js"; import { NonInviteClientTransaction } from "../transactions/non-invite-client-transaction.js"; import { TransactionState } from "../transactions/transaction-state.js"; /** * User Agent Client (UAC). * @remarks * A user agent client is a logical entity * that creates a new request, and then uses the client * transaction state machinery to send it. The role of UAC lasts * only for the duration of that transaction. In other words, if * a piece of software initiates a request, it acts as a UAC for * the duration of that transaction. If it receives a request * later, it assumes the role of a user agent server for the * processing of that transaction. * https://tools.ietf.org/html/rfc3261#section-6 * @public */ export class UserAgentClient { constructor(transactionConstructor, core, message, delegate) { this.transactionConstructor = transactionConstructor; this.core = core; this.message = message; this.delegate = delegate; this.challenged = false; this.stale = false; this.logger = this.loggerFactory.getLogger("sip.user-agent-client"); this.init(); } dispose() { this.transaction.dispose(); } get loggerFactory() { return this.core.loggerFactory; } /** The transaction associated with this request. */ get transaction() { if (!this._transaction) { throw new Error("Transaction undefined."); } return this._transaction; } /** * Since requests other than INVITE are responded to immediately, sending a * CANCEL for a non-INVITE request would always create a race condition. * A CANCEL request SHOULD NOT be sent to cancel a request other than INVITE. * https://tools.ietf.org/html/rfc3261#section-9.1 * @param options - Cancel options bucket. */ cancel(reason, options = {}) { if (!this.transaction) { throw new Error("Transaction undefined."); } if (!this.message.to) { throw new Error("To undefined."); } if (!this.message.from) { throw new Error("From undefined."); } // The following procedures are used to construct a CANCEL request. The // Request-URI, Call-ID, To, the numeric part of CSeq, and From header // fields in the CANCEL request MUST be identical to those in the // request being cancelled, including tags. A CANCEL constructed by a // client MUST have only a single Via header field value matching the // top Via value in the request being cancelled. Using the same values // for these header fields allows the CANCEL to be matched with the // request it cancels (Section 9.2 indicates how such matching occurs). // However, the method part of the CSeq header field MUST have a value // of CANCEL. This allows it to be identified and processed as a // transaction in its own right (See Section 17). // https://tools.ietf.org/html/rfc3261#section-9.1 const message = this.core.makeOutgoingRequestMessage(C.CANCEL, this.message.ruri, this.message.from.uri, this.message.to.uri, { toTag: this.message.toTag, fromTag: this.message.fromTag, callId: this.message.callId, cseq: this.message.cseq }, options.extraHeaders); // TODO: Revisit this. // The CANCEL needs to use the same branch parameter so that // it matches the INVITE transaction, but this is a hacky way to do this. // Or at the very least not well documented. If the the branch parameter // is set on the outgoing request, the transaction will use it. // Otherwise the transaction will make a new one. message.branch = this.message.branch; if (this.message.headers.Route) { message.headers.Route = this.message.headers.Route; } if (reason) { message.setHeader("Reason", reason); } // If no provisional response has been received, the CANCEL request MUST // NOT be sent; rather, the client MUST wait for the arrival of a // provisional response before sending the request. If the original // request has generated a final response, the CANCEL SHOULD NOT be // sent, as it is an effective no-op, since CANCEL has no effect on // requests that have already generated a final response. // https://tools.ietf.org/html/rfc3261#section-9.1 if (this.transaction.state === TransactionState.Proceeding) { new UserAgentClient(NonInviteClientTransaction, this.core, message); } else { this.transaction.addStateChangeListener(() => { if (this.transaction && this.transaction.state === TransactionState.Proceeding) { new UserAgentClient(NonInviteClientTransaction, this.core, message); } }, { once: true }); } return message; } /** * If a 401 (Unauthorized) or 407 (Proxy Authentication Required) * response is received, the UAC SHOULD follow the authorization * procedures of Section 22.2 and Section 22.3 to retry the request with * credentials. * https://tools.ietf.org/html/rfc3261#section-8.1.3.5 * 22 Usage of HTTP Authentication * https://tools.ietf.org/html/rfc3261#section-22 * 22.1 Framework * https://tools.ietf.org/html/rfc3261#section-22.1 * 22.2 User-to-User Authentication * https://tools.ietf.org/html/rfc3261#section-22.2 * 22.3 Proxy-to-User Authentication * https://tools.ietf.org/html/rfc3261#section-22.3 * * FIXME: This "guard for and retry the request with credentials" * implementation is not complete and at best minimally passable. * @param response - The incoming response to guard. * @param dialog - If defined, the dialog within which the response was received. * @returns True if the program execution is to continue in the branch in question. * Otherwise the request is retried with credentials and current request processing must stop. */ authenticationGuard(message, dialog) { const statusCode = message.statusCode; if (!statusCode) { throw new Error("Response status code undefined."); } // If a 401 (Unauthorized) or 407 (Proxy Authentication Required) // response is received, the UAC SHOULD follow the authorization // procedures of Section 22.2 and Section 22.3 to retry the request with // credentials. // https://tools.ietf.org/html/rfc3261#section-8.1.3.5 if (statusCode !== 401 && statusCode !== 407) { return true; } // Get and parse the appropriate WWW-Authenticate or Proxy-Authenticate header. // eslint-disable-next-line @typescript-eslint/no-explicit-any let challenge; let authorizationHeaderName; if (statusCode === 401) { challenge = message.parseHeader("www-authenticate"); authorizationHeaderName = "authorization"; } else { challenge = message.parseHeader("proxy-authenticate"); authorizationHeaderName = "proxy-authorization"; } // Verify it seems a valid challenge. if (!challenge) { this.logger.warn(statusCode + " with wrong or missing challenge, cannot authenticate"); return true; } // Avoid infinite authentications. if (this.challenged && (this.stale || challenge.stale !== true)) { this.logger.warn(statusCode + " apparently in authentication loop, cannot authenticate"); return true; } // Get credentials. if (!this.credentials) { this.credentials = this.core.configuration.authenticationFactory(); if (!this.credentials) { this.logger.warn("Unable to obtain credentials, cannot authenticate"); return true; } } // Verify that the challenge is really valid. if (!this.credentials.authenticate(this.message, challenge)) { return true; } this.challenged = true; if (challenge.stale) { this.stale = true; } // If response to out of dialog request, assume incrementing the CSeq will suffice. let cseq = (this.message.cseq += 1); // If response to in dialog request, get a valid next CSeq number. if (dialog && dialog.localSequenceNumber) { dialog.incrementLocalSequenceNumber(); cseq = this.message.cseq = dialog.localSequenceNumber; } this.message.setHeader("cseq", cseq + " " + this.message.method); this.message.setHeader(authorizationHeaderName, this.credentials.toString()); // Calling init (again) will swap out our existing client transaction with a new one. // FIXME: HACK: An assumption is being made here that there is nothing that needs to // be cleaned up beyond the client transaction which is being replaced. For example, // it is assumed that no early dialogs have been created. this.init(); return false; } /** * 8.1.3.1 Transaction Layer Errors * In some cases, the response returned by the transaction layer will * not be a SIP message, but rather a transaction layer error. When a * timeout error is received from the transaction layer, it MUST be * treated as if a 408 (Request Timeout) status code has been received. * If a fatal transport error is reported by the transport layer * (generally, due to fatal ICMP errors in UDP or connection failures in * TCP), the condition MUST be treated as a 503 (Service Unavailable) * status code. * https://tools.ietf.org/html/rfc3261#section-8.1.3.1 */ onRequestTimeout() { this.logger.warn("User agent client request timed out. Generating internal 408 Request Timeout."); const message = new IncomingResponseMessage(); message.statusCode = 408; message.reasonPhrase = "Request Timeout"; this.receiveResponse(message); return; } /** * 8.1.3.1 Transaction Layer Errors * In some cases, the response returned by the transaction layer will * not be a SIP message, but rather a transaction layer error. When a * timeout error is received from the transaction layer, it MUST be * treated as if a 408 (Request Timeout) status code has been received. * If a fatal transport error is reported by the transport layer * (generally, due to fatal ICMP errors in UDP or connection failures in * TCP), the condition MUST be treated as a 503 (Service Unavailable) * status code. * https://tools.ietf.org/html/rfc3261#section-8.1.3.1 * @param error - Transport error */ onTransportError(error) { this.logger.error(error.message); this.logger.error("User agent client request transport error. Generating internal 503 Service Unavailable."); const message = new IncomingResponseMessage(); message.statusCode = 503; message.reasonPhrase = "Service Unavailable"; this.receiveResponse(message); } /** * Receive a response from the transaction layer. * @param message - Incoming response message. */ 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 }); } break; case /^1[0-9]{2}$/.test(statusCode): if (this.delegate && this.delegate.onProgress) { this.delegate.onProgress({ message }); } break; case /^2[0-9]{2}$/.test(statusCode): if (this.delegate && this.delegate.onAccept) { this.delegate.onAccept({ message }); } break; case /^3[0-9]{2}$/.test(statusCode): if (this.delegate && this.delegate.onRedirect) { this.delegate.onRedirect({ message }); } break; case /^[4-6][0-9]{2}$/.test(statusCode): if (this.delegate && this.delegate.onReject) { this.delegate.onReject({ message }); } break; default: throw new Error(`Invalid status code ${statusCode}`); } } init() { // We are the transaction user. const user = { loggerFactory: this.loggerFactory, onRequestTimeout: () => this.onRequestTimeout(), onStateChange: (newState) => { if (newState === TransactionState.Terminated) { // Remove the terminated transaction from the core. // eslint-disable-next-line @typescript-eslint/no-use-before-define this.core.userAgentClients.delete(userAgentClientId); // FIXME: HACK: Our transaction may have been swapped out with a new one // post authentication (see above), so make sure to only to dispose of // ourselves if this terminating transaction is our current transaction. // eslint-disable-next-line @typescript-eslint/no-use-before-define if (transaction === this._transaction) { this.dispose(); } } }, onTransportError: (error) => this.onTransportError(error), receiveResponse: (message) => this.receiveResponse(message) }; // Create a new transaction with us as the user. const transaction = new this.transactionConstructor(this.message, this.core.transport, user); this._transaction = transaction; // Add the new transaction to the core. const userAgentClientId = transaction.id + transaction.request.method; this.core.userAgentClients.set(userAgentClientId, this); } }