'use strict' const { isUUID } = require('./utils') const URN_REG = /([\da-z][\d\-a-z]{0,31}):((?:[\w!$'()*+,\-.:;=@]|%[\da-f]{2})+)/iu const supportedSchemeNames = /** @type {const} */ (['http', 'https', 'ws', 'wss', 'urn', 'urn:uuid']) /** @typedef {supportedSchemeNames[number]} SchemeName */ /** * @param {string} name * @returns {name is SchemeName} */ function isValidSchemeName (name) { return supportedSchemeNames.indexOf(/** @type {*} */ (name)) !== -1 } /** * @callback SchemeFn * @param {import('../types/index').URIComponent} component * @param {import('../types/index').Options} options * @returns {import('../types/index').URIComponent} */ /** * @typedef {Object} SchemeHandler * @property {SchemeName} scheme - The scheme name. * @property {boolean} [domainHost] - Indicates if the scheme supports domain hosts. * @property {SchemeFn} parse - Function to parse the URI component for this scheme. * @property {SchemeFn} serialize - Function to serialize the URI component for this scheme. * @property {boolean} [skipNormalize] - Indicates if normalization should be skipped for this scheme. * @property {boolean} [absolutePath] - Indicates if the scheme uses absolute paths. * @property {boolean} [unicodeSupport] - Indicates if the scheme supports Unicode. */ /** * @param {import('../types/index').URIComponent} wsComponent * @returns {boolean} */ function wsIsSecure (wsComponent) { if (wsComponent.secure === true) { return true } else if (wsComponent.secure === false) { return false } else if (wsComponent.scheme) { return ( wsComponent.scheme.length === 3 && (wsComponent.scheme[0] === 'w' || wsComponent.scheme[0] === 'W') && (wsComponent.scheme[1] === 's' || wsComponent.scheme[1] === 'S') && (wsComponent.scheme[2] === 's' || wsComponent.scheme[2] === 'S') ) } else { return false } } /** @type {SchemeFn} */ function httpParse (component) { if (!component.host) { component.error = component.error || 'HTTP URIs must have a host.' } return component } /** @type {SchemeFn} */ function httpSerialize (component) { const secure = String(component.scheme).toLowerCase() === 'https' // normalize the default port if (component.port === (secure ? 443 : 80) || component.port === '') { component.port = undefined } // normalize the empty path if (!component.path) { component.path = '/' } // NOTE: We do not parse query strings for HTTP URIs // as WWW Form Url Encoded query strings are part of the HTML4+ spec, // and not the HTTP spec. return component } /** @type {SchemeFn} */ function wsParse (wsComponent) { // indicate if the secure flag is set wsComponent.secure = wsIsSecure(wsComponent) // construct resouce name wsComponent.resourceName = (wsComponent.path || '/') + (wsComponent.query ? '?' + wsComponent.query : '') wsComponent.path = undefined wsComponent.query = undefined return wsComponent } /** @type {SchemeFn} */ function wsSerialize (wsComponent) { // normalize the default port if (wsComponent.port === (wsIsSecure(wsComponent) ? 443 : 80) || wsComponent.port === '') { wsComponent.port = undefined } // ensure scheme matches secure flag if (typeof wsComponent.secure === 'boolean') { wsComponent.scheme = (wsComponent.secure ? 'wss' : 'ws') wsComponent.secure = undefined } // reconstruct path from resource name if (wsComponent.resourceName) { const [path, query] = wsComponent.resourceName.split('?') wsComponent.path = (path && path !== '/' ? path : undefined) wsComponent.query = query wsComponent.resourceName = undefined } // forbid fragment component wsComponent.fragment = undefined return wsComponent } /** @type {SchemeFn} */ function urnParse (urnComponent, options) { if (!urnComponent.path) { urnComponent.error = 'URN can not be parsed' return urnComponent } const matches = urnComponent.path.match(URN_REG) if (matches) { const scheme = options.scheme || urnComponent.scheme || 'urn' urnComponent.nid = matches[1].toLowerCase() urnComponent.nss = matches[2] const urnScheme = `${scheme}:${options.nid || urnComponent.nid}` const schemeHandler = getSchemeHandler(urnScheme) urnComponent.path = undefined if (schemeHandler) { urnComponent = schemeHandler.parse(urnComponent, options) } } else { urnComponent.error = urnComponent.error || 'URN can not be parsed.' } return urnComponent } /** @type {SchemeFn} */ function urnSerialize (urnComponent, options) { if (urnComponent.nid === undefined) { throw new Error('URN without nid cannot be serialized') } const scheme = options.scheme || urnComponent.scheme || 'urn' const nid = urnComponent.nid.toLowerCase() const urnScheme = `${scheme}:${options.nid || nid}` const schemeHandler = getSchemeHandler(urnScheme) if (schemeHandler) { urnComponent = schemeHandler.serialize(urnComponent, options) } const uriComponent = urnComponent const nss = urnComponent.nss uriComponent.path = `${nid || options.nid}:${nss}` options.skipEscape = true return uriComponent } /** @type {SchemeFn} */ function urnuuidParse (urnComponent, options) { const uuidComponent = urnComponent uuidComponent.uuid = uuidComponent.nss uuidComponent.nss = undefined if (!options.tolerant && (!uuidComponent.uuid || !isUUID(uuidComponent.uuid))) { uuidComponent.error = uuidComponent.error || 'UUID is not valid.' } return uuidComponent } /** @type {SchemeFn} */ function urnuuidSerialize (uuidComponent) { const urnComponent = uuidComponent // normalize UUID urnComponent.nss = (uuidComponent.uuid || '').toLowerCase() return urnComponent } const http = /** @type {SchemeHandler} */ ({ scheme: 'http', domainHost: true, parse: httpParse, serialize: httpSerialize }) const https = /** @type {SchemeHandler} */ ({ scheme: 'https', domainHost: http.domainHost, parse: httpParse, serialize: httpSerialize }) const ws = /** @type {SchemeHandler} */ ({ scheme: 'ws', domainHost: true, parse: wsParse, serialize: wsSerialize }) const wss = /** @type {SchemeHandler} */ ({ scheme: 'wss', domainHost: ws.domainHost, parse: ws.parse, serialize: ws.serialize }) const urn = /** @type {SchemeHandler} */ ({ scheme: 'urn', parse: urnParse, serialize: urnSerialize, skipNormalize: true }) const urnuuid = /** @type {SchemeHandler} */ ({ scheme: 'urn:uuid', parse: urnuuidParse, serialize: urnuuidSerialize, skipNormalize: true }) const SCHEMES = /** @type {Record} */ ({ http, https, ws, wss, urn, 'urn:uuid': urnuuid }) Object.setPrototypeOf(SCHEMES, null) /** * @param {string|undefined} scheme * @returns {SchemeHandler|undefined} */ function getSchemeHandler (scheme) { return ( scheme && ( SCHEMES[/** @type {SchemeName} */ (scheme)] || SCHEMES[/** @type {SchemeName} */(scheme.toLowerCase())]) ) || undefined } module.exports = { wsIsSecure, SCHEMES, isValidSchemeName, getSchemeHandler, }