UNPKG

sip.js

Version:

A SIP library for JavaScript

303 lines (302 loc) 11.4 kB
/* eslint-disable @typescript-eslint/no-explicit-any */ import { Parameters } from "./parameters.js"; /** * URI. * @public */ export class URI extends Parameters { /** * Constructor * @param scheme - * @param user - * @param host - * @param port - * @param parameters - * @param headers - */ constructor(scheme = "sip", user, host, port, parameters, headers) { super(parameters || {}); this.headers = {}; // Checks if (!host) { throw new TypeError('missing or invalid "host" parameter'); } for (const header in headers) { // eslint-disable-next-line no-prototype-builtins if (headers.hasOwnProperty(header)) { this.setHeader(header, headers[header]); } } // Raw URI this.raw = { scheme, user, host, port }; // Normalized URI this.normal = { scheme: scheme.toLowerCase(), user, host: host.toLowerCase(), port }; } get scheme() { return this.normal.scheme; } set scheme(value) { this.raw.scheme = value; this.normal.scheme = value.toLowerCase(); } get user() { return this.normal.user; } set user(value) { this.normal.user = this.raw.user = value; } get host() { return this.normal.host; } set host(value) { this.raw.host = value; this.normal.host = value.toLowerCase(); } get aor() { return this.normal.user + "@" + this.normal.host; } get port() { return this.normal.port; } set port(value) { this.normal.port = this.raw.port = value === 0 ? value : value; } setHeader(name, value) { this.headers[this.headerize(name)] = (value instanceof Array) ? value : [value]; } getHeader(name) { if (name) { return this.headers[this.headerize(name)]; } } hasHeader(name) { // eslint-disable-next-line no-prototype-builtins return !!name && !!this.headers.hasOwnProperty(this.headerize(name)); } deleteHeader(header) { header = this.headerize(header); // eslint-disable-next-line no-prototype-builtins if (this.headers.hasOwnProperty(header)) { const value = this.headers[header]; delete this.headers[header]; return value; } } clearHeaders() { this.headers = {}; } clone() { return new URI(this._raw.scheme, this._raw.user || "", this._raw.host, this._raw.port, JSON.parse(JSON.stringify(this.parameters)), JSON.parse(JSON.stringify(this.headers))); } toRaw() { return this._toString(this._raw); } toString() { return this._toString(this._normal); } get _normal() { return this.normal; } get _raw() { return this.raw; } _toString(uri) { let uriString = uri.scheme + ":"; // add slashes if it's not a sip(s) URI if (!uri.scheme.toLowerCase().match("^sips?$")) { uriString += "//"; } if (uri.user) { uriString += this.escapeUser(uri.user) + "@"; } uriString += uri.host; if (uri.port || uri.port === 0) { uriString += ":" + uri.port; } for (const parameter in this.parameters) { // eslint-disable-next-line no-prototype-builtins if (this.parameters.hasOwnProperty(parameter)) { uriString += ";" + parameter; if (this.parameters[parameter] !== null) { uriString += "=" + this.parameters[parameter]; } } } const headers = []; for (const header in this.headers) { // eslint-disable-next-line no-prototype-builtins if (this.headers.hasOwnProperty(header)) { // eslint-disable-next-line @typescript-eslint/no-for-in-array for (const idx in this.headers[header]) { // eslint-disable-next-line no-prototype-builtins if (this.headers[header].hasOwnProperty(idx)) { headers.push(header + "=" + this.headers[header][idx]); } } } } if (headers.length > 0) { uriString += "?" + headers.join("&"); } return uriString; } /* * Hex-escape a SIP URI user. * @private * @param {String} user */ escapeUser(user) { let decodedUser; // FIXME: This is called by toString above which should never throw, but // decodeURIComponent can throw and I've seen one case in production where // it did throw resulting in a cascading failure. This class should be // fixed so that decodeURIComponent is not called at this point (in toString). // The user should be decoded when the URI is constructor or some other // place where we can catch the error before the URI is created or somesuch. // eslint-disable-next-line no-useless-catch try { decodedUser = decodeURIComponent(user); } catch (error) { throw error; } // Don't hex-escape ':' (%3A), '+' (%2B), '?' (%3F"), '/' (%2F). return encodeURIComponent(decodedUser) .replace(/%3A/ig, ":") .replace(/%2B/ig, "+") .replace(/%3F/ig, "?") .replace(/%2F/ig, "/"); } headerize(str) { const exceptions = { "Call-Id": "Call-ID", "Cseq": "CSeq", "Min-Se": "Min-SE", "Rack": "RAck", "Rseq": "RSeq", "Www-Authenticate": "WWW-Authenticate", }; const name = str.toLowerCase().replace(/_/g, "-").split("-"); const parts = name.length; let hname = ""; for (let 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; } } /** * Returns true if URIs are equivalent per RFC 3261 Section 19.1.4. * @param a - URI to compare * @param b - URI to compare * * @remarks * 19.1.4 URI Comparison * Some operations in this specification require determining whether two * SIP or SIPS URIs are equivalent. * * https://tools.ietf.org/html/rfc3261#section-19.1.4 * @internal */ export function equivalentURI(a, b) { // o A SIP and SIPS URI are never equivalent. if (a.scheme !== b.scheme) { return false; } // o Comparison of the userinfo of SIP and SIPS URIs is case- // sensitive. This includes userinfo containing passwords or // formatted as telephone-subscribers. Comparison of all other // components of the URI is case-insensitive unless explicitly // defined otherwise. // // o The ordering of parameters and header fields is not significant // in comparing SIP and SIPS URIs. // // o Characters other than those in the "reserved" set (see RFC 2396 // [5]) are equivalent to their ""%" HEX HEX" encoding. // // o An IP address that is the result of a DNS lookup of a host name // does not match that host name. // // o For two URIs to be equal, the user, password, host, and port // components must match. // // A URI omitting the user component will not match a URI that // includes one. A URI omitting the password component will not // match a URI that includes one. // // A URI omitting any component with a default value will not // match a URI explicitly containing that component with its // default value. For instance, a URI omitting the optional port // component will not match a URI explicitly declaring port 5060. // The same is true for the transport-parameter, ttl-parameter, // user-parameter, and method components. // // Defining sip:user@host to not be equivalent to // sip:user@host:5060 is a change from RFC 2543. When deriving // addresses from URIs, equivalent addresses are expected from // equivalent URIs. The URI sip:user@host:5060 will always // resolve to port 5060. The URI sip:user@host may resolve to // other ports through the DNS SRV mechanisms detailed in [4]. // FIXME: TODO: // - character compared to hex encoding is not handled // - password does not exist on URI currently if (a.user !== b.user || a.host !== b.host || a.port !== b.port) { return false; } // o URI uri-parameter components are compared as follows: function compareParameters(a, b) { // - Any uri-parameter appearing in both URIs must match. const parameterKeysA = Object.keys(a.parameters); const parameterKeysB = Object.keys(b.parameters); const intersection = parameterKeysA.filter(x => parameterKeysB.includes(x)); if (!intersection.every(key => a.parameters[key] === b.parameters[key])) { return false; } // - A user, ttl, or method uri-parameter appearing in only one // URI never matches, even if it contains the default value. if (!["user", "ttl", "method", "transport"].every(key => a.hasParam(key) && b.hasParam(key) || !a.hasParam(key) && !b.hasParam(key))) { return false; } // - A URI that includes an maddr parameter will not match a URI // that contains no maddr parameter. if (!["maddr"].every(key => a.hasParam(key) && b.hasParam(key) || !a.hasParam(key) && !b.hasParam(key))) { return false; } // - All other uri-parameters appearing in only one URI are // ignored when comparing the URIs. return true; } if (!compareParameters(a, b)) { return false; } // o URI header components are never ignored. Any present header // component MUST be present in both URIs and match for the URIs // to match. The matching rules are defined for each header field // in Section 20. const headerKeysA = Object.keys(a.headers); const headerKeysB = Object.keys(b.headers); // No need to check if no headers if (headerKeysA.length !== 0 || headerKeysB.length !== 0) { // Must have same number of headers if (headerKeysA.length !== headerKeysB.length) { return false; } // Must have same headers const intersection = headerKeysA.filter(x => headerKeysB.includes(x)); if (intersection.length !== headerKeysB.length) { return false; } // FIXME: Not to spec. But perhaps not worth fixing? // Must have same header values // It seems too much to consider multiple headers with same name. // It seems too much to compare two header params according to the rule of each header. // We'll assume a single header and compare them string to string... if (!intersection.every(key => a.headers[key].length && b.headers[key].length && a.headers[key][0] === b.headers[key][0])) { return false; } } return true; }