Commit 19f14c6 for jssip.net
commit 19f14c6d5be10dd40966906f9ebb2008eaa92fef
Author: José Luis Millán <jmillan@aliax.net>
Date: Wed Oct 21 11:53:13 2026 +0200
TS: RequestSender (#991)
Also fix, onTransportError() was always being called when UA was closed, even for BYE and ACK. Check comment:
// If ua is in closing process or even closed just allow sending Bye and ACK.
diff --git a/src/Dialog/RequestSender.js b/src/Dialog/RequestSender.js
index 95ab5b1..d846302 100644
--- a/src/Dialog/RequestSender.js
+++ b/src/Dialog/RequestSender.js
@@ -1,6 +1,6 @@
const JsSIP_C = require('../Constants');
const Transactions = require('../Transactions');
-const RequestSender = require('../RequestSender');
+const { RequestSender } = require('../RequestSender');
// Default event handlers.
const EventHandlers = {
diff --git a/src/Message.js b/src/Message.js
index 1c15b7d..94b94b9 100644
--- a/src/Message.js
+++ b/src/Message.js
@@ -3,7 +3,7 @@ const { Logger } = require('./Logger');
const JsSIP_C = require('./Constants');
const SIPMessage = require('./SIPMessage');
const Utils = require('./Utils');
-const RequestSender = require('./RequestSender');
+const { RequestSender } = require('./RequestSender');
const Exceptions = require('./Exceptions');
const URI = require('./URI');
diff --git a/src/Options.js b/src/Options.js
index 07a70de..ff9be47 100644
--- a/src/Options.js
+++ b/src/Options.js
@@ -3,7 +3,7 @@ const { Logger } = require('./Logger');
const JsSIP_C = require('./Constants');
const SIPMessage = require('./SIPMessage');
const Utils = require('./Utils');
-const RequestSender = require('./RequestSender');
+const { RequestSender } = require('./RequestSender');
const Exceptions = require('./Exceptions');
const logger = new Logger('Options');
diff --git a/src/RTCSession.js b/src/RTCSession.js
index f6c8b8b..654976e 100644
--- a/src/RTCSession.js
+++ b/src/RTCSession.js
@@ -11,7 +11,7 @@ const Utils = require('./Utils');
const Timers = require('./Timers');
const SIPMessage = require('./SIPMessage');
const Dialog = require('./Dialog');
-const RequestSender = require('./RequestSender');
+const { RequestSender } = require('./RequestSender');
const RTCSession_DTMF = require('./RTCSession/DTMF');
const RTCSession_Info = require('./RTCSession/Info');
const RTCSession_ReferNotifier = require('./RTCSession/ReferNotifier');
diff --git a/src/Registrator.js b/src/Registrator.js
index 0085139..1dd7141 100644
--- a/src/Registrator.js
+++ b/src/Registrator.js
@@ -2,7 +2,7 @@ const { Logger } = require('./Logger');
const Utils = require('./Utils');
const JsSIP_C = require('./Constants');
const SIPMessage = require('./SIPMessage');
-const RequestSender = require('./RequestSender');
+const { RequestSender } = require('./RequestSender');
const logger = new Logger('Registrator');
diff --git a/src/RequestSender.js b/src/RequestSender.js
deleted file mode 100644
index 1b6e198..0000000
--- a/src/RequestSender.js
+++ /dev/null
@@ -1,182 +0,0 @@
-const { Logger } = require('./Logger');
-const JsSIP_C = require('./Constants');
-const { DigestAuthentication } = require('./DigestAuthentication');
-const Transactions = require('./Transactions');
-
-const logger = new Logger('RequestSender');
-
-// Default event handlers.
-const EventHandlers = {
- onRequestTimeout: () => {},
- onTransportError: () => {},
- onReceiveResponse: () => {},
- onAuthenticated: () => {},
-};
-
-module.exports = class RequestSender {
- constructor(ua, request, eventHandlers) {
- this._ua = ua;
- this._eventHandlers = eventHandlers;
- this._method = request.method;
- this._request = request;
- this._auth = null;
- this._challenged = false;
- this._staled = false;
-
- // Define the undefined handlers.
- for (const handler in EventHandlers) {
- if (Object.prototype.hasOwnProperty.call(EventHandlers, handler)) {
- if (!this._eventHandlers[handler]) {
- this._eventHandlers[handler] = EventHandlers[handler];
- }
- }
- }
-
- // If ua is in closing process or even closed just allow sending Bye and ACK.
- if (
- ua.status === ua.C.STATUS_USER_CLOSED &&
- (this._method !== JsSIP_C.BYE || this._method !== JsSIP_C.ACK)
- ) {
- this._eventHandlers.onTransportError();
- }
- }
-
- /**
- * Create the client transaction and send the message.
- */
- send() {
- const eventHandlers = {
- onRequestTimeout: () => {
- this._eventHandlers.onRequestTimeout();
- },
- onTransportError: () => {
- this._eventHandlers.onTransportError();
- },
- onReceiveResponse: response => {
- this._receiveResponse(response);
- },
- };
-
- switch (this._method) {
- case 'INVITE': {
- this.clientTransaction = new Transactions.InviteClientTransaction(
- this._ua,
- this._ua.transport,
- this._request,
- eventHandlers
- );
- break;
- }
- case 'ACK': {
- this.clientTransaction = new Transactions.AckClientTransaction(
- this._ua,
- this._ua.transport,
- this._request,
- eventHandlers
- );
- break;
- }
- default: {
- this.clientTransaction = new Transactions.NonInviteClientTransaction(
- this._ua,
- this._ua.transport,
- this._request,
- eventHandlers
- );
- }
- }
- // If authorization JWT is present, use it.
- if (this._ua._configuration.authorization_jwt) {
- this._request.setHeader(
- 'Authorization',
- this._ua._configuration.authorization_jwt
- );
- }
-
- this.clientTransaction.send();
- }
-
- /**
- * Called from client transaction when receiving a correct response to the request.
- * Authenticate request if needed or pass the response back to the applicant.
- */
- _receiveResponse(response) {
- let challenge;
- let authorization_header_name;
- const status_code = response.status_code;
-
- /*
- * Authentication
- * Authenticate once. _challenged_ flag used to avoid infinite authentications.
- */
- if (
- (status_code === 401 || status_code === 407) &&
- (this._ua.configuration.password !== null ||
- this._ua.configuration.ha1 !== null)
- ) {
- // Get and parse the appropriate WWW-Authenticate or Proxy-Authenticate header.
- if (response.status_code === 401) {
- challenge = response.parseHeader('www-authenticate');
- authorization_header_name = 'authorization';
- } else {
- challenge = response.parseHeader('proxy-authenticate');
- authorization_header_name = 'proxy-authorization';
- }
-
- // Verify it seems a valid challenge.
- if (!challenge) {
- logger.debug(
- `${response.status_code} with wrong or missing challenge, cannot authenticate`
- );
- this._eventHandlers.onReceiveResponse(response);
-
- return;
- }
-
- if (!this._challenged || (!this._staled && challenge.stale === true)) {
- if (!this._auth) {
- this._auth = new DigestAuthentication({
- username: this._ua.configuration.authorization_user,
- password: this._ua.configuration.password,
- realm: this._ua.configuration.realm,
- ha1: this._ua.configuration.ha1,
- });
- }
-
- // Verify that the challenge is really valid.
- if (!this._auth.authenticate(this._request, challenge)) {
- this._eventHandlers.onReceiveResponse(response);
-
- return;
- }
- this._challenged = true;
-
- // Update ha1 and realm in the UA.
- this._ua.set('realm', this._auth.get('realm'));
- this._ua.set('ha1', this._auth.get('ha1'));
-
- if (challenge.stale) {
- this._staled = true;
- }
-
- this._request = this._request.clone();
- this._request.cseq += 1;
- this._request.setHeader(
- 'cseq',
- `${this._request.cseq} ${this._method}`
- );
- this._request.setHeader(
- authorization_header_name,
- this._auth.toString()
- );
-
- this._eventHandlers.onAuthenticated(this._request);
- this.send();
- } else {
- this._eventHandlers.onReceiveResponse(response);
- }
- } else {
- this._eventHandlers.onReceiveResponse(response);
- }
- }
-};
diff --git a/src/RequestSender.ts b/src/RequestSender.ts
new file mode 100644
index 0000000..8d267b7
--- /dev/null
+++ b/src/RequestSender.ts
@@ -0,0 +1,248 @@
+import * as JsSIP_C from './Constants';
+import { DigestAuthentication } from './DigestAuthentication';
+import type { DigestChallenge } from './DigestAuthentication';
+import { Logger } from './Logger';
+import type { IncomingResponse, OutgoingRequest } from './SIPMessage';
+import * as Transactions from './Transactions';
+
+const logger = new Logger('RequestSender');
+
+/**
+ * The `UA` surface a request sender depends on.
+ *
+ * TODO: Replace with `import type { UA } from './UA'` once `UA` is ported to
+ * TypeScript.
+ */
+export interface RequestSenderUA extends Transactions.TransactionUA {
+ readonly C: { STATUS_USER_CLOSED: number };
+ readonly status: number;
+ readonly configuration: Transactions.TransactionUA['configuration'] & {
+ authorization_user: string | null;
+ password: string | null;
+ realm: string | null;
+ ha1: string | null;
+ authorization_jwt: string | null;
+ };
+
+ set(parameter: string, value: unknown): boolean;
+}
+
+/**
+ * The callbacks a request sender reports to the applicant. Every one of them is
+ * optional; the ones left out fall back to `DEFAULT_EVENT_HANDLERS`.
+ */
+export interface RequestSenderEventHandlers {
+ onRequestTimeout(): void;
+ onTransportError(): void;
+ onReceiveResponse(response: IncomingResponse): void;
+ onAuthenticated(request: OutgoingRequest): void;
+}
+
+/**
+ * The transaction an outgoing request is sent over. Note that
+ * `AckClientTransaction` is not a real transaction and so, unlike the other
+ * two, has no `state`.
+ */
+export type ClientTransaction =
+ | Transactions.NonInviteClientTransaction
+ | Transactions.InviteClientTransaction
+ | Transactions.AckClientTransaction;
+
+// Default event handlers.
+const DEFAULT_EVENT_HANDLERS: RequestSenderEventHandlers = {
+ onRequestTimeout: () => {},
+ onTransportError: () => {},
+ onReceiveResponse: () => {},
+ onAuthenticated: () => {},
+};
+
+export class RequestSender {
+ /* NOTE: Not set by the constructor. Set by `send()`, which every applicant
+ * calls right after construction.
+ */
+ clientTransaction?: ClientTransaction;
+
+ #ua: RequestSenderUA;
+ #eventHandlers: RequestSenderEventHandlers;
+ #method: string;
+ #request: OutgoingRequest;
+ #auth: DigestAuthentication | null = null;
+ #challenged = false;
+ #staled = false;
+
+ constructor(
+ ua: RequestSenderUA,
+ request: OutgoingRequest,
+ eventHandlers: Partial<RequestSenderEventHandlers>
+ ) {
+ this.#ua = ua;
+ this.#method = request.method;
+ this.#request = request;
+
+ /* NOTE: Fills the given object in place, as the original implementation
+ * does, so the applicant sees the defaults too.
+ */
+ this.#eventHandlers = Object.assign(eventHandlers, {
+ onRequestTimeout:
+ eventHandlers.onRequestTimeout ||
+ DEFAULT_EVENT_HANDLERS.onRequestTimeout,
+ onTransportError:
+ eventHandlers.onTransportError ||
+ DEFAULT_EVENT_HANDLERS.onTransportError,
+ onReceiveResponse:
+ eventHandlers.onReceiveResponse ||
+ DEFAULT_EVENT_HANDLERS.onReceiveResponse,
+ onAuthenticated:
+ eventHandlers.onAuthenticated || DEFAULT_EVENT_HANDLERS.onAuthenticated,
+ });
+
+ // If ua is in closing process or even closed just allow sending Bye and ACK.
+ if (
+ ua.status === ua.C.STATUS_USER_CLOSED &&
+ this.#method !== JsSIP_C.BYE &&
+ this.#method !== JsSIP_C.ACK
+ ) {
+ this.#eventHandlers.onTransportError();
+ }
+ }
+
+ /**
+ * Create the client transaction and send the message.
+ */
+ send(): void {
+ const eventHandlers: Transactions.ClientTransactionEventHandlers = {
+ onRequestTimeout: () => {
+ this.#eventHandlers.onRequestTimeout();
+ },
+ onTransportError: () => {
+ this.#eventHandlers.onTransportError();
+ },
+ onReceiveResponse: response => {
+ this.#receiveResponse(response);
+ },
+ };
+
+ switch (this.#method) {
+ case 'INVITE': {
+ this.clientTransaction = new Transactions.InviteClientTransaction(
+ this.#ua,
+ this.#ua.transport,
+ this.#request,
+ eventHandlers
+ );
+ break;
+ }
+ case 'ACK': {
+ this.clientTransaction = new Transactions.AckClientTransaction(
+ this.#ua,
+ this.#ua.transport,
+ this.#request,
+ eventHandlers
+ );
+ break;
+ }
+ default: {
+ this.clientTransaction = new Transactions.NonInviteClientTransaction(
+ this.#ua,
+ this.#ua.transport,
+ this.#request,
+ eventHandlers
+ );
+ }
+ }
+
+ // If authorization JWT is present, use it.
+ if (this.#ua.configuration.authorization_jwt) {
+ this.#request.setHeader(
+ 'Authorization',
+ this.#ua.configuration.authorization_jwt
+ );
+ }
+
+ this.clientTransaction.send();
+ }
+
+ /**
+ * Called from client transaction when receiving a correct response to the request.
+ * Authenticate request if needed or pass the response back to the applicant.
+ */
+ #receiveResponse(response: IncomingResponse): void {
+ let challenge: DigestChallenge | undefined;
+ let authorization_header_name: string;
+ const status_code = response.status_code;
+
+ /*
+ * Authentication
+ * Authenticate once. _challenged_ flag used to avoid infinite authentications.
+ */
+ if (
+ (status_code === 401 || status_code === 407) &&
+ (this.#ua.configuration.password !== null ||
+ this.#ua.configuration.ha1 !== null)
+ ) {
+ // Get and parse the appropriate WWW-Authenticate or Proxy-Authenticate header.
+ if (response.status_code === 401) {
+ challenge = response.parseHeader('www-authenticate');
+ authorization_header_name = 'authorization';
+ } else {
+ challenge = response.parseHeader('proxy-authenticate');
+ authorization_header_name = 'proxy-authorization';
+ }
+
+ // Verify it seems a valid challenge.
+ if (!challenge) {
+ logger.debug(
+ `${response.status_code} with wrong or missing challenge, cannot authenticate`
+ );
+ this.#eventHandlers.onReceiveResponse(response);
+
+ return;
+ }
+
+ if (!this.#challenged || (!this.#staled && challenge.stale === true)) {
+ if (!this.#auth) {
+ this.#auth = new DigestAuthentication({
+ username: this.#ua.configuration.authorization_user,
+ password: this.#ua.configuration.password,
+ realm: this.#ua.configuration.realm,
+ ha1: this.#ua.configuration.ha1,
+ });
+ }
+
+ // Verify that the challenge is really valid.
+ if (!this.#auth.authenticate(this.#request, challenge)) {
+ this.#eventHandlers.onReceiveResponse(response);
+
+ return;
+ }
+ this.#challenged = true;
+
+ // Update ha1 and realm in the UA.
+ this.#ua.set('realm', this.#auth.get('realm'));
+ this.#ua.set('ha1', this.#auth.get('ha1'));
+
+ if (challenge.stale) {
+ this.#staled = true;
+ }
+
+ this.#request = this.#request.clone();
+ this.#request.cseq += 1;
+ this.#request.setHeader(
+ 'cseq',
+ `${this.#request.cseq} ${this.#method}`
+ );
+ this.#request.setHeader(
+ authorization_header_name,
+ this.#auth.toString()
+ );
+
+ this.#eventHandlers.onAuthenticated(this.#request);
+ this.send();
+ } else {
+ this.#eventHandlers.onReceiveResponse(response);
+ }
+ } else {
+ this.#eventHandlers.onReceiveResponse(response);
+ }
+ }
+}
diff --git a/src/Subscriber.js b/src/Subscriber.js
index 50bb0b8..99a10d9 100644
--- a/src/Subscriber.js
+++ b/src/Subscriber.js
@@ -5,7 +5,7 @@ const JsSIP_C = require('./Constants');
const Utils = require('./Utils');
const Grammar = require('./Grammar');
const SIPMessage = require('./SIPMessage');
-const RequestSender = require('./RequestSender');
+const { RequestSender } = require('./RequestSender');
const Dialog = require('./Dialog');
const logger = new Logger('Subscriber');
diff --git a/src/Transactions.ts b/src/Transactions.ts
index b94d9ea..b3c3bf0 100644
--- a/src/Transactions.ts
+++ b/src/Transactions.ts
@@ -89,10 +89,8 @@ export class NonInviteClientTransaction extends EventEmitter {
request: SIPMessage.OutgoingRequest;
eventHandlers: ClientTransactionEventHandlers;
- /* NOTE: `declare`d (and so not emitted) because the original implementation
- * does not create these own properties in the constructor. `state` is
- * created by `stateChanged()` and the timer handles when their timers are
- * armed.
+ /* NOTE: Not set by the constructor. `state` is set by `stateChanged()` and
+ * the timer handles when their timers are armed.
*/
state?: Status;
F?: ReturnType<typeof setTimeout>;
@@ -207,10 +205,8 @@ export class InviteClientTransaction extends EventEmitter {
request: SIPMessage.OutgoingRequest;
eventHandlers: ClientTransactionEventHandlers;
- /* NOTE: `declare`d (and so not emitted) because the original implementation
- * does not create these own properties in the constructor. `state` is
- * created by `stateChanged()` and the timer handles when their timers are
- * armed.
+ /* NOTE: Not set by the constructor. `state` is set by `stateChanged()` and
+ * the timer handles when their timers are armed.
*/
state?: Status;
B?: ReturnType<typeof setTimeout>;
@@ -453,8 +449,8 @@ export class NonInviteServerTransaction extends EventEmitter {
last_response: string;
state: Status;
- /* NOTE: `declare`d (and so not emitted) because the original implementation
- * does not create these own properties in the constructor.
+ /* NOTE: Not set by the constructor. `transportError` is set by
+ * `onTransportError()` and `J` when its timer is armed.
*/
transportError?: boolean;
J?: ReturnType<typeof setTimeout>;
@@ -574,8 +570,8 @@ export class InviteServerTransaction extends EventEmitter {
state: Status;
resendProvisionalTimer: ReturnType<typeof setInterval> | null;
- /* NOTE: `declare`d (and so not emitted) because the original implementation
- * does not create these own properties in the constructor.
+ /* NOTE: Not set by the constructor. `transportError` is set by
+ * `onTransportError()` and the timer handles when their timers are armed.
*/
transportError?: boolean;
H?: ReturnType<typeof setTimeout>;