| 1 | 'use strict'
|
|---|
| 2 |
|
|---|
| 3 | const { isUUID } = require('./utils')
|
|---|
| 4 | const URN_REG = /([\da-z][\d\-a-z]{0,31}):((?:[\w!$'()*+,\-.:;=@]|%[\da-f]{2})+)/iu
|
|---|
| 5 |
|
|---|
| 6 | const supportedSchemeNames = /** @type {const} */ (['http', 'https', 'ws',
|
|---|
| 7 | 'wss', 'urn', 'urn:uuid'])
|
|---|
| 8 |
|
|---|
| 9 | /** @typedef {supportedSchemeNames[number]} SchemeName */
|
|---|
| 10 |
|
|---|
| 11 | /**
|
|---|
| 12 | * @param {string} name
|
|---|
| 13 | * @returns {name is SchemeName}
|
|---|
| 14 | */
|
|---|
| 15 | function isValidSchemeName (name) {
|
|---|
| 16 | return supportedSchemeNames.indexOf(/** @type {*} */ (name)) !== -1
|
|---|
| 17 | }
|
|---|
| 18 |
|
|---|
| 19 | /**
|
|---|
| 20 | * @callback SchemeFn
|
|---|
| 21 | * @param {import('../types/index').URIComponent} component
|
|---|
| 22 | * @param {import('../types/index').Options} options
|
|---|
| 23 | * @returns {import('../types/index').URIComponent}
|
|---|
| 24 | */
|
|---|
| 25 |
|
|---|
| 26 | /**
|
|---|
| 27 | * @typedef {Object} SchemeHandler
|
|---|
| 28 | * @property {SchemeName} scheme - The scheme name.
|
|---|
| 29 | * @property {boolean} [domainHost] - Indicates if the scheme supports domain hosts.
|
|---|
| 30 | * @property {SchemeFn} parse - Function to parse the URI component for this scheme.
|
|---|
| 31 | * @property {SchemeFn} serialize - Function to serialize the URI component for this scheme.
|
|---|
| 32 | * @property {boolean} [skipNormalize] - Indicates if normalization should be skipped for this scheme.
|
|---|
| 33 | * @property {boolean} [absolutePath] - Indicates if the scheme uses absolute paths.
|
|---|
| 34 | * @property {boolean} [unicodeSupport] - Indicates if the scheme supports Unicode.
|
|---|
| 35 | */
|
|---|
| 36 |
|
|---|
| 37 | /**
|
|---|
| 38 | * @param {import('../types/index').URIComponent} wsComponent
|
|---|
| 39 | * @returns {boolean}
|
|---|
| 40 | */
|
|---|
| 41 | function wsIsSecure (wsComponent) {
|
|---|
| 42 | if (wsComponent.secure === true) {
|
|---|
| 43 | return true
|
|---|
| 44 | } else if (wsComponent.secure === false) {
|
|---|
| 45 | return false
|
|---|
| 46 | } else if (wsComponent.scheme) {
|
|---|
| 47 | return (
|
|---|
| 48 | wsComponent.scheme.length === 3 &&
|
|---|
| 49 | (wsComponent.scheme[0] === 'w' || wsComponent.scheme[0] === 'W') &&
|
|---|
| 50 | (wsComponent.scheme[1] === 's' || wsComponent.scheme[1] === 'S') &&
|
|---|
| 51 | (wsComponent.scheme[2] === 's' || wsComponent.scheme[2] === 'S')
|
|---|
| 52 | )
|
|---|
| 53 | } else {
|
|---|
| 54 | return false
|
|---|
| 55 | }
|
|---|
| 56 | }
|
|---|
| 57 |
|
|---|
| 58 | /** @type {SchemeFn} */
|
|---|
| 59 | function httpParse (component) {
|
|---|
| 60 | if (!component.host) {
|
|---|
| 61 | component.error = component.error || 'HTTP URIs must have a host.'
|
|---|
| 62 | }
|
|---|
| 63 |
|
|---|
| 64 | return component
|
|---|
| 65 | }
|
|---|
| 66 |
|
|---|
| 67 | /** @type {SchemeFn} */
|
|---|
| 68 | function httpSerialize (component) {
|
|---|
| 69 | const secure = String(component.scheme).toLowerCase() === 'https'
|
|---|
| 70 |
|
|---|
| 71 | // normalize the default port
|
|---|
| 72 | if (component.port === (secure ? 443 : 80) || component.port === '') {
|
|---|
| 73 | component.port = undefined
|
|---|
| 74 | }
|
|---|
| 75 |
|
|---|
| 76 | // normalize the empty path
|
|---|
| 77 | if (!component.path) {
|
|---|
| 78 | component.path = '/'
|
|---|
| 79 | }
|
|---|
| 80 |
|
|---|
| 81 | // NOTE: We do not parse query strings for HTTP URIs
|
|---|
| 82 | // as WWW Form Url Encoded query strings are part of the HTML4+ spec,
|
|---|
| 83 | // and not the HTTP spec.
|
|---|
| 84 |
|
|---|
| 85 | return component
|
|---|
| 86 | }
|
|---|
| 87 |
|
|---|
| 88 | /** @type {SchemeFn} */
|
|---|
| 89 | function wsParse (wsComponent) {
|
|---|
| 90 | // indicate if the secure flag is set
|
|---|
| 91 | wsComponent.secure = wsIsSecure(wsComponent)
|
|---|
| 92 |
|
|---|
| 93 | // construct resouce name
|
|---|
| 94 | wsComponent.resourceName = (wsComponent.path || '/') + (wsComponent.query ? '?' + wsComponent.query : '')
|
|---|
| 95 | wsComponent.path = undefined
|
|---|
| 96 | wsComponent.query = undefined
|
|---|
| 97 |
|
|---|
| 98 | return wsComponent
|
|---|
| 99 | }
|
|---|
| 100 |
|
|---|
| 101 | /** @type {SchemeFn} */
|
|---|
| 102 | function wsSerialize (wsComponent) {
|
|---|
| 103 | // normalize the default port
|
|---|
| 104 | if (wsComponent.port === (wsIsSecure(wsComponent) ? 443 : 80) || wsComponent.port === '') {
|
|---|
| 105 | wsComponent.port = undefined
|
|---|
| 106 | }
|
|---|
| 107 |
|
|---|
| 108 | // ensure scheme matches secure flag
|
|---|
| 109 | if (typeof wsComponent.secure === 'boolean') {
|
|---|
| 110 | wsComponent.scheme = (wsComponent.secure ? 'wss' : 'ws')
|
|---|
| 111 | wsComponent.secure = undefined
|
|---|
| 112 | }
|
|---|
| 113 |
|
|---|
| 114 | // reconstruct path from resource name
|
|---|
| 115 | if (wsComponent.resourceName) {
|
|---|
| 116 | const [path, query] = wsComponent.resourceName.split('?')
|
|---|
| 117 | wsComponent.path = (path && path !== '/' ? path : undefined)
|
|---|
| 118 | wsComponent.query = query
|
|---|
| 119 | wsComponent.resourceName = undefined
|
|---|
| 120 | }
|
|---|
| 121 |
|
|---|
| 122 | // forbid fragment component
|
|---|
| 123 | wsComponent.fragment = undefined
|
|---|
| 124 |
|
|---|
| 125 | return wsComponent
|
|---|
| 126 | }
|
|---|
| 127 |
|
|---|
| 128 | /** @type {SchemeFn} */
|
|---|
| 129 | function urnParse (urnComponent, options) {
|
|---|
| 130 | if (!urnComponent.path) {
|
|---|
| 131 | urnComponent.error = 'URN can not be parsed'
|
|---|
| 132 | return urnComponent
|
|---|
| 133 | }
|
|---|
| 134 | const matches = urnComponent.path.match(URN_REG)
|
|---|
| 135 | if (matches) {
|
|---|
| 136 | const scheme = options.scheme || urnComponent.scheme || 'urn'
|
|---|
| 137 | urnComponent.nid = matches[1].toLowerCase()
|
|---|
| 138 | urnComponent.nss = matches[2]
|
|---|
| 139 | const urnScheme = `${scheme}:${options.nid || urnComponent.nid}`
|
|---|
| 140 | const schemeHandler = getSchemeHandler(urnScheme)
|
|---|
| 141 | urnComponent.path = undefined
|
|---|
| 142 |
|
|---|
| 143 | if (schemeHandler) {
|
|---|
| 144 | urnComponent = schemeHandler.parse(urnComponent, options)
|
|---|
| 145 | }
|
|---|
| 146 | } else {
|
|---|
| 147 | urnComponent.error = urnComponent.error || 'URN can not be parsed.'
|
|---|
| 148 | }
|
|---|
| 149 |
|
|---|
| 150 | return urnComponent
|
|---|
| 151 | }
|
|---|
| 152 |
|
|---|
| 153 | /** @type {SchemeFn} */
|
|---|
| 154 | function urnSerialize (urnComponent, options) {
|
|---|
| 155 | if (urnComponent.nid === undefined) {
|
|---|
| 156 | throw new Error('URN without nid cannot be serialized')
|
|---|
| 157 | }
|
|---|
| 158 | const scheme = options.scheme || urnComponent.scheme || 'urn'
|
|---|
| 159 | const nid = urnComponent.nid.toLowerCase()
|
|---|
| 160 | const urnScheme = `${scheme}:${options.nid || nid}`
|
|---|
| 161 | const schemeHandler = getSchemeHandler(urnScheme)
|
|---|
| 162 |
|
|---|
| 163 | if (schemeHandler) {
|
|---|
| 164 | urnComponent = schemeHandler.serialize(urnComponent, options)
|
|---|
| 165 | }
|
|---|
| 166 |
|
|---|
| 167 | const uriComponent = urnComponent
|
|---|
| 168 | const nss = urnComponent.nss
|
|---|
| 169 | uriComponent.path = `${nid || options.nid}:${nss}`
|
|---|
| 170 |
|
|---|
| 171 | options.skipEscape = true
|
|---|
| 172 | return uriComponent
|
|---|
| 173 | }
|
|---|
| 174 |
|
|---|
| 175 | /** @type {SchemeFn} */
|
|---|
| 176 | function urnuuidParse (urnComponent, options) {
|
|---|
| 177 | const uuidComponent = urnComponent
|
|---|
| 178 | uuidComponent.uuid = uuidComponent.nss
|
|---|
| 179 | uuidComponent.nss = undefined
|
|---|
| 180 |
|
|---|
| 181 | if (!options.tolerant && (!uuidComponent.uuid || !isUUID(uuidComponent.uuid))) {
|
|---|
| 182 | uuidComponent.error = uuidComponent.error || 'UUID is not valid.'
|
|---|
| 183 | }
|
|---|
| 184 |
|
|---|
| 185 | return uuidComponent
|
|---|
| 186 | }
|
|---|
| 187 |
|
|---|
| 188 | /** @type {SchemeFn} */
|
|---|
| 189 | function urnuuidSerialize (uuidComponent) {
|
|---|
| 190 | const urnComponent = uuidComponent
|
|---|
| 191 | // normalize UUID
|
|---|
| 192 | urnComponent.nss = (uuidComponent.uuid || '').toLowerCase()
|
|---|
| 193 | return urnComponent
|
|---|
| 194 | }
|
|---|
| 195 |
|
|---|
| 196 | const http = /** @type {SchemeHandler} */ ({
|
|---|
| 197 | scheme: 'http',
|
|---|
| 198 | domainHost: true,
|
|---|
| 199 | parse: httpParse,
|
|---|
| 200 | serialize: httpSerialize
|
|---|
| 201 | })
|
|---|
| 202 |
|
|---|
| 203 | const https = /** @type {SchemeHandler} */ ({
|
|---|
| 204 | scheme: 'https',
|
|---|
| 205 | domainHost: http.domainHost,
|
|---|
| 206 | parse: httpParse,
|
|---|
| 207 | serialize: httpSerialize
|
|---|
| 208 | })
|
|---|
| 209 |
|
|---|
| 210 | const ws = /** @type {SchemeHandler} */ ({
|
|---|
| 211 | scheme: 'ws',
|
|---|
| 212 | domainHost: true,
|
|---|
| 213 | parse: wsParse,
|
|---|
| 214 | serialize: wsSerialize
|
|---|
| 215 | })
|
|---|
| 216 |
|
|---|
| 217 | const wss = /** @type {SchemeHandler} */ ({
|
|---|
| 218 | scheme: 'wss',
|
|---|
| 219 | domainHost: ws.domainHost,
|
|---|
| 220 | parse: ws.parse,
|
|---|
| 221 | serialize: ws.serialize
|
|---|
| 222 | })
|
|---|
| 223 |
|
|---|
| 224 | const urn = /** @type {SchemeHandler} */ ({
|
|---|
| 225 | scheme: 'urn',
|
|---|
| 226 | parse: urnParse,
|
|---|
| 227 | serialize: urnSerialize,
|
|---|
| 228 | skipNormalize: true
|
|---|
| 229 | })
|
|---|
| 230 |
|
|---|
| 231 | const urnuuid = /** @type {SchemeHandler} */ ({
|
|---|
| 232 | scheme: 'urn:uuid',
|
|---|
| 233 | parse: urnuuidParse,
|
|---|
| 234 | serialize: urnuuidSerialize,
|
|---|
| 235 | skipNormalize: true
|
|---|
| 236 | })
|
|---|
| 237 |
|
|---|
| 238 | const SCHEMES = /** @type {Record<SchemeName, SchemeHandler>} */ ({
|
|---|
| 239 | http,
|
|---|
| 240 | https,
|
|---|
| 241 | ws,
|
|---|
| 242 | wss,
|
|---|
| 243 | urn,
|
|---|
| 244 | 'urn:uuid': urnuuid
|
|---|
| 245 | })
|
|---|
| 246 |
|
|---|
| 247 | Object.setPrototypeOf(SCHEMES, null)
|
|---|
| 248 |
|
|---|
| 249 | /**
|
|---|
| 250 | * @param {string|undefined} scheme
|
|---|
| 251 | * @returns {SchemeHandler|undefined}
|
|---|
| 252 | */
|
|---|
| 253 | function getSchemeHandler (scheme) {
|
|---|
| 254 | return (
|
|---|
| 255 | scheme && (
|
|---|
| 256 | SCHEMES[/** @type {SchemeName} */ (scheme)] ||
|
|---|
| 257 | SCHEMES[/** @type {SchemeName} */(scheme.toLowerCase())])
|
|---|
| 258 | ) ||
|
|---|
| 259 | undefined
|
|---|
| 260 | }
|
|---|
| 261 |
|
|---|
| 262 | module.exports = {
|
|---|
| 263 | wsIsSecure,
|
|---|
| 264 | SCHEMES,
|
|---|
| 265 | isValidSchemeName,
|
|---|
| 266 | getSchemeHandler,
|
|---|
| 267 | }
|
|---|