Commit 3cf59dc for jssip.net
commit 3cf59dc7cb0ae47ea8180b912ca3e5b38e0b3f86
Author: José Luis Millán <jmillan@aliax.net>
Date: Tue Oct 20 12:21:52 2026 +0200
TS: move Utils, URI, NameAddress, Socket, WebSocketInterface, DigestAuthentication (#984)
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 96fed9a..d8122a8 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -135,6 +135,10 @@ const config = tsEslint.config(
],
'@typescript-eslint/prefer-function-type': 0,
'@typescript-eslint/dot-notation': 0,
+ // `import X = require('./X')` is the only way to consume a module that
+ // uses `export =` while `esModuleInterop` is disabled. Bare `require()`
+ // calls remain forbidden.
+ '@typescript-eslint/no-require-imports': [2, { allowAsImport: true }],
'@typescript-eslint/no-unused-vars': [
2,
{
diff --git a/src/JsSIP.d.ts b/src/JsSIP.d.ts
index e9a896e..fbf377e 100644
--- a/src/JsSIP.d.ts
+++ b/src/JsSIP.d.ts
@@ -7,11 +7,13 @@ import * as Utils from './Utils';
export { C, Exceptions, Grammar, Utils };
+import URI = require('./URI');
+import NameAddrHeader = require('./NameAddrHeader');
+
export { UA } from './UA';
-export { URI } from './URI';
-export { NameAddrHeader } from './NameAddrHeader';
+export { URI, NameAddrHeader };
export { WebSocketInterface } from './WebSocketInterface';
-export { Socket, WeightedSocket } from './Socket';
+export type { Socket, WeightedSocket } from './Socket';
export const debug: Debug;
export const name: string;
diff --git a/src/JsSIP.js b/src/JsSIP.js
index 39481d1..ccd80dd 100644
--- a/src/JsSIP.js
+++ b/src/JsSIP.js
@@ -6,7 +6,7 @@ const UA = require('./UA');
const URI = require('./URI');
const NameAddrHeader = require('./NameAddrHeader');
const Grammar = require('./Grammar');
-const WebSocketInterface = require('./WebSocketInterface');
+const { WebSocketInterface } = require('./WebSocketInterface');
const debug = require('debug')('JsSIP');
const RTCSession = require('./RTCSession');
diff --git a/src/Message.d.ts b/src/Message.d.ts
index e0cc24b..7b18260 100644
--- a/src/Message.d.ts
+++ b/src/Message.d.ts
@@ -8,7 +8,7 @@ import {
TerminateOptions,
} from './RTCSession';
import { IncomingResponse } from './SIPMessage';
-import { NameAddrHeader } from './NameAddrHeader';
+import NameAddrHeader = require('./NameAddrHeader');
import { causes } from './Constants';
export interface AcceptOptions extends ExtraHeaders {
diff --git a/src/NameAddrHeader.d.ts b/src/NameAddrHeader.d.ts
deleted file mode 100644
index c897ae4..0000000
--- a/src/NameAddrHeader.d.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { Parameters, URI } from './URI';
-import { Grammar } from './Grammar';
-
-export class NameAddrHeader {
- get display_name(): string;
- set display_name(value: string);
-
- get uri(): URI;
-
- constructor(uri: URI, display_name?: string, parameters?: Parameters);
-
- setParam(key: string, value?: string | number | null): void;
-
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- getParam<T = any>(key: string): T;
-
- hasParam(key: string): boolean;
-
- deleteParam(key: string): void;
-
- clearParams(): void;
-
- clone(): this;
-
- toString(): string;
-
- static parse(uri: string): Grammar | undefined;
-}
diff --git a/src/NameAddrHeader.js b/src/NameAddrHeader.js
deleted file mode 100644
index 8119331..0000000
--- a/src/NameAddrHeader.js
+++ /dev/null
@@ -1,118 +0,0 @@
-const URI = require('./URI');
-const Grammar = require('./Grammar');
-
-module.exports = class NameAddrHeader {
- /**
- * Parse the given string and returns a NameAddrHeader instance or undefined if
- * it is an invalid NameAddrHeader.
- */
- static parse(name_addr_header) {
- name_addr_header = Grammar.parse(name_addr_header, 'Name_Addr_Header');
-
- if (name_addr_header !== -1) {
- return name_addr_header;
- } else {
- return undefined;
- }
- }
-
- constructor(uri, display_name, parameters) {
- // Checks.
- if (!uri || !(uri instanceof URI)) {
- throw new TypeError('missing or invalid "uri" parameter');
- }
-
- // Initialize parameters.
- this._uri = uri;
- this._parameters = {};
- this.display_name = display_name;
-
- for (const param in parameters) {
- if (Object.prototype.hasOwnProperty.call(parameters, param)) {
- this.setParam(param, parameters[param]);
- }
- }
- }
-
- get uri() {
- return this._uri;
- }
-
- get display_name() {
- return this._display_name;
- }
-
- set display_name(value) {
- this._display_name = value === 0 ? '0' : value;
- }
-
- setParam(key, value) {
- if (key) {
- this._parameters[key.toLowerCase()] =
- typeof value === 'undefined' || value === null
- ? null
- : value.toString();
- }
- }
-
- getParam(key) {
- if (key) {
- return this._parameters[key.toLowerCase()];
- }
- }
-
- hasParam(key) {
- if (key) {
- return (
- (this._parameters.hasOwnProperty(key.toLowerCase()) && true) || false
- );
- }
- }
-
- deleteParam(parameter) {
- parameter = parameter.toLowerCase();
- if (this._parameters.hasOwnProperty(parameter)) {
- const value = this._parameters[parameter];
-
- delete this._parameters[parameter];
-
- return value;
- }
- }
-
- clearParams() {
- this._parameters = {};
- }
-
- clone() {
- return new NameAddrHeader(
- this._uri.clone(),
- this._display_name,
- JSON.parse(JSON.stringify(this._parameters))
- );
- }
-
- _quote(str) {
- return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
- }
-
- toString() {
- let body = this._display_name
- ? `"${this._quote(this._display_name)}" `
- : '';
-
- body += `<${this._uri.toString()}>`;
-
- for (const parameter in this._parameters) {
- if (Object.prototype.hasOwnProperty.call(this._parameters, parameter)) {
- body += `;${parameter}`;
-
- if (this._parameters[parameter] !== null) {
- body += `=${this._parameters[parameter]}`;
- }
- }
- }
-
- return body;
- }
-};
diff --git a/src/NameAddrHeader.ts b/src/NameAddrHeader.ts
new file mode 100644
index 0000000..2837b77
--- /dev/null
+++ b/src/NameAddrHeader.ts
@@ -0,0 +1,144 @@
+import * as Grammar from './Grammar';
+import URI = require('./URI');
+
+class NameAddrHeader {
+ /**
+ * Parse the given string and returns a NameAddrHeader instance or undefined if
+ * it is an invalid NameAddrHeader.
+ */
+ static parse(name_addr_header: string): NameAddrHeader | undefined {
+ const parsed = Grammar.parse(name_addr_header, 'Name_Addr_Header') as
+ | NameAddrHeader
+ | -1;
+
+ if (parsed !== -1) {
+ return parsed;
+ } else {
+ return undefined;
+ }
+ }
+
+ #uri: URI;
+ #display_name: string | undefined;
+ #parameters: URI.Parameters;
+
+ constructor(
+ uri: URI,
+ display_name?: string,
+ parameters?: URI.Parameters | null
+ ) {
+ // Checks.
+ if (!uri || !(uri instanceof URI)) {
+ throw new TypeError('missing or invalid "uri" parameter');
+ }
+
+ // Initialize parameters.
+ this.#uri = uri;
+ this.#parameters = {};
+ this.display_name = display_name;
+
+ for (const param in parameters) {
+ if (Object.prototype.hasOwnProperty.call(parameters, param)) {
+ this.setParam(param, parameters[param]);
+ }
+ }
+ }
+
+ get uri(): URI {
+ return this.#uri;
+ }
+
+ get display_name(): string | undefined {
+ return this.#display_name;
+ }
+
+ set display_name(value: string | number | undefined) {
+ this.#display_name = value === 0 ? '0' : (value as string | undefined);
+ }
+
+ setParam(key: string, value?: string | number | null): void {
+ if (key) {
+ this.#parameters[key.toLowerCase()] =
+ typeof value === 'undefined' || value === null
+ ? null
+ : value.toString();
+ }
+ }
+
+ getParam(key: string): string | null | undefined {
+ if (key) {
+ return this.#parameters[key.toLowerCase()];
+ }
+
+ return undefined;
+ }
+
+ hasParam(key: string): boolean | undefined {
+ if (key) {
+ return (
+ (Object.prototype.hasOwnProperty.call(
+ this.#parameters,
+ key.toLowerCase()
+ ) &&
+ true) ||
+ false
+ );
+ }
+
+ return undefined;
+ }
+
+ deleteParam(parameter: string): string | null | undefined {
+ parameter = parameter.toLowerCase();
+ if (Object.prototype.hasOwnProperty.call(this.#parameters, parameter)) {
+ const value = this.#parameters[parameter];
+
+ delete this.#parameters[parameter];
+
+ return value;
+ }
+
+ return undefined;
+ }
+
+ clearParams(): void {
+ this.#parameters = {};
+ }
+
+ clone(): NameAddrHeader {
+ return new NameAddrHeader(
+ this.#uri.clone(),
+ this.#display_name,
+ JSON.parse(JSON.stringify(this.#parameters)) as URI.Parameters
+ );
+ }
+
+ #quote(str: string): string {
+ return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
+ }
+
+ toString(): string {
+ let body = this.#display_name
+ ? `"${this.#quote(this.#display_name)}" `
+ : '';
+
+ body += `<${this.#uri.toString()}>`;
+
+ for (const parameter in this.#parameters) {
+ if (Object.prototype.hasOwnProperty.call(this.#parameters, parameter)) {
+ body += `;${parameter}`;
+
+ if (this.#parameters[parameter] !== null) {
+ body += `=${this.#parameters[parameter]}`;
+ }
+ }
+ }
+
+ return body;
+ }
+}
+
+// NOTE: Exported with `export =` because the pegjs generated `Grammar.js` does
+// `var NameAddrHeader = require('./NameAddrHeader')` and then
+// `new NameAddrHeader(...)`.
+export = NameAddrHeader;
diff --git a/src/RTCSession.d.ts b/src/RTCSession.d.ts
index 4e03d92..232669e 100644
--- a/src/RTCSession.d.ts
+++ b/src/RTCSession.d.ts
@@ -5,8 +5,8 @@ import {
IncomingResponse,
OutgoingRequest,
} from './SIPMessage';
-import { NameAddrHeader } from './NameAddrHeader';
-import { URI } from './URI';
+import NameAddrHeader = require('./NameAddrHeader');
+import URI = require('./URI');
import { UA } from './UA';
import { causes, DTMF_TRANSPORT } from './Constants';
diff --git a/src/RTCSession/ReferSubscriber.js b/src/RTCSession/ReferSubscriber.js
index e8aca9b..91d4065 100644
--- a/src/RTCSession/ReferSubscriber.js
+++ b/src/RTCSession/ReferSubscriber.js
@@ -53,7 +53,7 @@ module.exports = class ReferSubscriber extends EventEmitter {
header.toLowerCase().startsWith('referred-by:')
)
) {
- const referredBy = `Referred-By: <${this._session._ua._configuration.uri._scheme}:${this._session._ua._configuration.uri._user}@${this._session._ua._configuration.uri._host}>`;
+ const referredBy = `Referred-By: <${this._session._ua._configuration.uri.scheme}:${this._session._ua._configuration.uri.user}@${this._session._ua._configuration.uri.host}>`;
extraHeaders.push(referredBy);
}
diff --git a/src/SIPMessage.d.ts b/src/SIPMessage.d.ts
index 31a20a9..40da3cd 100644
--- a/src/SIPMessage.d.ts
+++ b/src/SIPMessage.d.ts
@@ -1,5 +1,5 @@
-import { NameAddrHeader } from './NameAddrHeader';
-import { URI } from './URI';
+import NameAddrHeader = require('./NameAddrHeader');
+import URI = require('./URI');
declare class IncomingMessage {
method: string;
diff --git a/src/Socket.d.ts b/src/Socket.d.ts
deleted file mode 100644
index 034ac62..0000000
--- a/src/Socket.d.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-export interface WeightedSocket {
- socket: Socket;
- weight: number;
-}
-
-export class Socket {
- get via_transport(): string;
- set via_transport(value: string);
-
- get url(): string;
-
- get sip_uri(): string;
-
- connect(): void;
-
- disconnect(): void;
-
- send(message: string | ArrayBufferLike | Blob | ArrayBufferView): boolean;
-
- isConnected(): boolean;
-
- isConnecting(): boolean;
-
- onconnect(): void;
-
- ondisconnect(error: boolean, code?: number, reason?: string): void;
-
- ondata<T>(event: T): void;
-}
diff --git a/src/Socket.js b/src/Socket.ts
similarity index 55%
rename from src/Socket.js
rename to src/Socket.ts
index 5c9ffc1..5c6e5a0 100644
--- a/src/Socket.js
+++ b/src/Socket.ts
@@ -1,29 +1,42 @@
-const { Logger } = require('./Logger');
-const Utils = require('./Utils');
-const Grammar = require('./Grammar');
+import { Logger } from './Logger';
+import { isFunction, isString } from './Utils';
+import * as Grammar from './Grammar';
const logger = new Logger('Socket');
/**
* Interface documentation: https://jssip.net/documentation/$last_version/api/socket/
- *
- * interface Socket {
- * attribute String via_transport
- * attribute String url
- * attribute String sip_uri
- *
- * method connect();
- * method disconnect();
- * method send(data);
- *
- * attribute EventHandler onconnect
- * attribute EventHandler ondisconnect
- * attribute EventHandler ondata
- * }
- *
*/
+export interface Socket {
+ readonly url: string;
-exports.isSocket = socket => {
+ readonly sip_uri: string;
+
+ via_transport: string;
+
+ connect(): void;
+
+ disconnect(): void;
+
+ send(message: string | ArrayBufferLike | Blob | ArrayBufferView): boolean;
+
+ isConnected(): boolean;
+
+ isConnecting(): boolean;
+
+ onconnect(): void;
+
+ ondisconnect(error: boolean, code?: number, reason?: string): void;
+
+ ondata<T>(event: T): void;
+}
+
+export interface WeightedSocket {
+ socket: Socket;
+ weight: number;
+}
+
+export const isSocket = (socket: unknown): socket is Socket => {
// Ignore if an array is given.
if (Array.isArray(socket)) {
return false;
@@ -35,19 +48,21 @@ exports.isSocket = socket => {
return false;
}
+ const candidate = socket as Partial<Socket> & Record<string, unknown>;
+
// Check Properties.
try {
- if (!Utils.isString(socket.url)) {
+ if (!isString(candidate.url)) {
logger.warn('missing or invalid JsSIP.Socket url property');
throw new Error('Missing or invalid JsSIP.Socket url property');
}
- if (!Utils.isString(socket.via_transport)) {
+ if (!isString(candidate.via_transport)) {
logger.warn('missing or invalid JsSIP.Socket via_transport property');
throw new Error('Missing or invalid JsSIP.Socket via_transport property');
}
- if (Grammar.parse(socket.sip_uri, 'SIP_URI') === -1) {
+ if (Grammar.parse(candidate.sip_uri!, 'SIP_URI') === -1) {
logger.warn('missing or invalid JsSIP.Socket sip_uri property');
throw new Error('missing or invalid JsSIP.Socket sip_uri property');
}
@@ -58,7 +73,7 @@ exports.isSocket = socket => {
// Check Methods.
try {
['connect', 'disconnect', 'send'].forEach(method => {
- if (!Utils.isFunction(socket[method])) {
+ if (!isFunction(candidate[method])) {
logger.warn(`missing or invalid JsSIP.Socket method: ${method}`);
throw new Error(`Missing or invalid JsSIP.Socket method: ${method}`);
}
diff --git a/src/Transport.d.ts b/src/Transport.d.ts
index 0829a9b..8f05346 100644
--- a/src/Transport.d.ts
+++ b/src/Transport.d.ts
@@ -5,6 +5,30 @@ export interface RecoveryOptions {
max_interval: number;
}
-export class Transport extends Socket {
+export class Transport {
constructor(sockets: Socket | Socket[], recovery_options?: RecoveryOptions);
+
+ get via_transport(): string;
+
+ get url(): string;
+
+ get sip_uri(): string;
+
+ connect(): void;
+
+ disconnect(): void;
+
+ send(data: string | ArrayBufferLike | Blob | ArrayBufferView): boolean;
+
+ isConnected(): boolean;
+
+ isConnecting(): boolean;
+
+ onconnecting(attempts: number): void;
+
+ onconnect(): void;
+
+ ondisconnect(error: boolean, code?: number, reason?: string): void;
+
+ ondata<T>(event: T): void;
}
diff --git a/src/UA.d.ts b/src/UA.d.ts
index ae562cc..0bffad4 100644
--- a/src/UA.d.ts
+++ b/src/UA.d.ts
@@ -17,7 +17,7 @@ import { Message, SendMessageOptions } from './Message';
import { Registrator } from './Registrator';
import { Notifier } from './Notifier';
import { Subscriber } from './Subscriber';
-import { URI } from './URI';
+import URI = require('./URI');
import { causes } from './Constants';
export interface UnRegisterOptions {
diff --git a/src/URI.d.ts b/src/URI.d.ts
deleted file mode 100644
index eb1502c..0000000
--- a/src/URI.d.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { Grammar } from './Grammar';
-
-export type URIScheme = 'sip' | string;
-
-export type Parameters = Record<string, string | null>;
-
-export type Headers = Record<string, string | string[]>;
-
-export class URI {
- scheme: URIScheme;
- user: string;
- host: string;
- port: number;
-
- constructor(
- scheme: URIScheme,
- user: string,
- host: string,
- port?: number,
- parameters?: Parameters,
- headers?: Headers
- );
-
- setParam(key: string, value?: string | number | null): void;
-
- getParam<T = unknown>(key: string): T;
-
- hasParam(key: string): boolean;
-
- deleteParam(key: string): void;
-
- clearParams(): void;
-
- setHeader(key: string, value: string | string[]): void;
-
- getHeader(key: string): string[];
-
- hasHeader(key: string): boolean;
-
- deleteHeader(key: string): void;
-
- clearHeaders(): void;
-
- clone(): this;
-
- toString(): string;
-
- toAor(show_port?: boolean): string;
-
- static parse(uri: string): Grammar | undefined;
-}
diff --git a/src/URI.js b/src/URI.js
deleted file mode 100644
index 8e629a9..0000000
--- a/src/URI.js
+++ /dev/null
@@ -1,224 +0,0 @@
-const JsSIP_C = require('./Constants');
-const Utils = require('./Utils');
-const Grammar = require('./Grammar');
-
-/**
- * -param {String} [scheme]
- * -param {String} [user]
- * -param {String} host
- * -param {String} [port]
- * -param {Object} [parameters]
- * -param {Object} [headers]
- *
- */
-module.exports = class URI {
- /**
- * Parse the given string and returns a JsSIP.URI instance or undefined if
- * it is an invalid URI.
- */
- static parse(uri) {
- uri = Grammar.parse(uri, 'SIP_URI');
-
- if (uri !== -1) {
- return uri;
- } else {
- return undefined;
- }
- }
-
- constructor(scheme, user, host, port, parameters = {}, headers = {}) {
- // Checks.
- if (!host) {
- throw new TypeError('missing or invalid "host" parameter');
- }
-
- // Initialize parameters.
- this._parameters = {};
- this._headers = {};
-
- this._scheme = scheme || JsSIP_C.SIP;
- this._user = user;
- this._host = host;
- this._port = port;
-
- for (const param in parameters) {
- if (Object.prototype.hasOwnProperty.call(parameters, param)) {
- this.setParam(param, parameters[param]);
- }
- }
-
- for (const header in headers) {
- if (Object.prototype.hasOwnProperty.call(headers, header)) {
- this.setHeader(header, headers[header]);
- }
- }
- }
-
- get scheme() {
- return this._scheme;
- }
-
- set scheme(value) {
- this._scheme = value.toLowerCase();
- }
-
- get user() {
- return this._user;
- }
-
- set user(value) {
- this._user = value;
- }
-
- get host() {
- return this._host;
- }
-
- set host(value) {
- this._host = value.toLowerCase();
- }
-
- get port() {
- return this._port;
- }
-
- set port(value) {
- this._port = value === 0 ? value : parseInt(value, 10) || null;
- }
-
- setParam(key, value) {
- if (key) {
- this._parameters[key.toLowerCase()] =
- typeof value === 'undefined' || value === null
- ? null
- : value.toString();
- }
- }
-
- getParam(key) {
- if (key) {
- return this._parameters[key.toLowerCase()];
- }
- }
-
- hasParam(key) {
- if (key) {
- return (
- (this._parameters.hasOwnProperty(key.toLowerCase()) && true) || false
- );
- }
- }
-
- deleteParam(parameter) {
- parameter = parameter.toLowerCase();
- if (this._parameters.hasOwnProperty(parameter)) {
- const value = this._parameters[parameter];
-
- delete this._parameters[parameter];
-
- return value;
- }
- }
-
- clearParams() {
- this._parameters = {};
- }
-
- setHeader(name, value) {
- this._headers[Utils.headerize(name)] = Array.isArray(value)
- ? value
- : [value];
- }
-
- getHeader(name) {
- if (name) {
- return this._headers[Utils.headerize(name)];
- }
- }
-
- hasHeader(name) {
- if (name) {
- return (
- (this._headers.hasOwnProperty(Utils.headerize(name)) && true) || false
- );
- }
- }
-
- deleteHeader(header) {
- header = Utils.headerize(header);
- if (this._headers.hasOwnProperty(header)) {
- const value = this._headers[header];
-
- delete this._headers[header];
-
- return value;
- }
- }
-
- clearHeaders() {
- this._headers = {};
- }
-
- clone() {
- return new URI(
- this._scheme,
- this._user,
- this._host,
- this._port,
- JSON.parse(JSON.stringify(this._parameters)),
- JSON.parse(JSON.stringify(this._headers))
- );
- }
-
- toString() {
- const headers = [];
-
- let uri = `${this._scheme}:`;
-
- if (this._user) {
- uri += `${Utils.escapeUser(this._user)}@`;
- }
- uri += this._host;
- if (this._port || this._port === 0) {
- uri += `:${this._port}`;
- }
-
- for (const parameter in this._parameters) {
- if (Object.prototype.hasOwnProperty.call(this._parameters, parameter)) {
- uri += `;${parameter}`;
-
- if (this._parameters[parameter] !== null) {
- uri += `=${this._parameters[parameter]}`;
- }
- }
- }
-
- for (const header in this._headers) {
- if (Object.prototype.hasOwnProperty.call(this._headers, header)) {
- for (const item of this._headers[header]) {
- headers.push(`${header}=${item}`);
- }
- }
- }
-
- if (headers.length > 0) {
- uri += `?${headers.join('&')}`;
- }
-
- return uri;
- }
-
- toAor(show_port) {
- let aor = `${this._scheme}:`;
-
- if (this._user) {
- aor += `${Utils.escapeUser(this._user)}@`;
- }
- aor += this._host;
- if (show_port && (this._port || this._port === 0)) {
- aor += `:${this._port}`;
- }
-
- return aor;
- }
-};
diff --git a/src/URI.ts b/src/URI.ts
new file mode 100644
index 0000000..371d097
--- /dev/null
+++ b/src/URI.ts
@@ -0,0 +1,270 @@
+import { SIP } from './Constants';
+import { escapeUser, headerize } from './Utils';
+import * as Grammar from './Grammar';
+
+/**
+ * -param {String} [scheme]
+ * -param {String} [user]
+ * -param {String} host
+ * -param {String} [port]
+ * -param {Object} [parameters]
+ * -param {Object} [headers]
+ *
+ */
+class URI {
+ /**
+ * Parse the given string and returns a JsSIP.URI instance or undefined if
+ * it is an invalid URI.
+ */
+ static parse(uri: string): URI | undefined {
+ const parsed = Grammar.parse(uri, 'SIP_URI') as URI | -1;
+
+ if (parsed !== -1) {
+ return parsed;
+ } else {
+ return undefined;
+ }
+ }
+
+ #scheme: string;
+ #user: string | undefined;
+ #host: string;
+ #port: number | null | undefined;
+ #parameters: URI.Parameters;
+ #headers: URI.Headers;
+
+ constructor(
+ scheme: URI.URIScheme | undefined,
+ user: string | undefined,
+ host: string,
+ port?: number | null,
+ parameters: URI.Parameters = {},
+ headers: URI.Headers = {}
+ ) {
+ // Checks.
+ if (!host) {
+ throw new TypeError('missing or invalid "host" parameter');
+ }
+
+ // Initialize parameters.
+ this.#parameters = {};
+ this.#headers = {};
+
+ this.#scheme = scheme || SIP;
+ this.#user = user;
+ this.#host = host;
+ this.#port = port;
+
+ for (const param in parameters) {
+ if (Object.prototype.hasOwnProperty.call(parameters, param)) {
+ this.setParam(param, parameters[param]);
+ }
+ }
+
+ for (const header in headers) {
+ if (Object.prototype.hasOwnProperty.call(headers, header)) {
+ this.setHeader(header, headers[header]!);
+ }
+ }
+ }
+
+ get scheme(): string {
+ return this.#scheme;
+ }
+
+ set scheme(value: string) {
+ this.#scheme = value.toLowerCase();
+ }
+
+ get user(): string | undefined {
+ return this.#user;
+ }
+
+ set user(value: string | undefined) {
+ this.#user = value;
+ }
+
+ get host(): string {
+ return this.#host;
+ }
+
+ set host(value: string) {
+ this.#host = value.toLowerCase();
+ }
+
+ get port(): number | null | undefined {
+ return this.#port;
+ }
+
+ set port(value: number | string | null | undefined) {
+ this.#port = value === 0 ? value : parseInt(String(value), 10) || null;
+ }
+
+ setParam(key: string, value?: string | number | null): void {
+ if (key) {
+ this.#parameters[key.toLowerCase()] =
+ typeof value === 'undefined' || value === null
+ ? null
+ : value.toString();
+ }
+ }
+
+ getParam(key: string): string | null | undefined {
+ if (key) {
+ return this.#parameters[key.toLowerCase()];
+ }
+
+ return undefined;
+ }
+
+ hasParam(key: string): boolean | undefined {
+ if (key) {
+ return (
+ (Object.prototype.hasOwnProperty.call(
+ this.#parameters,
+ key.toLowerCase()
+ ) &&
+ true) ||
+ false
+ );
+ }
+
+ return undefined;
+ }
+
+ deleteParam(parameter: string): string | null | undefined {
+ parameter = parameter.toLowerCase();
+ if (Object.prototype.hasOwnProperty.call(this.#parameters, parameter)) {
+ const value = this.#parameters[parameter];
+
+ delete this.#parameters[parameter];
+
+ return value;
+ }
+
+ return undefined;
+ }
+
+ clearParams(): void {
+ this.#parameters = {};
+ }
+
+ setHeader(name: string, value: string | string[]): void {
+ this.#headers[headerize(name)] = Array.isArray(value) ? value : [value];
+ }
+
+ getHeader(name: string): string[] | undefined {
+ if (name) {
+ return this.#headers[headerize(name)];
+ }
+
+ return undefined;
+ }
+
+ hasHeader(name: string): boolean | undefined {
+ if (name) {
+ return (
+ (Object.prototype.hasOwnProperty.call(this.#headers, headerize(name)) &&
+ true) ||
+ false
+ );
+ }
+
+ return undefined;
+ }
+
+ deleteHeader(header: string): string[] | undefined {
+ header = headerize(header);
+ if (Object.prototype.hasOwnProperty.call(this.#headers, header)) {
+ const value = this.#headers[header];
+
+ delete this.#headers[header];
+
+ return value;
+ }
+
+ return undefined;
+ }
+
+ clearHeaders(): void {
+ this.#headers = {};
+ }
+
+ clone(): URI {
+ return new URI(
+ this.#scheme,
+ this.#user,
+ this.#host,
+ this.#port,
+ JSON.parse(JSON.stringify(this.#parameters)) as URI.Parameters,
+ JSON.parse(JSON.stringify(this.#headers)) as URI.Headers
+ );
+ }
+
+ toString(): string {
+ const headers = [];
+
+ let uri = `${this.#scheme}:`;
+
+ if (this.#user) {
+ uri += `${escapeUser(this.#user)}@`;
+ }
+ uri += this.#host;
+ if (this.#port || this.#port === 0) {
+ uri += `:${this.#port}`;
+ }
+
+ for (const parameter in this.#parameters) {
+ if (Object.prototype.hasOwnProperty.call(this.#parameters, parameter)) {
+ uri += `;${parameter}`;
+
+ if (this.#parameters[parameter] !== null) {
+ uri += `=${this.#parameters[parameter]}`;
+ }
+ }
+ }
+
+ for (const header in this.#headers) {
+ if (Object.prototype.hasOwnProperty.call(this.#headers, header)) {
+ for (const item of this.#headers[header] ?? []) {
+ headers.push(`${header}=${item}`);
+ }
+ }
+ }
+
+ if (headers.length > 0) {
+ uri += `?${headers.join('&')}`;
+ }
+
+ return uri;
+ }
+
+ toAor(show_port?: boolean): string {
+ let aor = `${this.#scheme}:`;
+
+ if (this.#user) {
+ aor += `${escapeUser(this.#user)}@`;
+ }
+ aor += this.#host;
+ if (show_port && (this.#port || this.#port === 0)) {
+ aor += `:${this.#port}`;
+ }
+
+ return aor;
+ }
+}
+
+// NOTE: `URI` is exported with `export =` because the pegjs generated
+// `Grammar.js` does `var URI = require('./URI')` and then `new URI(...)`.
+// Types that used to be named exports of this module are merged into the
+// `URI` namespace so that they remain reachable.
+// eslint-disable-next-line @typescript-eslint/no-namespace
+declare namespace URI {
+ type URIScheme = 'sip' | string;
+
+ type Parameters = Record<string, string | null>;
+
+ type Headers = Record<string, string[]>;
+}
+
+export = URI;
diff --git a/src/Utils.d.ts b/src/Utils.d.ts
deleted file mode 100644
index 9eb0f30..0000000
--- a/src/Utils.d.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import { URI } from './URI';
-import { causes } from './Constants';
-
-export function str_utf8_length(str: string): number;
-
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export function isFunction(fn: unknown): fn is (...args: any[]) => any;
-
-export function isString(str: unknown): str is string;
-
-export function isDecimal(num: unknown): num is number;
-
-export function isEmpty(value: unknown): boolean;
-
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export function hasMethods(obj: any, ...methodNames: string[]): boolean;
-
-export function createRandomToken(size: number, base?: number): string;
-
-export function newTag(): string;
-
-export function newUUID(): string;
-
-export function hostType(host: string): string;
-
-export function escapeUser(user: string): string;
-
-export function normalizeTarget(
- target: URI | string,
- domain?: string
-): URI | undefined;
-
-export function headerize(str: string): string;
-
-export function sipErrorCause(status_code: number): causes;
-
-export function getRandomTestNetIP(): string;
-
-export function calculateMD5(str: string): string;
-
-export function closeMediaStream(stream?: MediaStream): void;
-
-export function cloneArray<T = unknown>(arr: T[]): T[];
-
-export function cloneObject<T>(obj: T, fallback?: T): T;
diff --git a/src/Utils.js b/src/Utils.js
deleted file mode 100644
index 1ed5aac..0000000
--- a/src/Utils.js
+++ /dev/null
@@ -1,495 +0,0 @@
-const JsSIP_C = require('./Constants');
-const URI = require('./URI');
-const Grammar = require('./Grammar');
-
-exports.str_utf8_length = string => unescape(encodeURIComponent(string)).length;
-
-// Used by 'hasMethods'.
-const isFunction = (exports.isFunction = fn => {
- if (fn !== undefined) {
- return Object.prototype.toString.call(fn) === '[object Function]'
- ? true
- : false;
- } else {
- return false;
- }
-});
-
-exports.isString = str => {
- if (str !== undefined) {
- return Object.prototype.toString.call(str) === '[object String]'
- ? true
- : false;
- } else {
- return false;
- }
-};
-
-exports.isDecimal = num => !isNaN(num) && parseFloat(num) === parseInt(num, 10);
-
-exports.isEmpty = value => {
- return (
- value === null ||
- value === '' ||
- value === undefined ||
- (Array.isArray(value) && value.length === 0) ||
- (typeof value === 'number' && isNaN(value))
- );
-};
-
-exports.hasMethods = function (obj, ...methodNames) {
- for (const methodName of methodNames) {
- if (isFunction(obj[methodName])) {
- return false;
- }
- }
-
- return true;
-};
-
-// Used by 'newTag'.
-const createRandomToken = (exports.createRandomToken = (size, base = 32) => {
- let i,
- r,
- token = '';
-
- for (i = 0; i < size; i++) {
- r = (Math.random() * base) | 0;
- token += r.toString(base);
- }
-
- return token;
-});
-
-exports.newTag = () => createRandomToken(10);
-
-// https://stackoverflow.com/users/109538/broofa.
-exports.newUUID = () => {
- const UUID = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
- const r = (Math.random() * 16) | 0,
- v = c === 'x' ? r : (r & 0x3) | 0x8;
-
- return v.toString(16);
- });
-
- return UUID;
-};
-
-exports.hostType = host => {
- if (!host) {
- return;
- } else {
- host = Grammar.parse(host, 'host');
- if (host !== -1) {
- return host.host_type;
- }
- }
-};
-
-/**
- * Hex-escape a SIP URI user.
- * Don't hex-escape ':' (%3A), '+' (%2B), '?' (%3F"), '/' (%2F).
- *
- * Used by 'normalizeTarget'.
- */
-const escapeUser = (exports.escapeUser = user =>
- encodeURIComponent(decodeURIComponent(user))
- .replace(/%3A/gi, ':')
- .replace(/%2B/gi, '+')
- .replace(/%3F/gi, '?')
- .replace(/%2F/gi, '/'));
-
-/**
- * Normalize SIP URI.
- * NOTE: It does not allow a SIP URI without username.
- * Accepts 'sip', 'sips' and 'tel' URIs and convert them into 'sip'.
- * Detects the domain part (if given) and properly hex-escapes the user portion.
- * If the user portion has only 'tel' number symbols the user portion is clean of 'tel' visual separators.
- */
-exports.normalizeTarget = (target, domain) => {
- // If no target is given then raise an error.
- if (!target) {
- return;
- // If a URI instance is given then return it.
- } else if (target instanceof URI) {
- return target;
-
- // If a string is given split it by '@':
- // - Last fragment is the desired domain.
- // - Otherwise append the given domain argument.
- } else if (typeof target === 'string') {
- const target_array = target.split('@');
- let target_user;
- let target_domain;
-
- switch (target_array.length) {
- case 1: {
- if (!domain) {
- return;
- }
- target_user = target;
- target_domain = domain;
- break;
- }
- case 2: {
- target_user = target_array[0];
- target_domain = target_array[1];
- break;
- }
- default: {
- target_user = target_array.slice(0, target_array.length - 1).join('@');
- target_domain = target_array[target_array.length - 1];
- }
- }
-
- // Remove the URI scheme (if present).
- target_user = target_user.replace(/^(sips?|tel):/i, '');
-
- // Remove 'tel' visual separators if the user portion just contains 'tel' number symbols.
- if (/^[-.()]*\+?[0-9\-.()]+$/.test(target_user)) {
- target_user = target_user.replace(/[-.()]/g, '');
- }
-
- // Build the complete SIP URI.
- target = `${JsSIP_C.SIP}:${escapeUser(target_user)}@${target_domain}`;
-
- // Finally parse the resulting URI.
- let uri;
-
- if ((uri = URI.parse(target))) {
- return uri;
- } else {
- return;
- }
- } else {
- return;
- }
-};
-
-exports.headerize = string => {
- const exceptions = {
- 'Call-Id': 'Call-ID',
- Cseq: 'CSeq',
- 'Www-Authenticate': 'WWW-Authenticate',
- };
-
- const name = string.toLowerCase().replace(/_/g, '-').split('-');
- let hname = '';
- const parts = name.length;
- let part;
-
- for (part = 0; part < parts; part++) {
- if (part !== 0) {
- hname += '-';
- }
- hname += name[part].charAt(0).toUpperCase() + name[part].substring(1);
- }
- if (exceptions[hname]) {
- hname = exceptions[hname];
- }
-
- return hname;
-};
-
-exports.sipErrorCause = status_code => {
- for (const cause in JsSIP_C.SIP_ERROR_CAUSES) {
- if (JsSIP_C.SIP_ERROR_CAUSES[cause].indexOf(status_code) !== -1) {
- return JsSIP_C.causes[cause];
- }
- }
-
- return JsSIP_C.causes.SIP_FAILURE_CODE;
-};
-
-/**
- * Generate a random Test-Net IP (https://tools.ietf.org/html/rfc5735)
- */
-exports.getRandomTestNetIP = () => {
- function getOctet(from, to) {
- return Math.floor(Math.random() * (to - from + 1) + from);
- }
-
- return `192.0.2.${getOctet(1, 254)}`;
-};
-
-// MD5 (Message-Digest Algorithm) https://www.webtoolkit.info.
-exports.calculateMD5 = string => {
- function rotateLeft(lValue, iShiftBits) {
- return (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits));
- }
-
- function addUnsigned(lX, lY) {
- const lX8 = lX & 0x80000000;
- const lY8 = lY & 0x80000000;
- const lX4 = lX & 0x40000000;
- const lY4 = lY & 0x40000000;
- const lResult = (lX & 0x3fffffff) + (lY & 0x3fffffff);
-
- if (lX4 & lY4) {
- return lResult ^ 0x80000000 ^ lX8 ^ lY8;
- }
- if (lX4 | lY4) {
- if (lResult & 0x40000000) {
- return lResult ^ 0xc0000000 ^ lX8 ^ lY8;
- } else {
- return lResult ^ 0x40000000 ^ lX8 ^ lY8;
- }
- } else {
- return lResult ^ lX8 ^ lY8;
- }
- }
-
- function doF(x, y, z) {
- return (x & y) | (~x & z);
- }
-
- function doG(x, y, z) {
- return (x & z) | (y & ~z);
- }
-
- function doH(x, y, z) {
- return x ^ y ^ z;
- }
-
- function doI(x, y, z) {
- return y ^ (x | ~z);
- }
-
- function doFF(a, b, c, d, x, s, ac) {
- a = addUnsigned(a, addUnsigned(addUnsigned(doF(b, c, d), x), ac));
-
- return addUnsigned(rotateLeft(a, s), b);
- }
-
- function doGG(a, b, c, d, x, s, ac) {
- a = addUnsigned(a, addUnsigned(addUnsigned(doG(b, c, d), x), ac));
-
- return addUnsigned(rotateLeft(a, s), b);
- }
-
- function doHH(a, b, c, d, x, s, ac) {
- a = addUnsigned(a, addUnsigned(addUnsigned(doH(b, c, d), x), ac));
-
- return addUnsigned(rotateLeft(a, s), b);
- }
-
- function doII(a, b, c, d, x, s, ac) {
- a = addUnsigned(a, addUnsigned(addUnsigned(doI(b, c, d), x), ac));
-
- return addUnsigned(rotateLeft(a, s), b);
- }
-
- function convertToWordArray(str) {
- let lWordCount;
- const lMessageLength = str.length;
- const lNumberOfWords_temp1 = lMessageLength + 8;
- const lNumberOfWords_temp2 =
- (lNumberOfWords_temp1 - (lNumberOfWords_temp1 % 64)) / 64;
- const lNumberOfWords = (lNumberOfWords_temp2 + 1) * 16;
- const lWordArray = new Array(lNumberOfWords - 1);
- let lBytePosition = 0;
- let lByteCount = 0;
-
- while (lByteCount < lMessageLength) {
- lWordCount = (lByteCount - (lByteCount % 4)) / 4;
- lBytePosition = (lByteCount % 4) * 8;
- lWordArray[lWordCount] =
- lWordArray[lWordCount] | (str.charCodeAt(lByteCount) << lBytePosition);
- lByteCount++;
- }
- lWordCount = (lByteCount - (lByteCount % 4)) / 4;
- lBytePosition = (lByteCount % 4) * 8;
- lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80 << lBytePosition);
- lWordArray[lNumberOfWords - 2] = lMessageLength << 3;
- lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29;
-
- return lWordArray;
- }
-
- function wordToHex(lValue) {
- let wordToHexValue = '',
- wordToHexValue_temp = '',
- lByte,
- lCount;
-
- for (lCount = 0; lCount <= 3; lCount++) {
- lByte = (lValue >>> (lCount * 8)) & 255;
- wordToHexValue_temp = `0${lByte.toString(16)}`;
- wordToHexValue =
- wordToHexValue +
- wordToHexValue_temp.substr(wordToHexValue_temp.length - 2, 2);
- }
-
- return wordToHexValue;
- }
-
- function utf8Encode(str) {
- let utftext = '';
-
- for (let n = 0; n < str.length; n++) {
- const c = str.charCodeAt(n);
-
- if (c < 128) {
- utftext += String.fromCharCode(c);
- } else if (c > 127 && c < 2048) {
- utftext += String.fromCharCode((c >> 6) | 192);
- utftext += String.fromCharCode((c & 63) | 128);
- } else {
- utftext += String.fromCharCode((c >> 12) | 224);
- utftext += String.fromCharCode(((c >> 6) & 63) | 128);
- utftext += String.fromCharCode((c & 63) | 128);
- }
- }
-
- return utftext;
- }
-
- let x = [];
- let k, AA, BB, CC, DD, a, b, c, d;
- const S11 = 7,
- S12 = 12,
- S13 = 17,
- S14 = 22;
- const S21 = 5,
- S22 = 9,
- S23 = 14,
- S24 = 20;
- const S31 = 4,
- S32 = 11,
- S33 = 16,
- S34 = 23;
- const S41 = 6,
- S42 = 10,
- S43 = 15,
- S44 = 21;
-
- string = utf8Encode(string);
-
- x = convertToWordArray(string);
-
- a = 0x67452301;
- b = 0xefcdab89;
- c = 0x98badcfe;
- d = 0x10325476;
-
- for (k = 0; k < x.length; k += 16) {
- AA = a;
- BB = b;
- CC = c;
- DD = d;
- a = doFF(a, b, c, d, x[k + 0], S11, 0xd76aa478);
- d = doFF(d, a, b, c, x[k + 1], S12, 0xe8c7b756);
- c = doFF(c, d, a, b, x[k + 2], S13, 0x242070db);
- b = doFF(b, c, d, a, x[k + 3], S14, 0xc1bdceee);
- a = doFF(a, b, c, d, x[k + 4], S11, 0xf57c0faf);
- d = doFF(d, a, b, c, x[k + 5], S12, 0x4787c62a);
- c = doFF(c, d, a, b, x[k + 6], S13, 0xa8304613);
- b = doFF(b, c, d, a, x[k + 7], S14, 0xfd469501);
- a = doFF(a, b, c, d, x[k + 8], S11, 0x698098d8);
- d = doFF(d, a, b, c, x[k + 9], S12, 0x8b44f7af);
- c = doFF(c, d, a, b, x[k + 10], S13, 0xffff5bb1);
- b = doFF(b, c, d, a, x[k + 11], S14, 0x895cd7be);
- a = doFF(a, b, c, d, x[k + 12], S11, 0x6b901122);
- d = doFF(d, a, b, c, x[k + 13], S12, 0xfd987193);
- c = doFF(c, d, a, b, x[k + 14], S13, 0xa679438e);
- b = doFF(b, c, d, a, x[k + 15], S14, 0x49b40821);
- a = doGG(a, b, c, d, x[k + 1], S21, 0xf61e2562);
- d = doGG(d, a, b, c, x[k + 6], S22, 0xc040b340);
- c = doGG(c, d, a, b, x[k + 11], S23, 0x265e5a51);
- b = doGG(b, c, d, a, x[k + 0], S24, 0xe9b6c7aa);
- a = doGG(a, b, c, d, x[k + 5], S21, 0xd62f105d);
- d = doGG(d, a, b, c, x[k + 10], S22, 0x2441453);
- c = doGG(c, d, a, b, x[k + 15], S23, 0xd8a1e681);
- b = doGG(b, c, d, a, x[k + 4], S24, 0xe7d3fbc8);
- a = doGG(a, b, c, d, x[k + 9], S21, 0x21e1cde6);
- d = doGG(d, a, b, c, x[k + 14], S22, 0xc33707d6);
- c = doGG(c, d, a, b, x[k + 3], S23, 0xf4d50d87);
- b = doGG(b, c, d, a, x[k + 8], S24, 0x455a14ed);
- a = doGG(a, b, c, d, x[k + 13], S21, 0xa9e3e905);
- d = doGG(d, a, b, c, x[k + 2], S22, 0xfcefa3f8);
- c = doGG(c, d, a, b, x[k + 7], S23, 0x676f02d9);
- b = doGG(b, c, d, a, x[k + 12], S24, 0x8d2a4c8a);
- a = doHH(a, b, c, d, x[k + 5], S31, 0xfffa3942);
- d = doHH(d, a, b, c, x[k + 8], S32, 0x8771f681);
- c = doHH(c, d, a, b, x[k + 11], S33, 0x6d9d6122);
- b = doHH(b, c, d, a, x[k + 14], S34, 0xfde5380c);
- a = doHH(a, b, c, d, x[k + 1], S31, 0xa4beea44);
- d = doHH(d, a, b, c, x[k + 4], S32, 0x4bdecfa9);
- c = doHH(c, d, a, b, x[k + 7], S33, 0xf6bb4b60);
- b = doHH(b, c, d, a, x[k + 10], S34, 0xbebfbc70);
- a = doHH(a, b, c, d, x[k + 13], S31, 0x289b7ec6);
- d = doHH(d, a, b, c, x[k + 0], S32, 0xeaa127fa);
- c = doHH(c, d, a, b, x[k + 3], S33, 0xd4ef3085);
- b = doHH(b, c, d, a, x[k + 6], S34, 0x4881d05);
- a = doHH(a, b, c, d, x[k + 9], S31, 0xd9d4d039);
- d = doHH(d, a, b, c, x[k + 12], S32, 0xe6db99e5);
- c = doHH(c, d, a, b, x[k + 15], S33, 0x1fa27cf8);
- b = doHH(b, c, d, a, x[k + 2], S34, 0xc4ac5665);
- a = doII(a, b, c, d, x[k + 0], S41, 0xf4292244);
- d = doII(d, a, b, c, x[k + 7], S42, 0x432aff97);
- c = doII(c, d, a, b, x[k + 14], S43, 0xab9423a7);
- b = doII(b, c, d, a, x[k + 5], S44, 0xfc93a039);
- a = doII(a, b, c, d, x[k + 12], S41, 0x655b59c3);
- d = doII(d, a, b, c, x[k + 3], S42, 0x8f0ccc92);
- c = doII(c, d, a, b, x[k + 10], S43, 0xffeff47d);
- b = doII(b, c, d, a, x[k + 1], S44, 0x85845dd1);
- a = doII(a, b, c, d, x[k + 8], S41, 0x6fa87e4f);
- d = doII(d, a, b, c, x[k + 15], S42, 0xfe2ce6e0);
- c = doII(c, d, a, b, x[k + 6], S43, 0xa3014314);
- b = doII(b, c, d, a, x[k + 13], S44, 0x4e0811a1);
- a = doII(a, b, c, d, x[k + 4], S41, 0xf7537e82);
- d = doII(d, a, b, c, x[k + 11], S42, 0xbd3af235);
- c = doII(c, d, a, b, x[k + 2], S43, 0x2ad7d2bb);
- b = doII(b, c, d, a, x[k + 9], S44, 0xeb86d391);
- a = addUnsigned(a, AA);
- b = addUnsigned(b, BB);
- c = addUnsigned(c, CC);
- d = addUnsigned(d, DD);
- }
-
- const temp = wordToHex(a) + wordToHex(b) + wordToHex(c) + wordToHex(d);
-
- return temp.toLowerCase();
-};
-
-exports.closeMediaStream = stream => {
- if (!stream) {
- return;
- }
-
- // Latest spec states that MediaStream has no stop() method and instead must
- // call stop() on every MediaStreamTrack.
- try {
- let tracks;
-
- if (stream.getTracks) {
- tracks = stream.getTracks();
- for (const track of tracks) {
- track.stop();
- }
- } else {
- tracks = stream.getAudioTracks();
- for (const track of tracks) {
- track.stop();
- }
- tracks = stream.getVideoTracks();
- for (const track of tracks) {
- track.stop();
- }
- }
- } catch (error) {
- // Deprecated by the spec, but still in use.
- // NOTE: In Temasys IE plugin stream.stop is a callable 'object'.
- if (typeof stream.stop === 'function' || typeof stream.stop === 'object') {
- stream.stop();
- }
- }
-};
-
-exports.cloneArray = array => {
- return (array && array.slice()) || [];
-};
-
-exports.cloneObject = (obj, fallback = {}) => {
- return (obj && Object.assign({}, obj)) || fallback;
-};
diff --git a/src/Utils.ts b/src/Utils.ts
new file mode 100644
index 0000000..0bcd054
--- /dev/null
+++ b/src/Utils.ts
@@ -0,0 +1,558 @@
+import { SIP, SIP_ERROR_CAUSES, causes } from './Constants';
+import * as Grammar from './Grammar';
+// `URI` is exported with `export =` (CommonJS style) because the pegjs generated
+// `Grammar.js` does `var URI = require('./URI'); new URI(...)`.
+
+import URI = require('./URI');
+
+export const str_utf8_length = (string: string): number =>
+ unescape(encodeURIComponent(string)).length;
+
+// Used by 'hasMethods'.
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export const isFunction = (fn: unknown): fn is (...args: any[]) => any => {
+ if (fn !== undefined) {
+ return Object.prototype.toString.call(fn) === '[object Function]'
+ ? true
+ : false;
+ } else {
+ return false;
+ }
+};
+
+export const isString = (str: unknown): str is string => {
+ if (str !== undefined) {
+ return Object.prototype.toString.call(str) === '[object String]'
+ ? true
+ : false;
+ } else {
+ return false;
+ }
+};
+
+export const isDecimal = (num: unknown): boolean =>
+ !isNaN(num as number) &&
+ parseFloat(num as string) === parseInt(num as string, 10);
+
+export const isEmpty = (value: unknown): boolean => {
+ return (
+ value === null ||
+ value === '' ||
+ value === undefined ||
+ (Array.isArray(value) && value.length === 0) ||
+ (typeof value === 'number' && isNaN(value))
+ );
+};
+
+export const hasMethods = function (
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ obj: any,
+ ...methodNames: string[]
+): boolean {
+ for (const methodName of methodNames) {
+ if (isFunction(obj[methodName])) {
+ return false;
+ }
+ }
+
+ return true;
+};
+
+// Used by 'newTag'.
+export const createRandomToken = (size: number, base = 32): string => {
+ let i,
+ r,
+ token = '';
+
+ for (i = 0; i < size; i++) {
+ r = (Math.random() * base) | 0;
+ token += r.toString(base);
+ }
+
+ return token;
+};
+
+export const newTag = (): string => createRandomToken(10);
+
+// https://stackoverflow.com/users/109538/broofa.
+export const newUUID = (): string => {
+ const UUID = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
+ const r = (Math.random() * 16) | 0,
+ v = c === 'x' ? r : (r & 0x3) | 0x8;
+
+ return v.toString(16);
+ });
+
+ return UUID;
+};
+
+export const hostType = (host?: string): string | undefined => {
+ if (!host) {
+ return undefined;
+ } else {
+ const parsed = Grammar.parse(host, 'host');
+
+ if (parsed !== -1) {
+ return parsed.host_type;
+ }
+
+ return undefined;
+ }
+};
+
+/**
+ * Hex-escape a SIP URI user.
+ * Don't hex-escape ':' (%3A), '+' (%2B), '?' (%3F"), '/' (%2F).
+ *
+ * Used by 'normalizeTarget'.
+ */
+export const escapeUser = (user: string): string =>
+ encodeURIComponent(decodeURIComponent(user))
+ .replace(/%3A/gi, ':')
+ .replace(/%2B/gi, '+')
+ .replace(/%3F/gi, '?')
+ .replace(/%2F/gi, '/');
+
+/**
+ * Normalize SIP URI.
+ * NOTE: It does not allow a SIP URI without username.
+ * Accepts 'sip', 'sips' and 'tel' URIs and convert them into 'sip'.
+ * Detects the domain part (if given) and properly hex-escapes the user portion.
+ * If the user portion has only 'tel' number symbols the user portion is clean of 'tel' visual separators.
+ */
+export const normalizeTarget = (
+ target?: URI | string,
+ domain?: string
+): URI | undefined => {
+ // If no target is given then raise an error.
+ if (!target) {
+ return undefined;
+ // If a URI instance is given then return it.
+ } else if (target instanceof URI) {
+ return target;
+
+ // If a string is given split it by '@':
+ // - Last fragment is the desired domain.
+ // - Otherwise append the given domain argument.
+ } else if (typeof target === 'string') {
+ const target_array = target.split('@');
+ let target_user: string;
+ let target_domain: string;
+
+ switch (target_array.length) {
+ case 1: {
+ if (!domain) {
+ return undefined;
+ }
+ target_user = target;
+ target_domain = domain;
+ break;
+ }
+ case 2: {
+ target_user = target_array[0]!;
+ target_domain = target_array[1]!;
+ break;
+ }
+ default: {
+ target_user = target_array.slice(0, target_array.length - 1).join('@');
+ target_domain = target_array[target_array.length - 1]!;
+ }
+ }
+
+ // Remove the URI scheme (if present).
+ target_user = target_user.replace(/^(sips?|tel):/i, '');
+
+ // Remove 'tel' visual separators if the user portion just contains 'tel' number symbols.
+ if (/^[-.()]*\+?[0-9\-.()]+$/.test(target_user)) {
+ target_user = target_user.replace(/[-.()]/g, '');
+ }
+
+ // Build the complete SIP URI.
+ target = `${SIP}:${escapeUser(target_user)}@${target_domain}`;
+
+ // Finally parse the resulting URI.
+ let uri;
+
+ if ((uri = URI.parse(target))) {
+ return uri;
+ } else {
+ return undefined;
+ }
+ } else {
+ return undefined;
+ }
+};
+
+export const headerize = (string: string): string => {
+ const exceptions: Record<string, string> = {
+ 'Call-Id': 'Call-ID',
+ Cseq: 'CSeq',
+ 'Www-Authenticate': 'WWW-Authenticate',
+ };
+
+ const name = string.toLowerCase().replace(/_/g, '-').split('-');
+ let hname = '';
+ const parts = name.length;
+ let part: number;
+
+ for (part = 0; part < parts; part++) {
+ if (part !== 0) {
+ hname += '-';
+ }
+ hname += name[part]!.charAt(0).toUpperCase() + name[part]!.substring(1);
+ }
+ if (exceptions[hname]) {
+ hname = exceptions[hname]!;
+ }
+
+ return hname;
+};
+
+export const sipErrorCause = (status_code: number): causes => {
+ for (const cause in SIP_ERROR_CAUSES) {
+ if (SIP_ERROR_CAUSES[cause]!.includes(status_code)) {
+ return (causes as Record<string, causes>)[cause]!;
+ }
+ }
+
+ return causes.SIP_FAILURE_CODE;
+};
+
+/**
+ * Generate a random Test-Net IP (https://tools.ietf.org/html/rfc5735)
+ */
+export const getRandomTestNetIP = (): string => {
+ function getOctet(from: number, to: number): number {
+ return Math.floor(Math.random() * (to - from + 1) + from);
+ }
+
+ return `192.0.2.${getOctet(1, 254)}`;
+};
+
+// MD5 (Message-Digest Algorithm) https://www.webtoolkit.info.
+export const calculateMD5 = (string: string): string => {
+ function rotateLeft(lValue: number, iShiftBits: number): number {
+ return (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits));
+ }
+
+ function addUnsigned(lX: number, lY: number): number {
+ const lX8 = lX & 0x80000000;
+ const lY8 = lY & 0x80000000;
+ const lX4 = lX & 0x40000000;
+ const lY4 = lY & 0x40000000;
+ const lResult = (lX & 0x3fffffff) + (lY & 0x3fffffff);
+
+ if (lX4 & lY4) {
+ return lResult ^ 0x80000000 ^ lX8 ^ lY8;
+ }
+ if (lX4 | lY4) {
+ if (lResult & 0x40000000) {
+ return lResult ^ 0xc0000000 ^ lX8 ^ lY8;
+ } else {
+ return lResult ^ 0x40000000 ^ lX8 ^ lY8;
+ }
+ } else {
+ return lResult ^ lX8 ^ lY8;
+ }
+ }
+
+ function doF(x: number, y: number, z: number): number {
+ return (x & y) | (~x & z);
+ }
+
+ function doG(x: number, y: number, z: number): number {
+ return (x & z) | (y & ~z);
+ }
+
+ function doH(x: number, y: number, z: number): number {
+ return x ^ y ^ z;
+ }
+
+ function doI(x: number, y: number, z: number): number {
+ return y ^ (x | ~z);
+ }
+
+ function doFF(
+ a: number,
+ b: number,
+ c: number,
+ d: number,
+ x: number,
+ s: number,
+ ac: number
+ ): number {
+ a = addUnsigned(a, addUnsigned(addUnsigned(doF(b, c, d), x), ac));
+
+ return addUnsigned(rotateLeft(a, s), b);
+ }
+
+ function doGG(
+ a: number,
+ b: number,
+ c: number,
+ d: number,
+ x: number,
+ s: number,
+ ac: number
+ ): number {
+ a = addUnsigned(a, addUnsigned(addUnsigned(doG(b, c, d), x), ac));
+
+ return addUnsigned(rotateLeft(a, s), b);
+ }
+
+ function doHH(
+ a: number,
+ b: number,
+ c: number,
+ d: number,
+ x: number,
+ s: number,
+ ac: number
+ ): number {
+ a = addUnsigned(a, addUnsigned(addUnsigned(doH(b, c, d), x), ac));
+
+ return addUnsigned(rotateLeft(a, s), b);
+ }
+
+ function doII(
+ a: number,
+ b: number,
+ c: number,
+ d: number,
+ x: number,
+ s: number,
+ ac: number
+ ): number {
+ a = addUnsigned(a, addUnsigned(addUnsigned(doI(b, c, d), x), ac));
+
+ return addUnsigned(rotateLeft(a, s), b);
+ }
+
+ function convertToWordArray(str: string): number[] {
+ let lWordCount: number;
+ const lMessageLength = str.length;
+ const lNumberOfWords_temp1 = lMessageLength + 8;
+ const lNumberOfWords_temp2 =
+ (lNumberOfWords_temp1 - (lNumberOfWords_temp1 % 64)) / 64;
+ const lNumberOfWords = (lNumberOfWords_temp2 + 1) * 16;
+ const lWordArray: number[] = new Array(lNumberOfWords - 1);
+ let lBytePosition = 0;
+ let lByteCount = 0;
+
+ while (lByteCount < lMessageLength) {
+ lWordCount = (lByteCount - (lByteCount % 4)) / 4;
+ lBytePosition = (lByteCount % 4) * 8;
+ lWordArray[lWordCount] =
+ (lWordArray[lWordCount] ?? 0) |
+ (str.charCodeAt(lByteCount) << lBytePosition);
+ lByteCount++;
+ }
+ lWordCount = (lByteCount - (lByteCount % 4)) / 4;
+ lBytePosition = (lByteCount % 4) * 8;
+ lWordArray[lWordCount] =
+ (lWordArray[lWordCount] ?? 0) | (0x80 << lBytePosition);
+ lWordArray[lNumberOfWords - 2] = lMessageLength << 3;
+ lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29;
+
+ return lWordArray;
+ }
+
+ function wordToHex(lValue: number): string {
+ let wordToHexValue = '',
+ wordToHexValue_temp = '',
+ lByte,
+ lCount;
+
+ for (lCount = 0; lCount <= 3; lCount++) {
+ lByte = (lValue >>> (lCount * 8)) & 255;
+ wordToHexValue_temp = `0${lByte.toString(16)}`;
+ wordToHexValue =
+ wordToHexValue +
+ wordToHexValue_temp.substr(wordToHexValue_temp.length - 2, 2);
+ }
+
+ return wordToHexValue;
+ }
+
+ function utf8Encode(str: string): string {
+ let utftext = '';
+
+ for (let n = 0; n < str.length; n++) {
+ const c = str.charCodeAt(n);
+
+ if (c < 128) {
+ utftext += String.fromCharCode(c);
+ } else if (c > 127 && c < 2048) {
+ utftext += String.fromCharCode((c >> 6) | 192);
+ utftext += String.fromCharCode((c & 63) | 128);
+ } else {
+ utftext += String.fromCharCode((c >> 12) | 224);
+ utftext += String.fromCharCode(((c >> 6) & 63) | 128);
+ utftext += String.fromCharCode((c & 63) | 128);
+ }
+ }
+
+ return utftext;
+ }
+
+ let x: number[] = [];
+ let k, AA, BB, CC, DD, a, b, c, d;
+ const S11 = 7,
+ S12 = 12,
+ S13 = 17,
+ S14 = 22;
+ const S21 = 5,
+ S22 = 9,
+ S23 = 14,
+ S24 = 20;
+ const S31 = 4,
+ S32 = 11,
+ S33 = 16,
+ S34 = 23;
+ const S41 = 6,
+ S42 = 10,
+ S43 = 15,
+ S44 = 21;
+ // NOTE: `x` is always allocated with the exact required length, so the `?? 0`
+ // fallback is never taken at runtime. It just satisfies
+ // `noUncheckedIndexedAccess`.
+ const w = (i: number): number => x[i] ?? 0;
+
+ string = utf8Encode(string);
+
+ x = convertToWordArray(string);
+
+ a = 0x67452301;
+ b = 0xefcdab89;
+ c = 0x98badcfe;
+ d = 0x10325476;
+
+ for (k = 0; k < x.length; k += 16) {
+ AA = a;
+ BB = b;
+ CC = c;
+ DD = d;
+ a = doFF(a, b, c, d, w(k + 0), S11, 0xd76aa478);
+ d = doFF(d, a, b, c, w(k + 1), S12, 0xe8c7b756);
+ c = doFF(c, d, a, b, w(k + 2), S13, 0x242070db);
+ b = doFF(b, c, d, a, w(k + 3), S14, 0xc1bdceee);
+ a = doFF(a, b, c, d, w(k + 4), S11, 0xf57c0faf);
+ d = doFF(d, a, b, c, w(k + 5), S12, 0x4787c62a);
+ c = doFF(c, d, a, b, w(k + 6), S13, 0xa8304613);
+ b = doFF(b, c, d, a, w(k + 7), S14, 0xfd469501);
+ a = doFF(a, b, c, d, w(k + 8), S11, 0x698098d8);
+ d = doFF(d, a, b, c, w(k + 9), S12, 0x8b44f7af);
+ c = doFF(c, d, a, b, w(k + 10), S13, 0xffff5bb1);
+ b = doFF(b, c, d, a, w(k + 11), S14, 0x895cd7be);
+ a = doFF(a, b, c, d, w(k + 12), S11, 0x6b901122);
+ d = doFF(d, a, b, c, w(k + 13), S12, 0xfd987193);
+ c = doFF(c, d, a, b, w(k + 14), S13, 0xa679438e);
+ b = doFF(b, c, d, a, w(k + 15), S14, 0x49b40821);
+ a = doGG(a, b, c, d, w(k + 1), S21, 0xf61e2562);
+ d = doGG(d, a, b, c, w(k + 6), S22, 0xc040b340);
+ c = doGG(c, d, a, b, w(k + 11), S23, 0x265e5a51);
+ b = doGG(b, c, d, a, w(k + 0), S24, 0xe9b6c7aa);
+ a = doGG(a, b, c, d, w(k + 5), S21, 0xd62f105d);
+ d = doGG(d, a, b, c, w(k + 10), S22, 0x2441453);
+ c = doGG(c, d, a, b, w(k + 15), S23, 0xd8a1e681);
+ b = doGG(b, c, d, a, w(k + 4), S24, 0xe7d3fbc8);
+ a = doGG(a, b, c, d, w(k + 9), S21, 0x21e1cde6);
+ d = doGG(d, a, b, c, w(k + 14), S22, 0xc33707d6);
+ c = doGG(c, d, a, b, w(k + 3), S23, 0xf4d50d87);
+ b = doGG(b, c, d, a, w(k + 8), S24, 0x455a14ed);
+ a = doGG(a, b, c, d, w(k + 13), S21, 0xa9e3e905);
+ d = doGG(d, a, b, c, w(k + 2), S22, 0xfcefa3f8);
+ c = doGG(c, d, a, b, w(k + 7), S23, 0x676f02d9);
+ b = doGG(b, c, d, a, w(k + 12), S24, 0x8d2a4c8a);
+ a = doHH(a, b, c, d, w(k + 5), S31, 0xfffa3942);
+ d = doHH(d, a, b, c, w(k + 8), S32, 0x8771f681);
+ c = doHH(c, d, a, b, w(k + 11), S33, 0x6d9d6122);
+ b = doHH(b, c, d, a, w(k + 14), S34, 0xfde5380c);
+ a = doHH(a, b, c, d, w(k + 1), S31, 0xa4beea44);
+ d = doHH(d, a, b, c, w(k + 4), S32, 0x4bdecfa9);
+ c = doHH(c, d, a, b, w(k + 7), S33, 0xf6bb4b60);
+ b = doHH(b, c, d, a, w(k + 10), S34, 0xbebfbc70);
+ a = doHH(a, b, c, d, w(k + 13), S31, 0x289b7ec6);
+ d = doHH(d, a, b, c, w(k + 0), S32, 0xeaa127fa);
+ c = doHH(c, d, a, b, w(k + 3), S33, 0xd4ef3085);
+ b = doHH(b, c, d, a, w(k + 6), S34, 0x4881d05);
+ a = doHH(a, b, c, d, w(k + 9), S31, 0xd9d4d039);
+ d = doHH(d, a, b, c, w(k + 12), S32, 0xe6db99e5);
+ c = doHH(c, d, a, b, w(k + 15), S33, 0x1fa27cf8);
+ b = doHH(b, c, d, a, w(k + 2), S34, 0xc4ac5665);
+ a = doII(a, b, c, d, w(k + 0), S41, 0xf4292244);
+ d = doII(d, a, b, c, w(k + 7), S42, 0x432aff97);
+ c = doII(c, d, a, b, w(k + 14), S43, 0xab9423a7);
+ b = doII(b, c, d, a, w(k + 5), S44, 0xfc93a039);
+ a = doII(a, b, c, d, w(k + 12), S41, 0x655b59c3);
+ d = doII(d, a, b, c, w(k + 3), S42, 0x8f0ccc92);
+ c = doII(c, d, a, b, w(k + 10), S43, 0xffeff47d);
+ b = doII(b, c, d, a, w(k + 1), S44, 0x85845dd1);
+ a = doII(a, b, c, d, w(k + 8), S41, 0x6fa87e4f);
+ d = doII(d, a, b, c, w(k + 15), S42, 0xfe2ce6e0);
+ c = doII(c, d, a, b, w(k + 6), S43, 0xa3014314);
+ b = doII(b, c, d, a, w(k + 13), S44, 0x4e0811a1);
+ a = doII(a, b, c, d, w(k + 4), S41, 0xf7537e82);
+ d = doII(d, a, b, c, w(k + 11), S42, 0xbd3af235);
+ c = doII(c, d, a, b, w(k + 2), S43, 0x2ad7d2bb);
+ b = doII(b, c, d, a, w(k + 9), S44, 0xeb86d391);
+ a = addUnsigned(a, AA);
+ b = addUnsigned(b, BB);
+ c = addUnsigned(c, CC);
+ d = addUnsigned(d, DD);
+ }
+
+ const temp = wordToHex(a) + wordToHex(b) + wordToHex(c) + wordToHex(d);
+
+ return temp.toLowerCase();
+};
+
+export const closeMediaStream = (stream?: MediaStream): void => {
+ if (!stream) {
+ return;
+ }
+
+ // Latest spec states that MediaStream has no stop() method and instead must
+ // call stop() on every MediaStreamTrack.
+ try {
+ let tracks;
+
+ if (stream.getTracks) {
+ tracks = stream.getTracks();
+ for (const track of tracks) {
+ track.stop();
+ }
+ } else {
+ tracks = stream.getAudioTracks();
+ for (const track of tracks) {
+ track.stop();
+ }
+ tracks = stream.getVideoTracks();
+ for (const track of tracks) {
+ track.stop();
+ }
+ }
+ } catch (error) {
+ // Deprecated by the spec, but still in use.
+ // NOTE: In Temasys IE plugin stream.stop is a callable 'object'.
+ const legacyStream = stream as MediaStream & { stop?: unknown };
+
+ if (
+ typeof legacyStream.stop === 'function' ||
+ typeof legacyStream.stop === 'object'
+ ) {
+ (legacyStream.stop as () => void)();
+ }
+ }
+};
+
+export const cloneArray = <T = unknown>(array?: T[] | null): T[] => {
+ return array?.slice() || [];
+};
+
+export const cloneObject = <T extends object>(
+ obj?: T | null,
+ fallback: T = {} as T
+): T => {
+ return (obj && Object.assign({}, obj)) || fallback;
+};
diff --git a/src/WebSocketInterface.d.ts b/src/WebSocketInterface.d.ts
deleted file mode 100644
index 4e37a16..0000000
--- a/src/WebSocketInterface.d.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { Socket } from './Socket';
-
-export class WebSocketInterface extends Socket {
- constructor(url: string);
-}
diff --git a/src/WebSocketInterface.js b/src/WebSocketInterface.js
deleted file mode 100644
index 852de85..0000000
--- a/src/WebSocketInterface.js
+++ /dev/null
@@ -1,144 +0,0 @@
-const { Logger } = require('./Logger');
-const Grammar = require('./Grammar');
-
-const logger = new Logger('WebSocketInterface');
-
-module.exports = class WebSocketInterface {
- constructor(url) {
- logger.debug('new() [url:"%s"]', url);
-
- this._url = url;
- this._sip_uri = null;
- this._via_transport = null;
- this._ws = null;
-
- const parsed_url = Grammar.parse(url, 'absoluteURI');
-
- if (parsed_url === -1) {
- logger.warn(`invalid WebSocket URI: ${url}`);
- throw new TypeError(`Invalid argument: ${url}`);
- } else if (parsed_url.scheme !== 'wss' && parsed_url.scheme !== 'ws') {
- logger.warn(`invalid WebSocket URI scheme: ${parsed_url.scheme}`);
- throw new TypeError(`Invalid argument: ${url}`);
- } else {
- this._sip_uri = `sip:${parsed_url.host}${parsed_url.port ? `:${parsed_url.port}` : ''};transport=ws`;
- this._via_transport = parsed_url.scheme.toUpperCase();
- }
- }
-
- get via_transport() {
- return this._via_transport;
- }
-
- set via_transport(value) {
- this._via_transport = value.toUpperCase();
- }
-
- get sip_uri() {
- return this._sip_uri;
- }
-
- get url() {
- return this._url;
- }
-
- connect() {
- logger.debug('connect()');
-
- if (this.isConnected()) {
- logger.debug(`WebSocket ${this._url} is already connected`);
-
- return;
- } else if (this.isConnecting()) {
- logger.debug(`WebSocket ${this._url} is connecting`);
-
- return;
- }
-
- if (this._ws) {
- this.disconnect();
- }
-
- logger.debug(`connecting to WebSocket ${this._url}`);
-
- try {
- this._ws = new WebSocket(this._url, 'sip');
-
- this._ws.binaryType = 'arraybuffer';
-
- this._ws.onopen = this._onOpen.bind(this);
- this._ws.onclose = this._onClose.bind(this);
- this._ws.onmessage = this._onMessage.bind(this);
- this._ws.onerror = this._onError.bind(this);
- } catch (error) {
- this._onError(error);
- }
- }
-
- disconnect() {
- logger.debug('disconnect()');
-
- if (this._ws) {
- // Unbind websocket event callbacks.
- this._ws.onopen = () => {};
- this._ws.onclose = () => {};
- this._ws.onmessage = () => {};
- this._ws.onerror = () => {};
-
- this._ws.close();
- this._ws = null;
- }
- }
-
- send(message) {
- logger.debug('send()');
-
- if (this.isConnected()) {
- this._ws.send(message);
-
- return true;
- } else {
- logger.warn('unable to send message, WebSocket is not open');
-
- return false;
- }
- }
-
- isConnected() {
- return this._ws && this._ws.readyState === this._ws.OPEN;
- }
-
- isConnecting() {
- return this._ws && this._ws.readyState === this._ws.CONNECTING;
- }
-
- /**
- * WebSocket Event Handlers
- */
-
- _onOpen() {
- logger.debug(`WebSocket ${this._url} connected`);
-
- this.onconnect();
- }
-
- _onClose({ wasClean, code, reason }) {
- logger.debug(`WebSocket ${this._url} closed`);
-
- if (wasClean === false) {
- logger.debug('WebSocket abrupt disconnection');
- }
-
- this.ondisconnect(!wasClean, code, reason);
- }
-
- _onMessage({ data }) {
- logger.debug('received WebSocket message');
-
- this.ondata(data);
- }
-
- _onError(e) {
- logger.warn(`WebSocket ${this._url} error: `, e);
- }
-};
diff --git a/src/WebSocketInterface.ts b/src/WebSocketInterface.ts
new file mode 100644
index 0000000..733f824
--- /dev/null
+++ b/src/WebSocketInterface.ts
@@ -0,0 +1,157 @@
+import { Logger } from './Logger';
+import * as Grammar from './Grammar';
+import type { Socket } from './Socket';
+
+const logger = new Logger('WebSocketInterface');
+
+export class WebSocketInterface implements Socket {
+ #url: string;
+ #sip_uri: string;
+ #via_transport: string;
+ #ws: WebSocket | null;
+
+ // NOTE: These are assigned by `Transport` once the socket is in use.
+ declare onconnect: () => void;
+ declare ondisconnect: (
+ error: boolean,
+ code?: number,
+ reason?: string
+ ) => void;
+ declare ondata: <T>(event: T) => void;
+
+ constructor(url: string) {
+ logger.debug('new() [url:"%s"]', url);
+
+ this.#url = url;
+ this.#ws = null;
+
+ const parsed_url = Grammar.parse(url, 'absoluteURI');
+
+ if (parsed_url === -1) {
+ logger.warn(`invalid WebSocket URI: ${url}`);
+ throw new TypeError(`Invalid argument: ${url}`);
+ } else if (parsed_url.scheme !== 'wss' && parsed_url.scheme !== 'ws') {
+ logger.warn(`invalid WebSocket URI scheme: ${parsed_url.scheme}`);
+ throw new TypeError(`Invalid argument: ${url}`);
+ } else {
+ this.#sip_uri = `sip:${parsed_url.host}${parsed_url.port ? `:${parsed_url.port}` : ''};transport=ws`;
+ this.#via_transport = parsed_url.scheme.toUpperCase();
+ }
+ }
+
+ get via_transport(): string {
+ return this.#via_transport;
+ }
+
+ set via_transport(value: string) {
+ this.#via_transport = value.toUpperCase();
+ }
+
+ get sip_uri(): string {
+ return this.#sip_uri;
+ }
+
+ get url(): string {
+ return this.#url;
+ }
+
+ connect(): void {
+ logger.debug('connect()');
+
+ if (this.isConnected()) {
+ logger.debug(`WebSocket ${this.#url} is already connected`);
+
+ return;
+ } else if (this.isConnecting()) {
+ logger.debug(`WebSocket ${this.#url} is connecting`);
+
+ return;
+ }
+
+ if (this.#ws) {
+ this.disconnect();
+ }
+
+ logger.debug(`connecting to WebSocket ${this.#url}`);
+
+ try {
+ this.#ws = new WebSocket(this.#url, 'sip');
+
+ this.#ws.binaryType = 'arraybuffer';
+
+ this.#ws.onopen = this.#onOpen.bind(this);
+ this.#ws.onclose = this.#onClose.bind(this);
+ this.#ws.onmessage = this.#onMessage.bind(this);
+ this.#ws.onerror = this.#onError.bind(this);
+ } catch (error) {
+ this.#onError(error);
+ }
+ }
+
+ disconnect(): void {
+ logger.debug('disconnect()');
+
+ if (this.#ws) {
+ // Unbind websocket event callbacks.
+ this.#ws.onopen = () => {};
+ this.#ws.onclose = () => {};
+ this.#ws.onmessage = () => {};
+ this.#ws.onerror = () => {};
+
+ this.#ws.close();
+ this.#ws = null;
+ }
+ }
+
+ send(message: string | ArrayBufferLike | Blob | ArrayBufferView): boolean {
+ logger.debug('send()');
+
+ if (this.#ws && this.isConnected()) {
+ this.#ws.send(message);
+
+ return true;
+ } else {
+ logger.warn('unable to send message, WebSocket is not open');
+
+ return false;
+ }
+ }
+
+ isConnected(): boolean {
+ return this.#ws !== null && this.#ws.readyState === this.#ws.OPEN;
+ }
+
+ isConnecting(): boolean {
+ return this.#ws !== null && this.#ws.readyState === this.#ws.CONNECTING;
+ }
+
+ /**
+ * WebSocket Event Handlers
+ */
+
+ #onOpen(): void {
+ logger.debug(`WebSocket ${this.#url} connected`);
+
+ this.onconnect();
+ }
+
+ #onClose({ wasClean, code, reason }: CloseEvent): void {
+ logger.debug(`WebSocket ${this.#url} closed`);
+
+ if (wasClean === false) {
+ logger.debug('WebSocket abrupt disconnection');
+ }
+
+ this.ondisconnect(!wasClean, code, reason);
+ }
+
+ #onMessage({ data }: MessageEvent): void {
+ logger.debug('received WebSocket message');
+
+ this.ondata(data);
+ }
+
+ #onError(e: unknown): void {
+ logger.warn(`WebSocket ${this.#url} error: `, e);
+ }
+}