Commit 1729a27 for jssip.net
commit 1729a2749647c46e3cf48b0ee71820b399c79eed
Author: José Luis Millán <jmillan@aliax.net>
Date: Tue Oct 20 16:10:34 2026 +0200
TS: move SIPMessage (#985)
diff --git a/src/DigestAuthentication.ts b/src/DigestAuthentication.ts
index a89a09b..35dbbb2 100644
--- a/src/DigestAuthentication.ts
+++ b/src/DigestAuthentication.ts
@@ -3,20 +3,24 @@ import { calculateMD5, createRandomToken } from './Utils';
const logger = new Logger('DigestAuthentication');
+/* NOTE: All of them are nullable because they come straight from the UA
+ * configuration, whose defaults are `null`.
+ */
export type DigestCredentials = {
- username?: string;
- password?: string;
- realm?: string;
+ username?: string | null;
+ password?: string | null;
+ realm?: string | null;
ha1?: string | null;
};
+// A parsed WWW-Authenticate or Proxy-Authenticate header field.
export type DigestChallenge = {
- algorithm?: string | null;
- realm?: string | null;
- nonce?: string | null;
- opaque?: string | null;
- stale?: boolean | null;
- qop?: string | string[] | null;
+ algorithm?: string;
+ realm?: string;
+ nonce?: string;
+ opaque?: string;
+ stale?: boolean;
+ qop?: string | string[];
};
export type DigestAuthenticateRequest = {
@@ -27,36 +31,24 @@ export type DigestAuthenticateRequest = {
export class DigestAuthentication {
#credentials: DigestCredentials;
- #cnonce: string | null;
- #nc: number;
- #ncHex: string;
- #algorithm: string | null | undefined;
- #realm: string | null | undefined;
- #nonce: string | null | undefined;
- #opaque: string | null | undefined;
+ #nc = 0;
+ #ncHex = '00000000';
+ #cnonce?: string;
+ #algorithm?: string;
+ #realm?: string;
+ #nonce?: string;
+ #opaque?: string;
// eslint-disable-next-line no-unused-private-class-members
- #stale: boolean | null | undefined;
- #qop: 'auth' | 'auth-int' | null;
- #method: string | null;
- #uri: string | { toString(): string } | null;
- #ha1: string | null | undefined;
- #response: string | null;
+ #stale?: boolean;
+ #qop?: 'auth' | 'auth-int';
+ #method?: string;
+ #uri?: string | { toString(): string };
+ // NOTE: Nullable because it may come from the UA configuration.
+ #ha1?: string | null;
+ #response?: string;
constructor(credentials: DigestCredentials) {
this.#credentials = credentials;
- this.#cnonce = null;
- this.#nc = 0;
- this.#ncHex = '00000000';
- this.#algorithm = null;
- this.#realm = null;
- this.#nonce = null;
- this.#opaque = null;
- this.#stale = null;
- this.#qop = null;
- this.#method = null;
- this.#uri = null;
- this.#ha1 = null;
- this.#response = null;
}
/**
@@ -64,7 +56,7 @@ export class DigestAuthentication {
*
* NOTE: Exposed for testing purposes only.
*/
- get response(): string | null {
+ get response(): string | undefined {
return this.#response;
}
@@ -168,7 +160,7 @@ export class DigestAuthentication {
return false;
}
} else {
- this.#qop = null;
+ this.#qop = undefined;
}
// Fill other attributes.
@@ -226,7 +218,7 @@ export class DigestAuthentication {
this.#response = calculateMD5(
`${this.#ha1}:${this.#nonce}:${this.#ncHex}:${this.#cnonce}:auth-int:${ha2}`
);
- } else if (this.#qop === null) {
+ } else if (this.#qop === undefined) {
// HA2 = MD5(A2) = MD5(method:digestURI).
a2 = `${this.#method}:${this.#uri}`;
ha2 = calculateMD5(a2);
diff --git a/src/Grammar.d.ts b/src/Grammar.d.ts
index 3baa70c..6f69b8c 100644
--- a/src/Grammar.d.ts
+++ b/src/Grammar.d.ts
@@ -1,6 +1,80 @@
+import NameAddrHeader = require('./NameAddrHeader');
+
+/**
+ * The result of `parse()`, whose shape depends on the start rule used.
+ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Grammar = any;
+/**
+ * Generic header field parameters, as collected by the `generic_param` rule.
+ */
+export type GenericParams = Record<string, string | null>;
+
+/**
+ * How a host was written, as set by the `hostname`, `IPv4address` and
+ * `IPv6address` rules.
+ */
+export type HostType = 'domain' | 'IPv4' | 'IPv6';
+
+/**
+ * Result of the `Via` start rule.
+ */
+export type ViaHeader = {
+ protocol: string;
+ transport: string;
+ host: string;
+ host_type: HostType;
+ port?: number;
+ ttl?: number;
+ maddr?: string;
+ received?: string;
+ branch?: string;
+ rport?: number;
+ params?: GenericParams;
+};
+
+/**
+ * Result of the `CSeq` start rule.
+ */
+export type CSeqHeader = {
+ value: number;
+ method: string;
+};
+
+/**
+ * Result of the `Session_Expires` start rule.
+ */
+export type SessionExpiresHeader = {
+ expires: number;
+ refresher?: 'uac' | 'uas';
+ params?: GenericParams;
+};
+
+/**
+ * Result of the `Replaces` start rule.
+ */
+export type ReplacesHeader = {
+ call_id: string;
+ to_tag?: string;
+ from_tag?: string;
+ early_only?: true;
+ params?: GenericParams;
+};
+
+/**
+ * Result of the `Event` start rule.
+ */
+export type EventHeader = {
+ event: string;
+ params?: GenericParams;
+};
+
+/**
+ * Result of the `Refer_To` start rule.
+ */
+export type ReferToHeader = NameAddrHeader;
+
export function parse(input: string, startRule?: string): Grammar;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
diff --git a/src/IncomingMessageBuilder.ts b/src/IncomingMessageBuilder.ts
new file mode 100644
index 0000000..a75280f
--- /dev/null
+++ b/src/IncomingMessageBuilder.ts
@@ -0,0 +1,356 @@
+import { Logger } from './Logger';
+import * as Grammar from './Grammar';
+import NameAddrHeader = require('./NameAddrHeader');
+import URI = require('./URI');
+import {
+ IncomingMessage,
+ IncomingMessageParams,
+ IncomingRequest,
+ IncomingResponse,
+ UALike,
+} from './SIPMessage';
+import type { IncomingHeaders } from './SIPMessage';
+
+const logger = new Logger('IncomingMessageBuilder');
+
+/**
+ * Collects the header fields of an incoming SIP message as they are scanned
+ * and, once the whole message has been read, builds the corresponding
+ * `IncomingRequest` or `IncomingResponse`.
+ *
+ * It exists so that every mandatory field of an `IncomingMessage` is provided
+ * at construction time, rather than assigned onto a half built instance.
+ */
+export class IncomingMessageBuilder {
+ #headers: IncomingHeaders = {};
+
+ #via?: Grammar.ViaHeader;
+ #via_branch?: string;
+ #call_id?: string;
+ #cseq?: number;
+ /* NOTE: The method as given by the CSeq header field. A response takes its
+ * `method` from here, a request takes it from the request line.
+ */
+ #cseq_method?: string;
+ #from?: NameAddrHeader;
+ #to?: NameAddrHeader;
+ /* NOTE: `null` when the tag parameter is present but valueless (`;tag`),
+ * `undefined` when it is absent altogether.
+ */
+ #from_tag?: string | null;
+ #to_tag?: string | null;
+
+ // Only set when the corresponding header field is present.
+ #session_expires?: number;
+ #session_expires_refresher?: string;
+ #refer_to?: Grammar.ReferToHeader;
+ #replaces?: Grammar.ReplacesHeader;
+ #event?: Grammar.EventHeader;
+
+ /**
+ * Whether the given header field is present.
+ */
+ hasHeader(name: string): boolean {
+ return IncomingMessage.hasHeader(this.#headers, name);
+ }
+
+ /**
+ * Value of the given header field, or undefined if not present.
+ */
+ getHeader(name: string): string | undefined {
+ return IncomingMessage.getHeader(this.#headers, name);
+ }
+
+ /**
+ * Interpret a single raw header field, storing it and, when it is a well
+ * known one, extracting the message attributes it carries.
+ *
+ * Returns false if the header field is present but cannot be parsed.
+ */
+ addRawHeader(name: string, value: string): boolean {
+ let parsed;
+
+ // If header-field is well-known, parse it.
+ switch (name.toLowerCase()) {
+ case 'via':
+ case 'v': {
+ IncomingMessage.addHeader(this.#headers, 'via', value);
+ if (IncomingMessage.getHeaders(this.#headers, 'via').length === 1) {
+ parsed = IncomingMessage.parseHeader<Grammar.ViaHeader>(
+ this.#headers,
+ 'Via'
+ );
+ if (parsed) {
+ this.#via = parsed;
+ this.#via_branch = parsed.branch;
+ }
+ } else {
+ parsed = 0;
+ }
+ break;
+ }
+ case 'from':
+ case 'f': {
+ IncomingMessage.setHeader(this.#headers, 'from', value);
+ parsed = IncomingMessage.parseHeader<NameAddrHeader>(
+ this.#headers,
+ 'from'
+ );
+ if (parsed) {
+ this.#from = parsed;
+ this.#from_tag = parsed.getParam('tag');
+ }
+ break;
+ }
+ case 'to':
+ case 't': {
+ IncomingMessage.setHeader(this.#headers, 'to', value);
+ parsed = IncomingMessage.parseHeader<NameAddrHeader>(
+ this.#headers,
+ 'to'
+ );
+ if (parsed) {
+ this.#to = parsed;
+ this.#to_tag = parsed.getParam('tag');
+ }
+ break;
+ }
+ case 'record-route': {
+ parsed = Grammar.parse(value, 'Record_Route');
+
+ if (parsed === -1) {
+ parsed = undefined;
+ } else {
+ for (const header of parsed) {
+ IncomingMessage.addHeader(
+ this.#headers,
+ 'record-route',
+ value.substring(header.possition, header.offset)
+ );
+ this.#headers['Record-Route']![
+ IncomingMessage.getHeaders(this.#headers, 'record-route').length -
+ 1
+ ]!.parsed = header.parsed;
+ }
+ }
+ break;
+ }
+ case 'call-id':
+ case 'i': {
+ IncomingMessage.setHeader(this.#headers, 'call-id', value);
+ parsed = IncomingMessage.parseHeader(this.#headers, 'call-id');
+ if (parsed) {
+ this.#call_id = value;
+ }
+ break;
+ }
+ case 'contact':
+ case 'm': {
+ parsed = Grammar.parse(value, 'Contact');
+
+ if (parsed === -1) {
+ parsed = undefined;
+ } else {
+ for (const header of parsed) {
+ IncomingMessage.addHeader(
+ this.#headers,
+ 'contact',
+ value.substring(header.possition, header.offset)
+ );
+ this.#headers['Contact']![
+ IncomingMessage.getHeaders(this.#headers, 'contact').length - 1
+ ]!.parsed = header.parsed;
+ }
+ }
+ break;
+ }
+ case 'content-length':
+ case 'l': {
+ IncomingMessage.setHeader(this.#headers, 'content-length', value);
+ parsed = IncomingMessage.parseHeader(this.#headers, 'content-length');
+ break;
+ }
+ case 'content-type':
+ case 'c': {
+ IncomingMessage.setHeader(this.#headers, 'content-type', value);
+ parsed = IncomingMessage.parseHeader(this.#headers, 'content-type');
+ break;
+ }
+ case 'cseq': {
+ IncomingMessage.setHeader(this.#headers, 'cseq', value);
+ parsed = IncomingMessage.parseHeader<Grammar.CSeqHeader>(
+ this.#headers,
+ 'cseq'
+ );
+ if (parsed) {
+ this.#cseq = parsed.value;
+
+ /* NOTE: Only a response uses it, but it is recorded
+ * unconditionally so that the builder does not need to know
+ * which kind of message is being built.
+ */
+ this.#cseq_method = parsed.method;
+ }
+ break;
+ }
+ case 'max-forwards': {
+ IncomingMessage.setHeader(this.#headers, 'max-forwards', value);
+ parsed = IncomingMessage.parseHeader(this.#headers, 'max-forwards');
+ break;
+ }
+ case 'www-authenticate': {
+ IncomingMessage.setHeader(this.#headers, 'www-authenticate', value);
+ parsed = IncomingMessage.parseHeader(this.#headers, 'www-authenticate');
+ break;
+ }
+ case 'proxy-authenticate': {
+ IncomingMessage.setHeader(this.#headers, 'proxy-authenticate', value);
+ parsed = IncomingMessage.parseHeader(
+ this.#headers,
+ 'proxy-authenticate'
+ );
+ break;
+ }
+ case 'session-expires':
+ case 'x': {
+ IncomingMessage.setHeader(this.#headers, 'session-expires', value);
+ parsed = IncomingMessage.parseHeader<Grammar.SessionExpiresHeader>(
+ this.#headers,
+ 'session-expires'
+ );
+ if (parsed) {
+ this.#session_expires = parsed.expires;
+ this.#session_expires_refresher = parsed.refresher;
+ }
+ break;
+ }
+ case 'refer-to':
+ case 'r': {
+ IncomingMessage.setHeader(this.#headers, 'refer-to', value);
+ parsed = IncomingMessage.parseHeader<Grammar.ReferToHeader>(
+ this.#headers,
+ 'refer-to'
+ );
+ if (parsed) {
+ this.#refer_to = parsed;
+ }
+ break;
+ }
+ case 'replaces': {
+ IncomingMessage.setHeader(this.#headers, 'replaces', value);
+ parsed = IncomingMessage.parseHeader<Grammar.ReplacesHeader>(
+ this.#headers,
+ 'replaces'
+ );
+ if (parsed) {
+ this.#replaces = parsed;
+ }
+ break;
+ }
+ case 'event':
+ case 'o': {
+ IncomingMessage.setHeader(this.#headers, 'event', value);
+ parsed = IncomingMessage.parseHeader<Grammar.EventHeader>(
+ this.#headers,
+ 'event'
+ );
+ if (parsed) {
+ this.#event = parsed;
+ }
+ break;
+ }
+ default: {
+ // Do not parse this header.
+ IncomingMessage.addHeader(this.#headers, name, value);
+ parsed = 0;
+ }
+ }
+
+ return parsed !== undefined;
+ }
+
+ /**
+ * Build the `IncomingRequest`, or undefined if a mandatory header field is
+ * missing.
+ */
+ buildRequest(
+ ua: UALike,
+ data: string,
+ method: string,
+ ruri: URI,
+ body: string
+ ): IncomingRequest | undefined {
+ const params = this.#params(data, method, body);
+
+ if (!params) {
+ return undefined;
+ }
+
+ return new IncomingRequest({ ...params, ua, ruri });
+ }
+
+ /**
+ * Build the `IncomingResponse`, or undefined if a mandatory header field is
+ * missing.
+ */
+ buildResponse(
+ data: string,
+ status_code: number,
+ reason_phrase: string,
+ body: string
+ ): IncomingResponse | undefined {
+ // RFC 3261 8.1.3.3. A response takes its method from its CSeq.
+ if (this.#cseq_method === undefined) {
+ logger.debug('missing mandatory header field : cseq, dropping message');
+
+ return undefined;
+ }
+
+ const params = this.#params(data, this.#cseq_method, body);
+
+ if (!params) {
+ return undefined;
+ }
+
+ return new IncomingResponse({ ...params, status_code, reason_phrase });
+ }
+
+ #params(
+ data: string,
+ method: string,
+ body: string
+ ): IncomingMessageParams | undefined {
+ // RFC 3261 8.1.1. From and To are mandatory in every SIP message.
+ if (this.#from === undefined) {
+ logger.debug('missing mandatory header field : from, dropping message');
+
+ return undefined;
+ }
+
+ if (this.#to === undefined) {
+ logger.debug('missing mandatory header field : to, dropping message');
+
+ return undefined;
+ }
+
+ return {
+ data,
+ headers: this.#headers,
+ method,
+ via: this.#via,
+ via_branch: this.#via_branch,
+ call_id: this.#call_id,
+ cseq: this.#cseq,
+ from: this.#from,
+ from_tag: this.#from_tag,
+ to: this.#to,
+ to_tag: this.#to_tag,
+ body,
+ session_expires: this.#session_expires,
+ session_expires_refresher: this.#session_expires_refresher,
+ refer_to: this.#refer_to,
+ replaces: this.#replaces,
+ event: this.#event,
+ };
+ }
+}
diff --git a/src/NameAddrHeader.ts b/src/NameAddrHeader.ts
index 2837b77..87953ce 100644
--- a/src/NameAddrHeader.ts
+++ b/src/NameAddrHeader.ts
@@ -19,7 +19,7 @@ class NameAddrHeader {
}
#uri: URI;
- #display_name: string | undefined;
+ #display_name?: string;
#parameters: URI.Parameters;
constructor(
diff --git a/src/Parser.js b/src/Parser.js
index a2e67be..d2fc29a 100644
--- a/src/Parser.js
+++ b/src/Parser.js
@@ -1,6 +1,6 @@
const { Logger } = require('./Logger');
const Grammar = require('./Grammar');
-const SIPMessage = require('./SIPMessage');
+const { IncomingMessageBuilder } = require('./IncomingMessageBuilder');
const logger = new Logger('Parser');
@@ -8,7 +8,6 @@ const logger = new Logger('Parser');
* Parse SIP Message
*/
exports.parseMessage = (data, ua) => {
- let message;
let bodyStart;
let headerEnd = data.indexOf('\r\n');
@@ -20,7 +19,7 @@ exports.parseMessage = (data, ua) => {
// Parse first line. Check if it is a Request or a Reply.
const firstLine = data.substring(0, headerEnd);
- let parsed = Grammar.parse(firstLine, 'Request_Response');
+ const parsed = Grammar.parse(firstLine, 'Request_Response');
if (parsed === -1) {
logger.warn(
@@ -28,17 +27,11 @@ exports.parseMessage = (data, ua) => {
);
return;
- } else if (!parsed.status_code) {
- message = new SIPMessage.IncomingRequest(ua);
- message.method = parsed.method;
- message.ruri = parsed.uri;
- } else {
- message = new SIPMessage.IncomingResponse();
- message.status_code = parsed.status_code;
- message.reason_phrase = parsed.reason_phrase;
}
- message.data = data;
+ const isResponse = Boolean(parsed.status_code);
+ const builder = new IncomingMessageBuilder();
+
let headerStart = headerEnd + 2;
/* Loop over every line in data. Detect the end of each header and parse
@@ -59,10 +52,10 @@ exports.parseMessage = (data, ua) => {
return;
}
- parsed = parseHeader(message, data, headerStart, headerEnd);
+ const result = parseHeader(builder, data, headerStart, headerEnd);
- if (parsed !== true) {
- logger.warn('parseMessage() |', parsed.error);
+ if (result !== true) {
+ logger.warn('parseMessage() |', result.error);
return;
}
@@ -74,15 +67,26 @@ exports.parseMessage = (data, ua) => {
* If there are additional bytes in the transport packet
* beyond the end of the body, they MUST be discarded.
*/
- if (message.hasHeader('content-length')) {
- const contentLength = message.getHeader('content-length');
+ let body;
+
+ if (builder.hasHeader('content-length')) {
+ const contentLength = builder.getHeader('content-length');
- message.body = data.substr(bodyStart, contentLength);
+ body = data.substr(bodyStart, contentLength);
} else {
- message.body = data.substring(bodyStart);
+ body = data.substring(bodyStart);
}
- return message;
+ if (isResponse) {
+ return builder.buildResponse(
+ data,
+ parsed.status_code,
+ parsed.reason_phrase,
+ body
+ );
+ } else {
+ return builder.buildRequest(ua, data, parsed.method, parsed.uri, body);
+ }
};
/**
@@ -124,180 +128,16 @@ function getHeader(data, headerStart) {
return end;
}
-function parseHeader(message, data, headerStart, headerEnd) {
- let parsed;
+function parseHeader(builder, data, headerStart, headerEnd) {
const hcolonIndex = data.indexOf(':', headerStart);
const headerName = data.substring(headerStart, hcolonIndex).trim();
const headerValue = data.substring(hcolonIndex + 1, headerEnd).trim();
- // If header-field is well-known, parse it.
- switch (headerName.toLowerCase()) {
- case 'via':
- case 'v': {
- message.addHeader('via', headerValue);
- if (message.getHeaders('via').length === 1) {
- parsed = message.parseHeader('Via');
- if (parsed) {
- message.via = parsed;
- message.via_branch = parsed.branch;
- }
- } else {
- parsed = 0;
- }
- break;
- }
- case 'from':
- case 'f': {
- message.setHeader('from', headerValue);
- parsed = message.parseHeader('from');
- if (parsed) {
- message.from = parsed;
- message.from_tag = parsed.getParam('tag');
- }
- break;
- }
- case 'to':
- case 't': {
- message.setHeader('to', headerValue);
- parsed = message.parseHeader('to');
- if (parsed) {
- message.to = parsed;
- message.to_tag = parsed.getParam('tag');
- }
- break;
- }
- case 'record-route': {
- parsed = Grammar.parse(headerValue, 'Record_Route');
-
- if (parsed === -1) {
- parsed = undefined;
- } else {
- for (const header of parsed) {
- message.addHeader(
- 'record-route',
- headerValue.substring(header.possition, header.offset)
- );
- message.headers['Record-Route'][
- message.getHeaders('record-route').length - 1
- ].parsed = header.parsed;
- }
- }
- break;
- }
- case 'call-id':
- case 'i': {
- message.setHeader('call-id', headerValue);
- parsed = message.parseHeader('call-id');
- if (parsed) {
- message.call_id = headerValue;
- }
- break;
- }
- case 'contact':
- case 'm': {
- parsed = Grammar.parse(headerValue, 'Contact');
-
- if (parsed === -1) {
- parsed = undefined;
- } else {
- for (const header of parsed) {
- message.addHeader(
- 'contact',
- headerValue.substring(header.possition, header.offset)
- );
- message.headers.Contact[
- message.getHeaders('contact').length - 1
- ].parsed = header.parsed;
- }
- }
- break;
- }
- case 'content-length':
- case 'l': {
- message.setHeader('content-length', headerValue);
- parsed = message.parseHeader('content-length');
- break;
- }
- case 'content-type':
- case 'c': {
- message.setHeader('content-type', headerValue);
- parsed = message.parseHeader('content-type');
- break;
- }
- case 'cseq': {
- message.setHeader('cseq', headerValue);
- parsed = message.parseHeader('cseq');
- if (parsed) {
- message.cseq = parsed.value;
- }
- if (message instanceof SIPMessage.IncomingResponse) {
- message.method = parsed.method;
- }
- break;
- }
- case 'max-forwards': {
- message.setHeader('max-forwards', headerValue);
- parsed = message.parseHeader('max-forwards');
- break;
- }
- case 'www-authenticate': {
- message.setHeader('www-authenticate', headerValue);
- parsed = message.parseHeader('www-authenticate');
- break;
- }
- case 'proxy-authenticate': {
- message.setHeader('proxy-authenticate', headerValue);
- parsed = message.parseHeader('proxy-authenticate');
- break;
- }
- case 'session-expires':
- case 'x': {
- message.setHeader('session-expires', headerValue);
- parsed = message.parseHeader('session-expires');
- if (parsed) {
- message.session_expires = parsed.expires;
- message.session_expires_refresher = parsed.refresher;
- }
- break;
- }
- case 'refer-to':
- case 'r': {
- message.setHeader('refer-to', headerValue);
- parsed = message.parseHeader('refer-to');
- if (parsed) {
- message.refer_to = parsed;
- }
- break;
- }
- case 'replaces': {
- message.setHeader('replaces', headerValue);
- parsed = message.parseHeader('replaces');
- if (parsed) {
- message.replaces = parsed;
- }
- break;
- }
- case 'event':
- case 'o': {
- message.setHeader('event', headerValue);
- parsed = message.parseHeader('event');
- if (parsed) {
- message.event = parsed;
- }
- break;
- }
- default: {
- // Do not parse this header.
- message.addHeader(headerName, headerValue);
- parsed = 0;
- }
- }
-
- if (parsed === undefined) {
+ if (!builder.addRawHeader(headerName, headerValue)) {
return {
error: `error parsing header "${headerName}"`,
};
- } else {
- return true;
}
+
+ return true;
}
diff --git a/src/SIPMessage.d.ts b/src/SIPMessage.d.ts
deleted file mode 100644
index 40da3cd..0000000
--- a/src/SIPMessage.d.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import NameAddrHeader = require('./NameAddrHeader');
-import URI = require('./URI');
-
-declare class IncomingMessage {
- method: string;
- from: NameAddrHeader;
- to: NameAddrHeader;
- body: string;
-
- constructor();
-
- countHeader(name: string): number;
-
- getHeader(name: string): string;
-
- getHeaders(name: string): string[];
-
- hasHeader(name: string): boolean;
-
- parseHeader<T = unknown>(name: string, idx?: number): T;
-
- toString(): string;
-}
-
-export class IncomingRequest extends IncomingMessage {
- ruri: URI;
-}
-
-export class IncomingResponse extends IncomingMessage {
- status_code: number;
- reason_phrase: string;
-}
-
-export class OutgoingRequest {
- method: string;
- ruri: URI;
- cseq: number;
- call_id: string;
- from: NameAddrHeader;
- to: NameAddrHeader;
- body: string;
-
- setHeader(name: string, value: string | string[]): void;
-
- getHeader(name: string): string;
-
- getHeaders(name: string): string[];
-
- hasHeader(name: string): boolean;
-
- toString(): string;
-}
diff --git a/src/SIPMessage.js b/src/SIPMessage.js
deleted file mode 100644
index 44b08e7..0000000
--- a/src/SIPMessage.js
+++ /dev/null
@@ -1,684 +0,0 @@
-const sdp_transform = require('sdp-transform');
-const { Logger } = require('./Logger');
-const JsSIP_C = require('./Constants');
-const Utils = require('./Utils');
-const NameAddrHeader = require('./NameAddrHeader');
-const Grammar = require('./Grammar');
-
-const logger = new Logger('SIPMessage');
-
-/**
- * -param {String} method request method
- * -param {String} ruri request uri
- * -param {UA} ua
- * -param {Object} params parameters that will have priority over ua.configuration parameters:
- * <br>
- * - cseq, call_id, from_tag, from_uri, from_display_name, to_uri, to_tag, route_set
- * -param {Object} [headers] extra headers
- * -param {String} [body]
- */
-class OutgoingRequest {
- constructor(method, ruri, ua, params, extraHeaders, body) {
- // Mandatory parameters check.
- if (!method || !ruri || !ua) {
- return null;
- }
-
- params = params || {};
-
- this.ua = ua;
- this.headers = {};
- this.method = method;
- this.ruri = ruri;
- this.body = body;
- this.extraHeaders = Utils.cloneArray(extraHeaders);
-
- if (this.ua.configuration.extra_headers) {
- this.extraHeaders = this.extraHeaders.concat(
- this.ua.configuration.extra_headers
- );
- }
-
- // Fill the Common SIP Request Headers.
-
- // Route.
- if (params.route_set) {
- this.setHeader('route', params.route_set);
- } else if (ua.configuration.use_preloaded_route) {
- this.setHeader('route', `<${ua.transport.sip_uri};lr>`);
- }
-
- // Via.
- // Empty Via header. Will be filled by the client transaction.
- this.setHeader('via', '');
-
- // Max-Forwards.
- this.setHeader('max-forwards', JsSIP_C.MAX_FORWARDS);
-
- // To
- const to_uri = params.to_uri || ruri;
- const to_params = params.to_tag ? { tag: params.to_tag } : null;
- const to_display_name =
- typeof params.to_display_name !== 'undefined'
- ? params.to_display_name
- : null;
-
- this.to = new NameAddrHeader(to_uri, to_display_name, to_params);
- this.setHeader('to', this.to.toString());
-
- // From.
- const from_uri = params.from_uri || ua.configuration.uri;
- const from_params = { tag: params.from_tag || Utils.newTag() };
- let display_name;
-
- if (typeof params.from_display_name !== 'undefined') {
- display_name = params.from_display_name;
- } else if (ua.configuration.display_name) {
- display_name = ua.configuration.display_name;
- } else {
- display_name = null;
- }
-
- this.from = new NameAddrHeader(from_uri, display_name, from_params);
- this.setHeader('from', this.from.toString());
-
- // Call-ID.
- const call_id =
- params.call_id || ua.configuration.jssip_id + Utils.createRandomToken(15);
-
- this.call_id = call_id;
- this.setHeader('call-id', call_id);
-
- // CSeq.
- const cseq = params.cseq || Math.floor(Math.random() * 10000);
-
- this.cseq = cseq;
- this.setHeader('cseq', `${cseq} ${method}`);
- }
-
- /**
- * Replace the the given header by the given value.
- * -param {String} name header name
- * -param {String | Array} value header value
- */
- setHeader(name, value) {
- // Remove the header from extraHeaders if present.
- const regexp = new RegExp(`^\\s*${name}\\s*:`, 'i');
-
- for (let idx = 0; idx < this.extraHeaders.length; idx++) {
- if (regexp.test(this.extraHeaders[idx])) {
- this.extraHeaders.splice(idx, 1);
- }
- }
-
- this.headers[Utils.headerize(name)] = Array.isArray(value)
- ? value
- : [value];
- }
-
- /**
- * Get the value of the given header name at the given position.
- * -param {String} name header name
- * -returns {String|undefined} Returns the specified header, null if header doesn't exist.
- */
- getHeader(name) {
- const headers = this.headers[Utils.headerize(name)];
-
- if (headers) {
- if (headers[0]) {
- return headers[0];
- }
- } else {
- const regexp = new RegExp(`^\\s*${name}\\s*:`, 'i');
-
- for (const header of this.extraHeaders) {
- if (regexp.test(header)) {
- return header.substring(header.indexOf(':') + 1).trim();
- }
- }
- }
-
- return;
- }
-
- /**
- * Get the header/s of the given name.
- * -param {String} name header name
- * -returns {Array} Array with all the headers of the specified name.
- */
- getHeaders(name) {
- const headers = this.headers[Utils.headerize(name)];
- const result = [];
-
- if (headers) {
- for (const header of headers) {
- result.push(header);
- }
-
- return result;
- } else {
- const regexp = new RegExp(`^\\s*${name}\\s*:`, 'i');
-
- for (const header of this.extraHeaders) {
- if (regexp.test(header)) {
- result.push(header.substring(header.indexOf(':') + 1).trim());
- }
- }
-
- return result;
- }
- }
-
- /**
- * Verify the existence of the given header.
- * -param {String} name header name
- * -returns {boolean} true if header with given name exists, false otherwise
- */
- hasHeader(name) {
- if (this.headers[Utils.headerize(name)]) {
- return true;
- } else {
- const regexp = new RegExp(`^\\s*${name}\\s*:`, 'i');
-
- for (const header of this.extraHeaders) {
- if (regexp.test(header)) {
- return true;
- }
- }
- }
-
- return false;
- }
-
- /**
- * Parse the current body as a SDP and store the resulting object
- * into this.sdp.
- * -param {Boolean} force: Parse even if this.sdp already exists.
- *
- * Returns this.sdp.
- */
- parseSDP(force) {
- if (!force && this.sdp) {
- return this.sdp;
- } else {
- this.sdp = sdp_transform.parse(this.body || '');
-
- return this.sdp;
- }
- }
-
- toString() {
- let msg = `${this.method} ${this.ruri} SIP/2.0\r\n`;
-
- for (const headerName in this.headers) {
- if (Object.prototype.hasOwnProperty.call(this.headers, headerName)) {
- for (const headerValue of this.headers[headerName]) {
- msg += `${headerName}: ${headerValue}\r\n`;
- }
- }
- }
-
- for (const header of this.extraHeaders) {
- msg += `${header.trim()}\r\n`;
- }
-
- // Supported.
- const supported = [];
-
- switch (this.method) {
- case JsSIP_C.REGISTER: {
- supported.push('path', 'gruu');
- break;
- }
- case JsSIP_C.INVITE: {
- if (this.ua.configuration.session_timers) {
- supported.push('timer');
- }
- if (this.ua.contact.pub_gruu || this.ua.contact.temp_gruu) {
- supported.push('gruu');
- }
- supported.push('ice', 'replaces');
- break;
- }
- case JsSIP_C.UPDATE: {
- if (this.ua.configuration.session_timers) {
- supported.push('timer');
- }
- supported.push('ice');
- break;
- }
- }
-
- supported.push('outbound');
-
- const userAgent = this.ua.configuration.user_agent || JsSIP_C.USER_AGENT;
-
- // Allow.
- msg += `Allow: ${JsSIP_C.ALLOWED_METHODS}\r\n`;
- msg += `Supported: ${supported}\r\n`;
- msg += `User-Agent: ${userAgent}\r\n`;
-
- if (this.body) {
- const length = Utils.str_utf8_length(this.body);
-
- msg += `Content-Length: ${length}\r\n\r\n`;
- msg += this.body;
- } else {
- msg += 'Content-Length: 0\r\n\r\n';
- }
-
- return msg;
- }
-
- clone() {
- const request = new OutgoingRequest(this.method, this.ruri, this.ua);
-
- Object.keys(this.headers).forEach(function (name) {
- request.headers[name] = this.headers[name].slice();
- }, this);
-
- request.body = this.body;
- request.extraHeaders = Utils.cloneArray(this.extraHeaders);
- request.to = this.to;
- request.from = this.from;
- request.call_id = this.call_id;
- request.cseq = this.cseq;
-
- return request;
- }
-}
-
-class InitialOutgoingInviteRequest extends OutgoingRequest {
- constructor(ruri, ua, params, extraHeaders, body) {
- super(JsSIP_C.INVITE, ruri, ua, params, extraHeaders, body);
-
- this.transaction = null;
- }
-
- cancel(reason) {
- this.transaction.cancel(reason);
- }
-
- clone() {
- const request = new InitialOutgoingInviteRequest(this.ruri, this.ua);
-
- Object.keys(this.headers).forEach(function (name) {
- request.headers[name] = this.headers[name].slice();
- }, this);
-
- request.body = this.body;
- request.extraHeaders = Utils.cloneArray(this.extraHeaders);
- request.to = this.to;
- request.from = this.from;
- request.call_id = this.call_id;
- request.cseq = this.cseq;
-
- request.transaction = this.transaction;
-
- return request;
- }
-}
-
-class IncomingMessage {
- constructor() {
- this.data = null;
- this.headers = null;
- this.method = null;
- this.via = null;
- this.via_branch = null;
- this.call_id = null;
- this.cseq = null;
- this.from = null;
- this.from_tag = null;
- this.to = null;
- this.to_tag = null;
- this.body = null;
- this.sdp = null;
- }
-
- /**
- * Insert a header of the given name and value into the last position of the
- * header array.
- */
- addHeader(name, value) {
- const header = { raw: value };
-
- name = Utils.headerize(name);
-
- if (this.headers[name]) {
- this.headers[name].push(header);
- } else {
- this.headers[name] = [header];
- }
- }
-
- /**
- * Get the value of the given header name at the given position.
- */
- getHeader(name) {
- const header = this.headers[Utils.headerize(name)];
-
- if (header) {
- if (header[0]) {
- return header[0].raw;
- }
- } else {
- return;
- }
- }
-
- /**
- * Get the header/s of the given name.
- */
- getHeaders(name) {
- const headers = this.headers[Utils.headerize(name)];
- const result = [];
-
- if (!headers) {
- return [];
- }
-
- for (const header of headers) {
- result.push(header.raw);
- }
-
- return result;
- }
-
- /**
- * Verify the existence of the given header.
- */
- hasHeader(name) {
- return this.headers[Utils.headerize(name)] ? true : false;
- }
-
- /**
- * Parse the given header on the given index.
- * -param {String} name header name
- * -param {Number} [idx=0] header index
- * -returns {Object|undefined} Parsed header object, undefined if the header
- * is not present or in case of a parsing error.
- */
- parseHeader(name, idx = 0) {
- name = Utils.headerize(name);
-
- if (!this.headers[name]) {
- logger.debug(`header "${name}" not present`);
-
- return;
- } else if (idx >= this.headers[name].length) {
- logger.debug(`not so many "${name}" headers present`);
-
- return;
- }
-
- const header = this.headers[name][idx];
- const value = header.raw;
-
- if (header.parsed) {
- return header.parsed;
- }
-
- // Substitute '-' by '_' for grammar rule matching.
- const parsed = Grammar.parse(value, name.replace(/-/g, '_'));
-
- if (parsed === -1) {
- this.headers[name].splice(idx, 1); // delete from headers
- logger.debug(
- `error parsing "${name}" header field with value "${value}"`
- );
-
- return;
- } else {
- header.parsed = parsed;
-
- return parsed;
- }
- }
-
- /**
- * Message Header attribute selector. Alias of parseHeader.
- * -param {String} name header name
- * -param {Number} [idx=0] header index
- * -returns {Object|undefined} Parsed header object, undefined if the header
- * is not present or in case of a parsing error.
- *
- * -example
- * message.s('via',3).port
- */
- s(name, idx) {
- return this.parseHeader(name, idx);
- }
-
- /**
- * Replace the value of the given header by the value.
- * -param {String} name header name
- * -param {String} value header value
- */
- setHeader(name, value) {
- const header = { raw: value };
-
- this.headers[Utils.headerize(name)] = [header];
- }
-
- /**
- * Parse the current body as a SDP and store the resulting object
- * into this.sdp.
- * -param {Boolean} force: Parse even if this.sdp already exists.
- *
- * Returns this.sdp.
- */
- parseSDP(force) {
- if (!force && this.sdp) {
- return this.sdp;
- } else {
- this.sdp = sdp_transform.parse(this.body || '');
-
- return this.sdp;
- }
- }
-
- toString() {
- return this.data;
- }
-}
-
-class IncomingRequest extends IncomingMessage {
- constructor(ua) {
- super();
-
- this.ua = ua;
- this.headers = {};
- this.ruri = null;
- this.transport = null;
- this.server_transaction = null;
- }
-
- /**
- * Stateful reply.
- * -param {Number} code status code
- * -param {String} reason reason phrase
- * -param {Object} headers extra headers
- * -param {String} body body
- * -param {Function} [onSuccess] onSuccess callback
- * -param {Function} [onFailure] onFailure callback
- */
- reply(code, reason, extraHeaders, body, onSuccess, onFailure) {
- const supported = [];
- let to = this.getHeader('To');
-
- code = code || null;
- reason = reason || null;
-
- // Validate code and reason values.
- if (!code || code < 100 || code > 699) {
- throw new TypeError(`Invalid status_code: ${code}`);
- } else if (
- reason &&
- typeof reason !== 'string' &&
- !(reason instanceof String)
- ) {
- throw new TypeError(`Invalid reason_phrase: ${reason}`);
- }
-
- reason = reason || JsSIP_C.REASON_PHRASE[code] || '';
- extraHeaders = Utils.cloneArray(extraHeaders);
-
- if (this.ua.configuration.extra_headers) {
- extraHeaders = extraHeaders.concat(this.ua.configuration.extra_headers);
- }
-
- let response = `SIP/2.0 ${code} ${reason}\r\n`;
-
- if (this.method === JsSIP_C.INVITE && code > 100 && code <= 200) {
- const headers = this.getHeaders('record-route');
-
- for (const header of headers) {
- response += `Record-Route: ${header}\r\n`;
- }
- }
-
- const vias = this.getHeaders('via');
-
- for (const via of vias) {
- response += `Via: ${via}\r\n`;
- }
-
- if (!this.to_tag && code > 100) {
- to += `;tag=${Utils.newTag()}`;
- } else if (this.to_tag && !this.s('to').hasParam('tag')) {
- to += `;tag=${this.to_tag}`;
- }
-
- response += `To: ${to}\r\n`;
- response += `From: ${this.getHeader('From')}\r\n`;
- response += `Call-ID: ${this.call_id}\r\n`;
- response += `CSeq: ${this.cseq} ${this.method}\r\n`;
-
- for (const header of extraHeaders) {
- response += `${header.trim()}\r\n`;
- }
-
- // Supported.
- switch (this.method) {
- case JsSIP_C.INVITE: {
- if (this.ua.configuration.session_timers) {
- supported.push('timer');
- }
- if (this.ua.contact.pub_gruu || this.ua.contact.temp_gruu) {
- supported.push('gruu');
- }
- supported.push('ice', 'replaces');
- break;
- }
- case JsSIP_C.UPDATE: {
- if (this.ua.configuration.session_timers) {
- supported.push('timer');
- }
- if (body) {
- supported.push('ice');
- }
- supported.push('replaces');
- }
- }
-
- supported.push('outbound');
-
- // Allow and Accept.
- if (this.method === JsSIP_C.OPTIONS) {
- response += `Allow: ${JsSIP_C.ALLOWED_METHODS}\r\n`;
- response += `Accept: ${JsSIP_C.ACCEPTED_BODY_TYPES}\r\n`;
- } else if (code === 405) {
- response += `Allow: ${JsSIP_C.ALLOWED_METHODS}\r\n`;
- } else if (code === 415) {
- response += `Accept: ${JsSIP_C.ACCEPTED_BODY_TYPES}\r\n`;
- }
-
- response += `Supported: ${supported}\r\n`;
-
- if (body) {
- const length = Utils.str_utf8_length(body);
-
- response += 'Content-Type: application/sdp\r\n';
- response += `Content-Length: ${length}\r\n\r\n`;
- response += body;
- } else {
- response += `Content-Length: ${0}\r\n\r\n`;
- }
-
- this.server_transaction.receiveResponse(
- code,
- response,
- onSuccess,
- onFailure
- );
- }
-
- /**
- * Stateless reply.
- * -param {Number} code status code
- * -param {String} reason reason phrase
- */
- reply_sl(code = null, reason = null) {
- const vias = this.getHeaders('via');
-
- // Validate code and reason values.
- if (!code || code < 100 || code > 699) {
- throw new TypeError(`Invalid status_code: ${code}`);
- } else if (
- reason &&
- typeof reason !== 'string' &&
- !(reason instanceof String)
- ) {
- throw new TypeError(`Invalid reason_phrase: ${reason}`);
- }
-
- reason = reason || JsSIP_C.REASON_PHRASE[code] || '';
-
- let response = `SIP/2.0 ${code} ${reason}\r\n`;
-
- for (const via of vias) {
- response += `Via: ${via}\r\n`;
- }
-
- let to = this.getHeader('To');
-
- if (!this.to_tag && code > 100) {
- to += `;tag=${Utils.newTag()}`;
- } else if (this.to_tag && !this.s('to').hasParam('tag')) {
- to += `;tag=${this.to_tag}`;
- }
-
- response += `To: ${to}\r\n`;
- response += `From: ${this.getHeader('From')}\r\n`;
- response += `Call-ID: ${this.call_id}\r\n`;
- response += `CSeq: ${this.cseq} ${this.method}\r\n`;
-
- if (this.ua.configuration.extra_headers) {
- for (const header of this.ua.configuration.extra_headers) {
- response += `${header.trim()}\r\n`;
- }
- }
-
- response += `Content-Length: ${0}\r\n\r\n`;
-
- this.transport.send(response);
- }
-}
-
-class IncomingResponse extends IncomingMessage {
- constructor() {
- super();
-
- this.headers = {};
- this.status_code = null;
- this.reason_phrase = null;
- }
-}
-
-module.exports = {
- OutgoingRequest,
- InitialOutgoingInviteRequest,
- IncomingRequest,
- IncomingResponse,
-};
diff --git a/src/SIPMessage.ts b/src/SIPMessage.ts
new file mode 100644
index 0000000..7e632b7
--- /dev/null
+++ b/src/SIPMessage.ts
@@ -0,0 +1,1057 @@
+import type { EventEmitter } from 'events';
+
+import * as JsSIP_C from './Constants';
+import { Logger } from './Logger';
+import * as Grammar from './Grammar';
+import {
+ cloneArray,
+ createRandomToken,
+ headerize,
+ newTag,
+ str_utf8_length,
+} from './Utils';
+import NameAddrHeader = require('./NameAddrHeader');
+import URI = require('./URI');
+
+// NOTE: `sdp-transform` ships no type declarations and `@types/sdp-transform`
+// is not a dependency of this project, so it is consumed through a `require()`
+// narrowed to the single function used here.
+// eslint-disable-next-line @typescript-eslint/no-require-imports
+const sdp_transform = require('sdp-transform') as {
+ parse(sdp: string): SDP;
+};
+
+const logger = new Logger('SIPMessage');
+
+/**
+ * A single media description of a parsed SDP.
+ */
+export type SDPMediaDescription = {
+ type: string;
+ direction?: string;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ [key: string]: any;
+};
+
+/**
+ * An SDP as returned by `sdp-transform`'s `parse()`.
+ */
+export type SDP = {
+ media: SDPMediaDescription[];
+ direction?: string;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ [key: string]: any;
+};
+
+/**
+ * A header value as stored by an `OutgoingRequest`.
+ *
+ * NOTE: It is not just `string` because `Max-Forwards` is stored as the
+ * `JsSIP_C.MAX_FORWARDS` number.
+ */
+export type OutgoingHeaderValue = string | number;
+
+/**
+ * A header entry as stored by an `IncomingMessage`.
+ */
+export type IncomingHeader = {
+ raw: string;
+ parsed?: Grammar.Grammar;
+};
+
+/**
+ * Parameters that take priority over the `ua.configuration` ones.
+ */
+export type OutgoingRequestParams = {
+ cseq?: number;
+ call_id?: string;
+ from_uri?: URI;
+ from_tag?: string;
+ from_display_name?: string;
+ to_uri?: URI;
+ to_tag?: string;
+ to_display_name?: string;
+ route_set?: OutgoingHeaderValue[];
+};
+
+/*
+ * TODO: The `*Like` types below are structural views of modules that are still
+ * JavaScript. They are declared here so that this module stays a leaf of the
+ * TypeScript migration. The real classes satisfy them structurally, so each one
+ * must be replaced with a plain type import once its module is ported.
+ *
+ * Note that importing those modules only for their types does not create a
+ * runtime require cycle, since type imports are erased.
+ */
+
+/**
+ * The transport surface SIP messages depend on.
+ *
+ * TODO: Replace with `import type { Transport } from './Transport'` once
+ * `Transport` is ported to TypeScript.
+ */
+export interface TransportLike {
+ readonly sip_uri: string;
+
+ send(data: string): boolean;
+}
+
+/**
+ * The client transaction an outgoing INVITE is bound to.
+ *
+ * TODO: Replace with `import type { InviteClientTransaction } from
+ * './Transactions'` once `Transactions` is ported to TypeScript.
+ */
+export interface ClientTransactionLike {
+ cancel(reason?: string): void;
+}
+
+/**
+ * The server transaction an incoming request is bound to.
+ *
+ * TODO: Replace with the server transaction types from './Transactions' once
+ * that module is ported to TypeScript.
+ */
+export interface ServerTransactionLike extends EventEmitter {
+ state: number;
+
+ receiveResponse(
+ code: number,
+ response: string,
+ onSuccess?: () => void,
+ onFailure?: () => void
+ ): void;
+}
+
+/**
+ * The `UA` surface SIP messages depend on.
+ *
+ * TODO: Replace with `import type { UA } from './UA'` once `UA` is ported to
+ * TypeScript. Note that `UA.d.ts` currently declares neither `configuration`
+ * nor `transport`, even though `UA` exposes both as public getters, so that
+ * gap must be closed first.
+ *
+ * TODO: The `configuration` shape below is the *resolved* configuration, which
+ * is not the same as the `UAConfiguration` input type declared in `UA.d.ts`.
+ * It should become a type exported by `Config` once that module is ported.
+ */
+export interface UALike {
+ readonly configuration: {
+ uri: URI;
+ display_name?: string | null;
+ jssip_id: string;
+ session_timers?: boolean;
+ use_preloaded_route?: boolean;
+ user_agent?: string | null;
+ extra_headers?: string[] | null;
+ };
+ readonly contact: {
+ pub_gruu?: string | null;
+ temp_gruu?: string | null;
+ };
+ readonly transport: TransportLike;
+}
+
+/**
+ * -param {String} method request method
+ * -param {String} ruri request uri
+ * -param {UA} ua
+ * -param {Object} params parameters that will have priority over ua.configuration parameters:
+ * <br>
+ * - cseq, call_id, from_tag, from_uri, from_display_name, to_uri, to_tag, route_set
+ * -param {Object} [headers] extra headers
+ * -param {String} [body]
+ */
+export class OutgoingRequest {
+ ua: UALike;
+ headers: Record<string, OutgoingHeaderValue[]>;
+ method: string;
+ ruri: URI;
+ body?: string;
+ extraHeaders: string[];
+ to: NameAddrHeader;
+ from: NameAddrHeader;
+ call_id: string;
+ cseq: number;
+
+ /* NOTE: `declare`d (and so not emitted) because the original implementation
+ * does not create these own properties in the constructor. `sdp` is created
+ * by `parseSDP()` and `transaction` is assigned by `Transactions.js` when
+ * the request is sent.
+ */
+ declare sdp?: SDP;
+ declare transaction?: ClientTransactionLike | null;
+
+ constructor(
+ method: string,
+ ruri: URI,
+ ua: UALike,
+ params?: OutgoingRequestParams | null,
+ extraHeaders?: string[] | null,
+ body?: string
+ ) {
+ // Mandatory parameters check.
+ if (!method || !ruri || !ua) {
+ throw new TypeError(
+ 'missing or invalid "method", "ruri" or "ua" parameter'
+ );
+ }
+
+ params = params || {};
+
+ this.ua = ua;
+ this.headers = {};
+ this.method = method;
+ this.ruri = ruri;
+ this.body = body;
+ this.extraHeaders = cloneArray(extraHeaders);
+
+ if (this.ua.configuration.extra_headers) {
+ this.extraHeaders = this.extraHeaders.concat(
+ this.ua.configuration.extra_headers
+ );
+ }
+
+ // Fill the Common SIP Request Headers.
+
+ // Route.
+ if (params.route_set) {
+ this.setHeader('route', params.route_set);
+ } else if (ua.configuration.use_preloaded_route) {
+ this.setHeader('route', `<${ua.transport.sip_uri};lr>`);
+ }
+
+ // Via.
+ // Empty Via header. Will be filled by the client transaction.
+ this.setHeader('via', '');
+
+ // Max-Forwards.
+ this.setHeader('max-forwards', JsSIP_C.MAX_FORWARDS);
+
+ // To
+ const to_uri = params.to_uri || ruri;
+ const to_params = params.to_tag ? { tag: params.to_tag } : null;
+ // NOTE: `NameAddrHeader` tolerates a `null` display name (its setter
+ // stores it as is), so the cast keeps the original behaviour.
+ const to_display_name = (
+ typeof params.to_display_name !== 'undefined'
+ ? params.to_display_name
+ : null
+ ) as string | undefined;
+
+ this.to = new NameAddrHeader(to_uri, to_display_name, to_params);
+ this.setHeader('to', this.to.toString());
+
+ // From.
+ const from_uri = params.from_uri || ua.configuration.uri;
+ const from_params = { tag: params.from_tag || newTag() };
+ let display_name: string | null;
+
+ if (typeof params.from_display_name !== 'undefined') {
+ display_name = params.from_display_name;
+ } else if (ua.configuration.display_name) {
+ display_name = ua.configuration.display_name;
+ } else {
+ display_name = null;
+ }
+
+ this.from = new NameAddrHeader(
+ from_uri,
+ display_name as string | undefined,
+ from_params
+ );
+ this.setHeader('from', this.from.toString());
+
+ // Call-ID.
+ const call_id =
+ params.call_id || ua.configuration.jssip_id + createRandomToken(15);
+
+ this.call_id = call_id;
+ this.setHeader('call-id', call_id);
+
+ // CSeq.
+ const cseq = params.cseq || Math.floor(Math.random() * 10000);
+
+ this.cseq = cseq;
+ this.setHeader('cseq', `${cseq} ${method}`);
+ }
+
+ /**
+ * Replace the the given header by the given value.
+ * -param {String} name header name
+ * -param {String | Array} value header value
+ */
+ setHeader(
+ name: string,
+ value: OutgoingHeaderValue | OutgoingHeaderValue[]
+ ): void {
+ // Remove the header from extraHeaders if present.
+ const regexp = new RegExp(`^\\s*${name}\\s*:`, 'i');
+
+ for (let idx = 0; idx < this.extraHeaders.length; idx++) {
+ if (regexp.test(this.extraHeaders[idx]!)) {
+ this.extraHeaders.splice(idx, 1);
+ }
+ }
+
+ this.headers[headerize(name)] = Array.isArray(value) ? value : [value];
+ }
+
+ /**
+ * Get the value of the given header name at the given position.
+ * -param {String} name header name
+ * -returns {String|undefined} Returns the specified header, null if header doesn't exist.
+ */
+ getHeader(name: string): OutgoingHeaderValue | undefined {
+ const headers = this.headers[headerize(name)];
+
+ if (headers) {
+ if (headers[0]) {
+ return headers[0];
+ }
+ } else {
+ const regexp = new RegExp(`^\\s*${name}\\s*:`, 'i');
+
+ for (const header of this.extraHeaders) {
+ if (regexp.test(header)) {
+ return header.substring(header.indexOf(':') + 1).trim();
+ }
+ }
+ }
+
+ return;
+ }
+
+ /**
+ * Get the header/s of the given name.
+ * -param {String} name header name
+ * -returns {Array} Array with all the headers of the specified name.
+ */
+ getHeaders(name: string): OutgoingHeaderValue[] {
+ const headers = this.headers[headerize(name)];
+ const result: OutgoingHeaderValue[] = [];
+
+ if (headers) {
+ for (const header of headers) {
+ result.push(header);
+ }
+
+ return result;
+ } else {
+ const regexp = new RegExp(`^\\s*${name}\\s*:`, 'i');
+
+ for (const header of this.extraHeaders) {
+ if (regexp.test(header)) {
+ result.push(header.substring(header.indexOf(':') + 1).trim());
+ }
+ }
+
+ return result;
+ }
+ }
+
+ /**
+ * Verify the existence of the given header.
+ * -param {String} name header name
+ * -returns {boolean} true if header with given name exists, false otherwise
+ */
+ hasHeader(name: string): boolean {
+ if (this.headers[headerize(name)]) {
+ return true;
+ } else {
+ const regexp = new RegExp(`^\\s*${name}\\s*:`, 'i');
+
+ for (const header of this.extraHeaders) {
+ if (regexp.test(header)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Parse the current body as a SDP and store the resulting object
+ * into this.sdp.
+ * -param {Boolean} force: Parse even if this.sdp already exists.
+ *
+ * Returns this.sdp.
+ */
+ parseSDP(force?: boolean): SDP {
+ if (!force && this.sdp) {
+ return this.sdp;
+ } else {
+ this.sdp = sdp_transform.parse(this.body || '');
+
+ return this.sdp;
+ }
+ }
+
+ toString(): string {
+ let msg = `${this.method} ${this.ruri} SIP/2.0\r\n`;
+
+ for (const headerName in this.headers) {
+ if (Object.prototype.hasOwnProperty.call(this.headers, headerName)) {
+ for (const headerValue of this.headers[headerName]!) {
+ msg += `${headerName}: ${headerValue}\r\n`;
+ }
+ }
+ }
+
+ for (const header of this.extraHeaders) {
+ msg += `${header.trim()}\r\n`;
+ }
+
+ // Supported.
+ const supported: string[] = [];
+
+ switch (this.method) {
+ case JsSIP_C.REGISTER: {
+ supported.push('path', 'gruu');
+ break;
+ }
+ case JsSIP_C.INVITE: {
+ if (this.ua.configuration.session_timers) {
+ supported.push('timer');
+ }
+ if (this.ua.contact.pub_gruu || this.ua.contact.temp_gruu) {
+ supported.push('gruu');
+ }
+ supported.push('ice', 'replaces');
+ break;
+ }
+ case JsSIP_C.UPDATE: {
+ if (this.ua.configuration.session_timers) {
+ supported.push('timer');
+ }
+ supported.push('ice');
+ break;
+ }
+ }
+
+ supported.push('outbound');
+
+ const userAgent = this.ua.configuration.user_agent || JsSIP_C.USER_AGENT;
+
+ // Allow.
+ msg += `Allow: ${JsSIP_C.ALLOWED_METHODS}\r\n`;
+ msg += `Supported: ${supported}\r\n`;
+ msg += `User-Agent: ${userAgent}\r\n`;
+
+ if (this.body) {
+ const length = str_utf8_length(this.body);
+
+ msg += `Content-Length: ${length}\r\n\r\n`;
+ msg += this.body;
+ } else {
+ msg += 'Content-Length: 0\r\n\r\n';
+ }
+
+ return msg;
+ }
+
+ clone(): OutgoingRequest {
+ const request = new OutgoingRequest(this.method, this.ruri, this.ua);
+
+ // NOTE: An arrow function is used instead of the original `function` plus
+ // `thisArg` so that `this` is properly typed. Iteration and assignment
+ // order are unchanged.
+ Object.keys(this.headers).forEach(name => {
+ request.headers[name] = this.headers[name]!.slice();
+ });
+
+ request.body = this.body;
+ request.extraHeaders = cloneArray(this.extraHeaders);
+ request.to = this.to;
+ request.from = this.from;
+ request.call_id = this.call_id;
+ request.cseq = this.cseq;
+
+ return request;
+ }
+}
+
+export class InitialOutgoingInviteRequest extends OutgoingRequest {
+ /* NOTE: Unlike in `OutgoingRequest`, this is a real (emitted) field because
+ * the original implementation does assign it in the constructor.
+ */
+ override transaction: ClientTransactionLike | null = null;
+
+ constructor(
+ ruri: URI,
+ ua: UALike,
+ params?: OutgoingRequestParams | null,
+ extraHeaders?: string[] | null,
+ body?: string
+ ) {
+ super(JsSIP_C.INVITE, ruri, ua, params, extraHeaders, body);
+ }
+
+ cancel(reason?: string): void {
+ this.transaction!.cancel(reason);
+ }
+
+ override clone(): InitialOutgoingInviteRequest {
+ const request = new InitialOutgoingInviteRequest(this.ruri, this.ua);
+
+ Object.keys(this.headers).forEach(name => {
+ request.headers[name] = this.headers[name]!.slice();
+ });
+
+ request.body = this.body;
+ request.extraHeaders = cloneArray(this.extraHeaders);
+ request.to = this.to;
+ request.from = this.from;
+ request.call_id = this.call_id;
+ request.cseq = this.cseq;
+
+ request.transaction = this.transaction;
+
+ return request;
+ }
+}
+
+export type IncomingHeaders = Record<string, IncomingHeader[]>;
+
+/**
+ * Everything needed to build an `IncomingMessage`.
+ */
+export type IncomingMessageParams = {
+ data: string;
+ headers: IncomingHeaders;
+ method: string;
+ // Undefined when the corresponding header field is missing or unparseable.
+ via?: Grammar.ViaHeader;
+ via_branch?: string;
+ call_id?: string;
+ cseq?: number;
+ from: NameAddrHeader;
+ /* NOTE: `null` when the tag parameter is present but valueless (`;tag`),
+ * `undefined` when it is absent altogether.
+ */
+ from_tag?: string | null;
+ to: NameAddrHeader;
+ to_tag?: string | null;
+ body: string;
+ session_expires?: number;
+ session_expires_refresher?: string;
+ refer_to?: Grammar.ReferToHeader;
+ replaces?: Grammar.ReplacesHeader;
+ event?: Grammar.EventHeader;
+};
+
+export type IncomingRequestParams = IncomingMessageParams & {
+ ua: UALike;
+ ruri: URI;
+};
+
+export type IncomingResponseParams = IncomingMessageParams & {
+ status_code: number;
+ reason_phrase: string;
+};
+
+export class IncomingMessage {
+ /* Header store operations.
+ *
+ * They are static so that `IncomingMessageBuilder` can collect and parse
+ * header fields before any `IncomingMessage` exists. The instance methods
+ * of the same name delegate to them.
+ */
+
+ /**
+ * Insert a header of the given name and value into the last position of the
+ * given header store.
+ */
+ static addHeader(
+ headers: IncomingHeaders,
+ name: string,
+ value: string
+ ): void {
+ const header: IncomingHeader = { raw: value };
+
+ name = headerize(name);
+
+ const existing = headers[name];
+
+ if (existing) {
+ existing.push(header);
+ } else {
+ headers[name] = [header];
+ }
+ }
+
+ /**
+ * Replace the value of the given header by the value.
+ */
+ static setHeader(
+ headers: IncomingHeaders,
+ name: string,
+ value: string
+ ): void {
+ headers[headerize(name)] = [{ raw: value }];
+ }
+
+ /**
+ * Get the value of the given header name at the first position, or undefined
+ * if the header field is not present.
+ */
+ static getHeader(headers: IncomingHeaders, name: string): string | undefined {
+ const header = headers[headerize(name)];
+
+ if (header?.[0]) {
+ return header[0].raw;
+ }
+
+ return undefined;
+ }
+
+ /**
+ * Get the header/s of the given name.
+ */
+ static getHeaders(headers: IncomingHeaders, name: string): string[] {
+ const found = headers[headerize(name)];
+ const result: string[] = [];
+
+ if (!found) {
+ return [];
+ }
+
+ for (const header of found) {
+ result.push(header.raw);
+ }
+
+ return result;
+ }
+
+ /**
+ * Verify the existence of the given header.
+ */
+ static hasHeader(headers: IncomingHeaders, name: string): boolean {
+ return headers[headerize(name)] ? true : false;
+ }
+
+ /**
+ * Parse the given header on the given index, caching the result.
+ */
+ /**
+ * Parse the given header on the given index, caching the result. Returns
+ * undefined if the header field is not present or cannot be parsed.
+ */
+ static parseHeader<T = unknown>(
+ headers: IncomingHeaders,
+ name: string,
+ idx = 0
+ ): T | undefined {
+ name = headerize(name);
+
+ const found = headers[name];
+
+ if (!found) {
+ logger.debug(`header "${name}" not present`);
+
+ return undefined;
+ } else if (idx >= found.length) {
+ logger.debug(`not so many "${name}" headers present`);
+
+ return undefined;
+ }
+
+ const header = found[idx]!;
+ const value = header.raw;
+
+ if (header.parsed) {
+ return header.parsed as T;
+ }
+
+ // Substitute '-' by '_' for grammar rule matching.
+ const parsed = Grammar.parse(value, name.replace(/-/g, '_'));
+
+ if (parsed === -1) {
+ found.splice(idx, 1); // delete from headers
+ logger.debug(
+ `error parsing "${name}" header field with value "${value}"`
+ );
+
+ return undefined;
+ } else {
+ header.parsed = parsed;
+
+ return parsed as T;
+ }
+ }
+
+ data: string;
+ headers: IncomingHeaders;
+ method: string;
+ via?: Grammar.ViaHeader;
+ via_branch?: string;
+ call_id?: string;
+ cseq?: number;
+ from: NameAddrHeader;
+ from_tag?: string | null;
+ to: NameAddrHeader;
+ to_tag?: string | null;
+ body: string;
+ sdp: SDP | null;
+
+ // Only set when the corresponding header field is present.
+ session_expires?: number;
+ session_expires_refresher?: string;
+ refer_to?: Grammar.ReferToHeader;
+ replaces?: Grammar.ReplacesHeader;
+ event?: Grammar.EventHeader;
+
+ constructor({
+ data,
+ headers,
+ method,
+ via,
+ via_branch,
+ call_id,
+ cseq,
+ from,
+ from_tag,
+ to,
+ to_tag,
+ body,
+ session_expires,
+ session_expires_refresher,
+ refer_to,
+ replaces,
+ event,
+ }: IncomingMessageParams) {
+ this.data = data;
+ this.headers = headers;
+ this.method = method;
+ this.via = via;
+ this.via_branch = via_branch;
+ this.call_id = call_id;
+ this.cseq = cseq;
+ this.from = from;
+ this.from_tag = from_tag;
+ this.to = to;
+ this.to_tag = to_tag;
+ this.body = body;
+ this.sdp = null;
+ this.session_expires = session_expires;
+ this.session_expires_refresher = session_expires_refresher;
+ this.refer_to = refer_to;
+ this.replaces = replaces;
+ this.event = event;
+ }
+
+ /**
+ * Insert a header of the given name and value into the last position of the
+ * header array.
+ */
+ addHeader(name: string, value: string): void {
+ IncomingMessage.addHeader(this.headers, name, value);
+ }
+
+ /**
+ * Get the value of the given header name at the given position.
+ */
+ getHeader(name: string): string | undefined {
+ return IncomingMessage.getHeader(this.headers, name);
+ }
+
+ /**
+ * Get the header/s of the given name.
+ */
+ getHeaders(name: string): string[] {
+ return IncomingMessage.getHeaders(this.headers, name);
+ }
+
+ /**
+ * Verify the existence of the given header.
+ */
+ hasHeader(name: string): boolean {
+ return IncomingMessage.hasHeader(this.headers, name);
+ }
+
+ /**
+ * Parse the given header on the given index.
+ * -param {String} name header name
+ * -param {Number} [idx=0] header index
+ * -returns {Object|undefined} Parsed header object, undefined if the header
+ * is not present or in case of a parsing error.
+ */
+ parseHeader<T = unknown>(name: string, idx = 0): T | undefined {
+ return IncomingMessage.parseHeader<T>(this.headers, name, idx);
+ }
+
+ /**
+ * Message Header attribute selector. Alias of parseHeader.
+ * -param {String} name header name
+ * -param {Number} [idx=0] header index
+ * -returns {Object|undefined} Parsed header object, undefined if the header
+ * is not present or in case of a parsing error.
+ *
+ * -example
+ * message.s('via',3).port
+ */
+ s<T = unknown>(name: string, idx?: number): T | undefined {
+ return this.parseHeader<T>(name, idx);
+ }
+
+ /**
+ * Replace the value of the given header by the value.
+ * -param {String} name header name
+ * -param {String} value header value
+ */
+ setHeader(name: string, value: string): void {
+ IncomingMessage.setHeader(this.headers, name, value);
+ }
+
+ /**
+ * Parse the current body as a SDP and store the resulting object
+ * into this.sdp.
+ * -param {Boolean} force: Parse even if this.sdp already exists.
+ *
+ * Returns this.sdp.
+ */
+ parseSDP(force?: boolean): SDP {
+ if (!force && this.sdp) {
+ return this.sdp;
+ } else {
+ this.sdp = sdp_transform.parse(this.body || '');
+
+ return this.sdp;
+ }
+ }
+
+ toString(): string {
+ return this.data;
+ }
+}
+
+export class IncomingRequest extends IncomingMessage {
+ ua: UALike;
+ ruri: URI;
+ transport: TransportLike | null;
+ server_transaction: ServerTransactionLike | null;
+
+ constructor({ ua, ruri, ...params }: IncomingRequestParams) {
+ super(params);
+
+ this.ua = ua;
+ this.ruri = ruri;
+ this.transport = null;
+ this.server_transaction = null;
+ }
+
+ /**
+ * Stateful reply.
+ * -param {Number} code status code
+ * -param {String} reason reason phrase
+ * -param {Object} headers extra headers
+ * -param {String} body body
+ * -param {Function} [onSuccess] onSuccess callback
+ * -param {Function} [onFailure] onFailure callback
+ */
+ reply(
+ code: number,
+ reason?: string | null,
+ extraHeaders?: string[] | null,
+ body?: string | null,
+ onSuccess?: () => void,
+ onFailure?: () => void
+ ): void {
+ const supported: string[] = [];
+ /* NOTE: `getHeader()` may return undefined, but a To header field is
+ * guaranteed here: `IncomingMessageBuilder` refuses to build a message
+ * whose To header field is missing or unparseable.
+ */
+ let to = this.getHeader('To');
+
+ // NOTE: `code` is normalized into a local (instead of onto the parameter
+ // itself) so that the `TypeError` message below is identical to the one
+ // of the original implementation.
+ const status_code: number | null = code || null;
+
+ reason = reason || null;
+
+ // Validate code and reason values.
+ if (!status_code || status_code < 100 || status_code > 699) {
+ throw new TypeError(`Invalid status_code: ${status_code}`);
+ } else if (
+ reason &&
+ typeof reason !== 'string' &&
+ // NOTE: `reason` is statically a string here, but the runtime check is
+ // kept as is for JavaScript callers passing a `String` object.
+ !((reason as unknown) instanceof String)
+ ) {
+ throw new TypeError(`Invalid reason_phrase: ${reason}`);
+ }
+
+ reason = reason || JsSIP_C.REASON_PHRASE[status_code] || '';
+ extraHeaders = cloneArray(extraHeaders);
+
+ if (this.ua.configuration.extra_headers) {
+ extraHeaders = extraHeaders.concat(this.ua.configuration.extra_headers);
+ }
+
+ let response = `SIP/2.0 ${status_code} ${reason}\r\n`;
+
+ if (
+ this.method === JsSIP_C.INVITE &&
+ status_code > 100 &&
+ status_code <= 200
+ ) {
+ const headers = this.getHeaders('record-route');
+
+ for (const header of headers) {
+ response += `Record-Route: ${header}\r\n`;
+ }
+ }
+
+ const vias = this.getHeaders('via');
+
+ for (const via of vias) {
+ response += `Via: ${via}\r\n`;
+ }
+
+ if (!this.to_tag && status_code > 100) {
+ to += `;tag=${newTag()}`;
+ } else if (this.to_tag && !this.to.hasParam('tag')) {
+ to += `;tag=${this.to_tag}`;
+ }
+
+ response += `To: ${to}\r\n`;
+ response += `From: ${this.getHeader('From')}\r\n`;
+ response += `Call-ID: ${this.call_id}\r\n`;
+ response += `CSeq: ${this.cseq} ${this.method}\r\n`;
+
+ for (const header of extraHeaders) {
+ response += `${header.trim()}\r\n`;
+ }
+
+ // Supported.
+ switch (this.method) {
+ case JsSIP_C.INVITE: {
+ if (this.ua.configuration.session_timers) {
+ supported.push('timer');
+ }
+ if (this.ua.contact.pub_gruu || this.ua.contact.temp_gruu) {
+ supported.push('gruu');
+ }
+ supported.push('ice', 'replaces');
+ break;
+ }
+ case JsSIP_C.UPDATE: {
+ if (this.ua.configuration.session_timers) {
+ supported.push('timer');
+ }
+ if (body) {
+ supported.push('ice');
+ }
+ supported.push('replaces');
+ }
+ }
+
+ supported.push('outbound');
+
+ // Allow and Accept.
+ if (this.method === JsSIP_C.OPTIONS) {
+ response += `Allow: ${JsSIP_C.ALLOWED_METHODS}\r\n`;
+ response += `Accept: ${JsSIP_C.ACCEPTED_BODY_TYPES}\r\n`;
+ } else if (status_code === 405) {
+ response += `Allow: ${JsSIP_C.ALLOWED_METHODS}\r\n`;
+ } else if (status_code === 415) {
+ response += `Accept: ${JsSIP_C.ACCEPTED_BODY_TYPES}\r\n`;
+ }
+
+ response += `Supported: ${supported}\r\n`;
+
+ if (body) {
+ const length = str_utf8_length(body);
+
+ response += 'Content-Type: application/sdp\r\n';
+ response += `Content-Length: ${length}\r\n\r\n`;
+ response += body;
+ } else {
+ response += `Content-Length: ${0}\r\n\r\n`;
+ }
+
+ this.server_transaction!.receiveResponse(
+ status_code,
+ response,
+ onSuccess,
+ onFailure
+ );
+ }
+
+ /**
+ * Stateless reply.
+ * -param {Number} code status code
+ * -param {String} reason reason phrase
+ */
+ reply_sl(code: number | null = null, reason: string | null = null): void {
+ const vias = this.getHeaders('via');
+
+ // Validate code and reason values.
+ if (!code || code < 100 || code > 699) {
+ throw new TypeError(`Invalid status_code: ${code}`);
+ } else if (
+ reason &&
+ typeof reason !== 'string' &&
+ // NOTE: `reason` is statically a string here, but the runtime check is
+ // kept as is for JavaScript callers passing a `String` object.
+ !((reason as unknown) instanceof String)
+ ) {
+ throw new TypeError(`Invalid reason_phrase: ${reason}`);
+ }
+
+ reason = reason || JsSIP_C.REASON_PHRASE[code] || '';
+
+ let response = `SIP/2.0 ${code} ${reason}\r\n`;
+
+ for (const via of vias) {
+ response += `Via: ${via}\r\n`;
+ }
+
+ /* NOTE: `getHeader()` may return undefined, but a To header field is
+ * guaranteed here: `IncomingMessageBuilder` refuses to build a message
+ * whose To header field is missing or unparseable.
+ */
+ let to = this.getHeader('To');
+
+ if (!this.to_tag && code > 100) {
+ to += `;tag=${newTag()}`;
+ } else if (this.to_tag && !this.to.hasParam('tag')) {
+ to += `;tag=${this.to_tag}`;
+ }
+
+ response += `To: ${to}\r\n`;
+ response += `From: ${this.getHeader('From')}\r\n`;
+ response += `Call-ID: ${this.call_id}\r\n`;
+ response += `CSeq: ${this.cseq} ${this.method}\r\n`;
+
+ if (this.ua.configuration.extra_headers) {
+ for (const header of this.ua.configuration.extra_headers) {
+ response += `${header.trim()}\r\n`;
+ }
+ }
+
+ response += `Content-Length: ${0}\r\n\r\n`;
+
+ this.transport!.send(response);
+ }
+}
+
+export class IncomingResponse extends IncomingMessage {
+ status_code: number;
+ reason_phrase: string;
+
+ constructor({
+ status_code,
+ reason_phrase,
+ ...params
+ }: IncomingResponseParams) {
+ super(params);
+
+ this.status_code = status_code;
+ this.reason_phrase = reason_phrase;
+ }
+}
diff --git a/src/URI.ts b/src/URI.ts
index 371d097..e3ebb2f 100644
--- a/src/URI.ts
+++ b/src/URI.ts
@@ -27,9 +27,9 @@ class URI {
}
#scheme: string;
- #user: string | undefined;
+ #user?: string;
#host: string;
- #port: number | null | undefined;
+ #port?: number | null;
#parameters: URI.Parameters;
#headers: URI.Headers;
diff --git a/src/test/test-DigestAuthentication.ts b/src/test/test-DigestAuthentication.ts
index 3acad50..360c207 100644
--- a/src/test/test-DigestAuthentication.ts
+++ b/src/test/test-DigestAuthentication.ts
@@ -20,7 +20,6 @@ describe('DigestAuthentication', () => {
realm: 'testrealm@host.com',
nonce: 'dcd98b7102dd2f0e8b11d0f600bfb0c093',
opaque: '5ccc069c403ebaf9f0171e9517f40e41',
- stale: null,
qop: 'auth',
};
@@ -44,9 +43,6 @@ describe('DigestAuthentication', () => {
algorithm: 'MD5',
realm: 'testrealm@host.com',
nonce: '5a071f75353f667787615249c62dcc7b15a4828f',
- opaque: null,
- stale: null,
- qop: null,
};
const digest = new DigestAuthentication(credentials);
@@ -70,8 +66,6 @@ describe('DigestAuthentication', () => {
algorithm: 'MD5',
realm: 'testrealm@host.com',
nonce: '5a071f75353f667787615249c62dcc7b15a4828f',
- opaque: null,
- stale: null,
qop: 'auth',
};
@@ -96,8 +90,6 @@ describe('DigestAuthentication', () => {
algorithm: 'MD5',
realm: 'testrealm@host.com',
nonce: '5a071f75353f667787615249c62dcc7b15a4828f',
- opaque: null,
- stale: null,
qop: 'auth-int',
};
@@ -123,8 +115,6 @@ describe('DigestAuthentication', () => {
algorithm: 'MD5',
realm: 'testrealm@host.com',
nonce: '5a071f75353f667787615249c62dcc7b15a4828f',
- opaque: null,
- stale: null,
qop: 'auth-int',
};