268 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			268 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| '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,
 | |
| }
 |