Commit 6408b7a for jssip.net

commit 6408b7a3bb82f3ae352621af1b4dbf6abd09c0a0
Author: José Luis Millán <jmillan@aliax.net>
Date:   Wed Oct 21 11:21:41 2026 +0200

    TS: sanityCheck (#990)

diff --git a/src/UA.js b/src/UA.js
index 571d6a9..d34284c 100644
--- a/src/UA.js
+++ b/src/UA.js
@@ -16,7 +16,7 @@ const Exceptions = require('./Exceptions');
 const URI = require('./URI');
 const Parser = require('./Parser');
 const SIPMessage = require('./SIPMessage');
-const sanityCheck = require('./sanityCheck');
+const { sanityCheck } = require('./sanityCheck');
 const config = require('./Configuration');

 const logger = new Logger('UA');
diff --git a/src/sanityCheck.js b/src/sanityCheck.ts
similarity index 50%
rename from src/sanityCheck.js
rename to src/sanityCheck.ts
index 5ff60ac..2e5a9b8 100644
--- a/src/sanityCheck.js
+++ b/src/sanityCheck.ts
@@ -1,15 +1,38 @@
-const { Logger } = require('./Logger');
-const JsSIP_C = require('./Constants');
-const SIPMessage = require('./SIPMessage');
-const Utils = require('./Utils');
+import * as JsSIP_C from './Constants';
+import { Logger } from './Logger';
+import { IncomingRequest, IncomingResponse } from './SIPMessage';
+import type { IncomingMessage, UALike } from './SIPMessage';
+import type { TransactionRegistry } from './Transactions';
+import type { Transport } from './Transport';
+import * as Utils from './Utils';

 const logger = new Logger('sanityCheck');

+/**
+ * The `UA` surface the sanity checks depend on.
+ *
+ * TODO: Replace with `import type { UA } from './UA'` once `UA` is ported to
+ * TypeScript.
+ */
+export interface SanityCheckUA extends UALike {
+	readonly _transactions: TransactionRegistry;
+}
+
+/**
+ * A single check. Returns `false` to drop the message, `true` to let it
+ * through.
+ */
+type Check<M extends IncomingMessage> = (
+	message: M,
+	transport: Transport,
+	ua: SanityCheckUA
+) => boolean;
+
 // Checks for requests and responses.
-const all = [minimumHeaders];
+const all: Check<IncomingMessage>[] = [minimumHeaders];

 // Checks for requests.
-const requests = [
+const requests: Check<IncomingRequest>[] = [
 	rfc3261_8_2_2_1,
 	rfc3261_16_3_4,
 	rfc3261_18_3_request,
@@ -17,33 +40,31 @@ const requests = [
 ];

 // Checks for responses.
-const responses = [rfc3261_8_1_3_3, rfc3261_18_3_response];
-
-// local variables.
-let message;
-let ua;
-let transport;
-
-module.exports = (m, u, t) => {
-	message = m;
-	ua = u;
-	transport = t;
+const responses: Check<IncomingResponse>[] = [
+	rfc3261_8_1_3_3,
+	rfc3261_18_3_response,
+];

+export const sanityCheck = (
+	message: IncomingRequest | IncomingResponse,
+	ua: SanityCheckUA,
+	transport: Transport
+): boolean => {
 	for (const check of all) {
-		if (check() === false) {
+		if (!check(message, transport, ua)) {
 			return false;
 		}
 	}

-	if (message instanceof SIPMessage.IncomingRequest) {
+	if (message instanceof IncomingRequest) {
 		for (const check of requests) {
-			if (check() === false) {
+			if (!check(message, transport, ua)) {
 				return false;
 			}
 		}
-	} else if (message instanceof SIPMessage.IncomingResponse) {
+	} else if (message instanceof IncomingResponse) {
 		for (const check of responses) {
-			if (check() === false) {
+			if (!check(message, transport, ua)) {
 				return false;
 			}
 		}
@@ -73,44 +94,63 @@ module.exports = (m, u, t) => {
  */

 // Sanity Check functions for requests.
-function rfc3261_8_2_2_1() {
-	if (message.s('to').uri.scheme !== 'sip') {
-		reply(416);
+function rfc3261_8_2_2_1(
+	message: IncomingRequest,
+	transport: Transport
+): boolean {
+	if (message.to.uri.scheme !== 'sip') {
+		reply(message, transport, 416);

 		return false;
 	}
+
+	return true;
 }

-function rfc3261_16_3_4() {
+function rfc3261_16_3_4(
+	message: IncomingRequest,
+	transport: Transport,
+	ua: SanityCheckUA
+): boolean {
 	if (!message.to_tag) {
-		if (message.call_id.substr(0, 5) === ua.configuration.jssip_id) {
-			reply(482);
+		if (message.call_id?.substring(0, 5) === ua.configuration.jssip_id) {
+			reply(message, transport, 482);

 			return false;
 		}
 	}
+
+	return true;
 }

-function rfc3261_18_3_request() {
+function rfc3261_18_3_request(
+	message: IncomingRequest,
+	transport: Transport
+): boolean {
 	const len = Utils.str_utf8_length(message.body);
-	const contentLength = message.getHeader('content-length');
+	const contentLength = Number(message.getHeader('content-length'));

 	if (len < contentLength) {
-		reply(400);
+		reply(message, transport, 400);

 		return false;
 	}
+
+	return true;
 }

-function rfc3261_8_2_2_2() {
+function rfc3261_8_2_2_2(
+	message: IncomingRequest,
+	transport: Transport,
+	ua: SanityCheckUA
+): boolean {
 	const fromTag = message.from_tag;
 	const call_id = message.call_id;
 	const cseq = message.cseq;
-	let tr;

 	// Accept any in-dialog request.
 	if (message.to_tag) {
-		return;
+		return true;
 	}

 	// INVITE request.
@@ -118,28 +158,23 @@ function rfc3261_8_2_2_2() {
 		// If the branch matches the key of any IST then assume it is a retransmission
 		// and ignore the INVITE.
 		// TODO: we should reply the last response.
-		if (ua._transactions.ist[message.via_branch]) {
+		// TODO: A Via with no `branch` parameter keys on the literal string
+		// 'undefined', so two such requests look like retransmissions of each
+		// other. Kept as is to preserve behaviour.
+		if (ua._transactions.ist[String(message.via_branch)]) {
 			return false;
 		}
 		// Otherwise check whether it is a merged request.
 		else {
-			for (const transaction in ua._transactions.ist) {
+			for (const tr of Object.values(ua._transactions.ist)) {
 				if (
-					Object.prototype.hasOwnProperty.call(
-						ua._transactions.ist,
-						transaction
-					)
+					tr.request.from_tag === fromTag &&
+					tr.request.call_id === call_id &&
+					tr.request.cseq === cseq
 				) {
-					tr = ua._transactions.ist[transaction];
-					if (
-						tr.request.from_tag === fromTag &&
-						tr.request.call_id === call_id &&
-						tr.request.cseq === cseq
-					) {
-						reply(482);
-
-						return false;
-					}
+					reply(message, transport, 482);
+
+					return false;
 				}
 			}
 		}
@@ -150,33 +185,30 @@ function rfc3261_8_2_2_2() {
 	// If the branch matches the key of any NIST then assume it is a retransmission
 	// and ignore the request.
 	// TODO: we should reply the last response.
-	else if (ua._transactions.nist[message.via_branch]) {
+	else if (ua._transactions.nist[String(message.via_branch)]) {
 		return false;
 	}

 	// Otherwise check whether it is a merged request.
 	else {
-		for (const transaction in ua._transactions.nist) {
+		for (const tr of Object.values(ua._transactions.nist)) {
 			if (
-				Object.prototype.hasOwnProperty.call(ua._transactions.nist, transaction)
+				tr.request.from_tag === fromTag &&
+				tr.request.call_id === call_id &&
+				tr.request.cseq === cseq
 			) {
-				tr = ua._transactions.nist[transaction];
-				if (
-					tr.request.from_tag === fromTag &&
-					tr.request.call_id === call_id &&
-					tr.request.cseq === cseq
-				) {
-					reply(482);
+				reply(message, transport, 482);

-					return false;
-				}
+				return false;
 			}
 		}
 	}
+
+	return true;
 }

 // Sanity Check functions for responses.
-function rfc3261_8_1_3_3() {
+function rfc3261_8_1_3_3(message: IncomingResponse): boolean {
 	if (message.getHeaders('via').length > 1) {
 		logger.debug(
 			'more than one Via header field present in the response, dropping the response'
@@ -184,11 +216,13 @@ function rfc3261_8_1_3_3() {

 		return false;
 	}
+
+	return true;
 }

-function rfc3261_18_3_response() {
+function rfc3261_18_3_response(message: IncomingResponse): boolean {
 	const len = Utils.str_utf8_length(message.body),
-		contentLength = message.getHeader('content-length');
+		contentLength = Number(message.getHeader('content-length'));

 	if (len < contentLength) {
 		logger.debug(
@@ -197,10 +231,12 @@ function rfc3261_18_3_response() {

 		return false;
 	}
+
+	return true;
 }

 // Sanity Check functions for requests and responses.
-function minimumHeaders() {
+function minimumHeaders(message: IncomingMessage): boolean {
 	const mandatoryHeaders = ['from', 'to', 'call_id', 'cseq', 'via'];

 	for (const header of mandatoryHeaders) {
@@ -212,26 +248,28 @@ function minimumHeaders() {
 			return false;
 		}
 	}
+
+	return true;
 }

 // Reply.
-function reply(status_code) {
+function reply(
+	message: IncomingRequest,
+	transport: Transport,
+	status_code: number
+): void {
 	const vias = message.getHeaders('via');

-	let to;
 	let response = `SIP/2.0 ${status_code} ${JsSIP_C.REASON_PHRASE[status_code]}\r\n`;

 	for (const via of vias) {
 		response += `Via: ${via}\r\n`;
 	}

-	to = message.getHeader('To');
-
-	if (!message.to_tag) {
-		to += `;tag=${Utils.newTag()}`;
-	}
+	const to = message.getHeader('To');
+	const toTag = message.to_tag ? '' : `;tag=${Utils.newTag()}`;

-	response += `To: ${to}\r\n`;
+	response += `To: ${to}${toTag}\r\n`;
 	response += `From: ${message.getHeader('From')}\r\n`;
 	response += `Call-ID: ${message.call_id}\r\n`;
 	response += `CSeq: ${message.cseq} ${message.method}\r\n`;