You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
267 lines
7.0 KiB
267 lines
7.0 KiB
'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<SchemeName, SchemeHandler>} */ ({ |
|
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, |
|
}
|
|
|