fast-uri 3.1.4 → 3.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +235 -46
- package/lib/schemes.js +9 -4
- package/lib/utils.js +388 -91
- package/package.json +1 -1
- package/test/component-safe-serialization.test.js +105 -0
- package/test/equal.test.js +31 -3
- package/test/fixtures/uri-js-parse.json +2 -0
- package/test/ipv6-canonical.test.js +34 -0
- package/test/ipv6-validation.test.js +90 -0
- package/test/malformed-percent.test.js +77 -0
- package/test/malformed-urn.test.js +61 -0
- package/test/parse.test.js +7 -3
- package/test/query-fragment-normalization.test.js +33 -0
- package/test/reserved-path-normalization.test.js +109 -0
- package/test/scheme-validation.test.js +124 -0
- package/test/security-normalization.test.js +101 -0
- package/test/security.test.js +211 -3
- package/test/urn-full-input.test.js +29 -0
- package/test/websocket-query-preservation.test.js +24 -0
package/index.js
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding,
|
|
3
|
+
const { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, serializePathEncoding, normalizeQueryFragmentEncoding, encodeQuery, encodeFragment, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require('./lib/utils')
|
|
4
4
|
const { SCHEMES, getSchemeHandler } = require('./lib/schemes')
|
|
5
5
|
|
|
6
|
+
const VALID_SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*$/u
|
|
7
|
+
const MALFORMED_SCHEME_ERROR = 'URI scheme is malformed.'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} scheme
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
13
|
+
function decodeValidScheme (scheme) {
|
|
14
|
+
const decodedScheme = unescape(String(scheme))
|
|
15
|
+
if (!VALID_SCHEME.test(decodedScheme)) {
|
|
16
|
+
throw new TypeError(MALFORMED_SCHEME_ERROR)
|
|
17
|
+
}
|
|
18
|
+
return decodedScheme
|
|
19
|
+
}
|
|
20
|
+
|
|
6
21
|
/**
|
|
7
22
|
* @template {import('./types/index').URIComponent|string} T
|
|
8
23
|
* @param {T} uri
|
|
@@ -26,7 +41,50 @@ function normalize (uri, options) {
|
|
|
26
41
|
*/
|
|
27
42
|
function resolve (baseURI, relativeURI, options) {
|
|
28
43
|
const schemelessOptions = options ? Object.assign({ scheme: 'null' }, options) : { scheme: 'null' }
|
|
29
|
-
const
|
|
44
|
+
const {
|
|
45
|
+
parsed: baseParsed,
|
|
46
|
+
malformedAuthorityOrPort: baseMalformed,
|
|
47
|
+
malformedPercentEncoding: baseMalformedPercentEncoding,
|
|
48
|
+
malformedSchemeSpecific: baseMalformedSchemeSpecific,
|
|
49
|
+
malformedHost: baseMalformedHost,
|
|
50
|
+
malformedScheme: baseMalformedScheme
|
|
51
|
+
} = parseWithStatus(baseURI, schemelessOptions)
|
|
52
|
+
const {
|
|
53
|
+
parsed: relativeParsed,
|
|
54
|
+
malformedAuthorityOrPort: relativeMalformed,
|
|
55
|
+
malformedPercentEncoding: relativeMalformedPercentEncoding,
|
|
56
|
+
malformedSchemeSpecific: relativeMalformedSchemeSpecific,
|
|
57
|
+
malformedHost: relativeMalformedHost,
|
|
58
|
+
malformedScheme: relativeMalformedScheme
|
|
59
|
+
} = parseWithStatus(relativeURI, schemelessOptions)
|
|
60
|
+
if (
|
|
61
|
+
baseMalformed ||
|
|
62
|
+
relativeMalformed ||
|
|
63
|
+
baseMalformedPercentEncoding ||
|
|
64
|
+
relativeMalformedPercentEncoding ||
|
|
65
|
+
baseMalformedSchemeSpecific ||
|
|
66
|
+
relativeMalformedSchemeSpecific ||
|
|
67
|
+
baseMalformedHost ||
|
|
68
|
+
relativeMalformedHost ||
|
|
69
|
+
baseMalformedScheme ||
|
|
70
|
+
relativeMalformedScheme
|
|
71
|
+
) {
|
|
72
|
+
throw new Error(baseParsed.error || relativeParsed.error || 'URI is malformed.')
|
|
73
|
+
}
|
|
74
|
+
const resolved = resolveComponent(baseParsed, relativeParsed, schemelessOptions, true)
|
|
75
|
+
const resolvedSchemeHandler = getSchemeHandler((options && options.scheme) || resolved.scheme)
|
|
76
|
+
const resolvedHost = resolved.host
|
|
77
|
+
const resolvedHostIsIP = resolvedHost !== undefined && resolvedHost !== '' &&
|
|
78
|
+
(isIPv4(resolvedHost) || normalizeIPv6(resolvedHost).isIPV6)
|
|
79
|
+
canonicalizeHost(resolved, options || {}, resolvedSchemeHandler, resolvedHostIsIP)
|
|
80
|
+
// Percent escapes in an ASCII reg-name are encoded data. The WHATWG hostname
|
|
81
|
+
// parser can reject them even though fast-uri preserves them safely as RFC
|
|
82
|
+
// 3986 data. A raw non-ASCII host must still fail closed if conversion fails.
|
|
83
|
+
const encodedASCIIHost = resolvedHost && resolvedHost.indexOf('%') !== -1 &&
|
|
84
|
+
!/\P{ASCII}/u.test(resolvedHost)
|
|
85
|
+
if (resolved.error && !encodedASCIIHost) {
|
|
86
|
+
throw new Error(resolved.error)
|
|
87
|
+
}
|
|
30
88
|
schemelessOptions.skipEscape = true
|
|
31
89
|
return serialize(resolved, schemelessOptions)
|
|
32
90
|
}
|
|
@@ -109,7 +167,7 @@ function equal (uriA, uriB, options) {
|
|
|
109
167
|
const normalizedA = normalizeComparableURI(uriA, options)
|
|
110
168
|
const normalizedB = normalizeComparableURI(uriB, options)
|
|
111
169
|
|
|
112
|
-
return normalizedA !== undefined && normalizedB !== undefined && normalizedA
|
|
170
|
+
return normalizedA !== undefined && normalizedB !== undefined && normalizedA === normalizedB
|
|
113
171
|
}
|
|
114
172
|
|
|
115
173
|
/**
|
|
@@ -137,25 +195,30 @@ function serialize (cmpts, opts) {
|
|
|
137
195
|
const options = Object.assign({}, opts)
|
|
138
196
|
const uriTokens = []
|
|
139
197
|
|
|
198
|
+
if (component.scheme) {
|
|
199
|
+
component.scheme = decodeValidScheme(component.scheme)
|
|
200
|
+
}
|
|
201
|
+
|
|
140
202
|
// find scheme handler
|
|
141
203
|
const schemeHandler = getSchemeHandler(options.scheme || component.scheme)
|
|
142
204
|
|
|
143
205
|
// perform scheme specific serialization
|
|
144
206
|
if (schemeHandler && schemeHandler.serialize) schemeHandler.serialize(component, options)
|
|
145
207
|
|
|
208
|
+
const hasAuthority = component.userinfo !== undefined || component.host !== undefined || component.port !== undefined
|
|
209
|
+
const pathNoScheme = !options.skipEscape && component.scheme === undefined && !hasAuthority
|
|
210
|
+
|
|
146
211
|
if (component.path !== undefined) {
|
|
147
212
|
if (!options.skipEscape) {
|
|
148
|
-
component.path =
|
|
149
|
-
|
|
150
|
-
if (component.scheme !== undefined) {
|
|
151
|
-
component.path = component.path.split('%3A').join(':')
|
|
152
|
-
}
|
|
213
|
+
component.path = serializePathEncoding(component.path, pathNoScheme)
|
|
153
214
|
} else {
|
|
154
215
|
component.path = normalizePercentEncoding(component.path)
|
|
155
216
|
}
|
|
156
217
|
}
|
|
157
218
|
|
|
158
219
|
if (options.reference !== 'suffix' && component.scheme) {
|
|
220
|
+
// Scheme handlers may replace the scheme during serialization.
|
|
221
|
+
component.scheme = decodeValidScheme(component.scheme)
|
|
159
222
|
uriTokens.push(component.scheme, ':')
|
|
160
223
|
}
|
|
161
224
|
|
|
@@ -178,6 +241,13 @@ function serialize (cmpts, opts) {
|
|
|
178
241
|
s = removeDotSegments(s)
|
|
179
242
|
}
|
|
180
243
|
|
|
244
|
+
// Dot-segment removal can expose a colon that was not originally in the
|
|
245
|
+
// first segment (for example, "./a:b"). Reapply path-noscheme encoding so
|
|
246
|
+
// the serialized relative reference cannot be reparsed as a URI scheme.
|
|
247
|
+
if (pathNoScheme) {
|
|
248
|
+
s = serializePathEncoding(s, true)
|
|
249
|
+
}
|
|
250
|
+
|
|
181
251
|
if (
|
|
182
252
|
authority === undefined &&
|
|
183
253
|
s[0] === '/' &&
|
|
@@ -191,11 +261,11 @@ function serialize (cmpts, opts) {
|
|
|
191
261
|
}
|
|
192
262
|
|
|
193
263
|
if (component.query !== undefined) {
|
|
194
|
-
uriTokens.push('?', component.query)
|
|
264
|
+
uriTokens.push('?', encodeQuery(component.query))
|
|
195
265
|
}
|
|
196
266
|
|
|
197
267
|
if (component.fragment !== undefined) {
|
|
198
|
-
uriTokens.push('#', component.fragment)
|
|
268
|
+
uriTokens.push('#', encodeFragment(component.fragment))
|
|
199
269
|
}
|
|
200
270
|
return uriTokens.join('')
|
|
201
271
|
}
|
|
@@ -206,6 +276,15 @@ const URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:
|
|
|
206
276
|
// with or without a scheme prefix, for the literal-backslash rejection below.
|
|
207
277
|
const AUTHORITY_PREFIX = /^(?:[^#/:?]+:)?\/\/([^/?#]*)/
|
|
208
278
|
|
|
279
|
+
// Captures the leading authority-introducer region after an optional scheme: a
|
|
280
|
+
// run of forward slashes, backslashes, and the characters the WHATWG URL parser
|
|
281
|
+
// removes before parsing (TAB U+0009, LF U+000A, CR U+000D). A valid introducer
|
|
282
|
+
// is exactly "//". Node treats "\" as "/" on special schemes and strips those
|
|
283
|
+
// characters first, so forms like "\\", "/\", "\/", "/<TAB>/", or a leading
|
|
284
|
+
// "<TAB>//" reach an authority in Node while fast-uri's URI_PARSE folds them into
|
|
285
|
+
// the path group (host confusion / SSRF / redirect bypass).
|
|
286
|
+
const AUTHORITY_INTRODUCER_REGION = /^(?:[^#/:?]+:)?([/\\\t\n\r]*)/
|
|
287
|
+
|
|
209
288
|
/**
|
|
210
289
|
* @param {import('./types/index').URIComponent} parsed
|
|
211
290
|
* @param {RegExpMatchArray} matches
|
|
@@ -223,10 +302,74 @@ function getParseError (parsed, matches) {
|
|
|
223
302
|
return undefined
|
|
224
303
|
}
|
|
225
304
|
|
|
305
|
+
/**
|
|
306
|
+
* Checks percent syntax without decoding the represented octets. RFC 3986
|
|
307
|
+
* percent-encoding is byte-oriented, so sequences such as `%FF` are valid even
|
|
308
|
+
* though they are not independently valid UTF-8.
|
|
309
|
+
*
|
|
310
|
+
* @param {string|undefined} component
|
|
311
|
+
* @returns {boolean}
|
|
312
|
+
*/
|
|
313
|
+
function hasMalformedPercentEncoding (component) {
|
|
314
|
+
if (component === undefined) return false
|
|
315
|
+
|
|
316
|
+
let percent = component.indexOf('%')
|
|
317
|
+
while (percent !== -1) {
|
|
318
|
+
if (percent + 2 >= component.length || !/^[\da-f]{2}$/iu.test(component.slice(percent + 1, percent + 3))) {
|
|
319
|
+
return true
|
|
320
|
+
}
|
|
321
|
+
percent = component.indexOf('%', percent + 3)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return false
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* @param {RegExpMatchArray} matches
|
|
329
|
+
* @returns {boolean}
|
|
330
|
+
*/
|
|
331
|
+
function hasMalformedComponentPercentEncoding (matches) {
|
|
332
|
+
// Bracketed IP literals use a raw "%" as the zone separator for historical
|
|
333
|
+
// compatibility. Their parsing is intentionally left to normalizeIPv6.
|
|
334
|
+
const host = matches[4]
|
|
335
|
+
return hasMalformedPercentEncoding(matches[3]) ||
|
|
336
|
+
(host !== undefined && !(host[0] === '[' && host[host.length - 1] === ']') && hasMalformedPercentEncoding(host)) ||
|
|
337
|
+
hasMalformedPercentEncoding(matches[6]) ||
|
|
338
|
+
hasMalformedPercentEncoding(matches[7]) ||
|
|
339
|
+
hasMalformedPercentEncoding(matches[8])
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* @param {import('./types/index').URIComponent} parsed
|
|
344
|
+
* @param {import('./types/index').Options} options
|
|
345
|
+
* @param {{ domainHost?: boolean, unicodeSupport?: boolean }|undefined} schemeHandler
|
|
346
|
+
* @param {boolean} isIP
|
|
347
|
+
* @returns {boolean} whether host conversion failed
|
|
348
|
+
*/
|
|
349
|
+
function canonicalizeHost (parsed, options, schemeHandler, isIP) {
|
|
350
|
+
if (
|
|
351
|
+
!options.unicodeSupport &&
|
|
352
|
+
(!schemeHandler || !schemeHandler.unicodeSupport) &&
|
|
353
|
+
parsed.host &&
|
|
354
|
+
parsed.host[0] !== '[' &&
|
|
355
|
+
(options.domainHost || (schemeHandler && schemeHandler.domainHost)) &&
|
|
356
|
+
isIP === false &&
|
|
357
|
+
nonSimpleDomain(parsed.host)
|
|
358
|
+
) {
|
|
359
|
+
try {
|
|
360
|
+
parsed.host = new URL('http://' + parsed.host).hostname
|
|
361
|
+
} catch (e) {
|
|
362
|
+
parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e
|
|
363
|
+
return true
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return false
|
|
367
|
+
}
|
|
368
|
+
|
|
226
369
|
/**
|
|
227
370
|
* @param {string} uri
|
|
228
371
|
* @param {import('./types/index').Options} [opts]
|
|
229
|
-
* @returns {{ parsed: import('./types/index').URIComponent, malformedAuthorityOrPort: boolean }}
|
|
372
|
+
* @returns {{ parsed: import('./types/index').URIComponent, malformedAuthorityOrPort: boolean, malformedPercentEncoding: boolean, malformedSchemeSpecific: boolean, malformedHost: boolean, malformedScheme: boolean }}
|
|
230
373
|
*/
|
|
231
374
|
function parseWithStatus (uri, opts) {
|
|
232
375
|
const options = Object.assign({}, opts)
|
|
@@ -242,6 +385,11 @@ function parseWithStatus (uri, opts) {
|
|
|
242
385
|
}
|
|
243
386
|
|
|
244
387
|
let malformedAuthorityOrPort = false
|
|
388
|
+
let malformedPercentEncoding = false
|
|
389
|
+
let malformedSchemeSpecific = false
|
|
390
|
+
let malformedHost = false
|
|
391
|
+
let malformedIPLiteral = false
|
|
392
|
+
let malformedScheme = false
|
|
245
393
|
|
|
246
394
|
let isIP = false
|
|
247
395
|
if (options.reference === 'suffix') {
|
|
@@ -265,6 +413,28 @@ function parseWithStatus (uri, opts) {
|
|
|
265
413
|
malformedAuthorityOrPort = true
|
|
266
414
|
}
|
|
267
415
|
|
|
416
|
+
// Reject a malformed or whitespace-smuggled authority introducer. fast-uri
|
|
417
|
+
// only recognizes a literal "//"; anything else in the leading separator run
|
|
418
|
+
// (a backslash, or a "//" that appears only after removing the TAB/LF/CR that
|
|
419
|
+
// Node strips) means the authority fast-uri parses differs from the one Node's
|
|
420
|
+
// URL resolves. Reject rather than rewrite, mirroring the literal-backslash
|
|
421
|
+
// guard above. Percent-encoded forms (%5C, %09) are untouched, valid data.
|
|
422
|
+
const introducerMatch = uri.match(AUTHORITY_INTRODUCER_REGION)
|
|
423
|
+
if (introducerMatch !== null) {
|
|
424
|
+
const region = introducerMatch[1]
|
|
425
|
+
const normalizedRegion = region.replace(/[\t\n\r]/g, '')
|
|
426
|
+
// Two or more leading separators introduce an authority.
|
|
427
|
+
if (normalizedRegion.length >= 2) {
|
|
428
|
+
if (normalizedRegion.slice(0, 2) !== '//') {
|
|
429
|
+
parsed.error = parsed.error || 'URI authority must not contain a literal backslash.'
|
|
430
|
+
malformedAuthorityOrPort = true
|
|
431
|
+
} else if (region.length !== normalizedRegion.length) {
|
|
432
|
+
parsed.error = parsed.error || 'URI authority introducer must not contain whitespace.'
|
|
433
|
+
malformedAuthorityOrPort = true
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
268
438
|
const matches = uri.match(URI_PARSE)
|
|
269
439
|
|
|
270
440
|
if (matches) {
|
|
@@ -277,6 +447,21 @@ function parseWithStatus (uri, opts) {
|
|
|
277
447
|
parsed.query = matches[7]
|
|
278
448
|
parsed.fragment = matches[8]
|
|
279
449
|
|
|
450
|
+
if (parsed.scheme !== undefined) {
|
|
451
|
+
const decodedScheme = unescape(parsed.scheme)
|
|
452
|
+
if (VALID_SCHEME.test(decodedScheme)) {
|
|
453
|
+
parsed.scheme = decodedScheme.toLowerCase()
|
|
454
|
+
} else {
|
|
455
|
+
parsed.error = parsed.error || MALFORMED_SCHEME_ERROR
|
|
456
|
+
malformedScheme = true
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
malformedPercentEncoding = hasMalformedComponentPercentEncoding(matches)
|
|
461
|
+
if (malformedPercentEncoding) {
|
|
462
|
+
parsed.error = parsed.error || 'URI contains malformed percent-encoding.'
|
|
463
|
+
}
|
|
464
|
+
|
|
280
465
|
// fix port number
|
|
281
466
|
if (isNaN(parsed.port)) {
|
|
282
467
|
parsed.port = matches[5]
|
|
@@ -291,9 +476,16 @@ function parseWithStatus (uri, opts) {
|
|
|
291
476
|
if (parsed.host) {
|
|
292
477
|
const ipv4result = isIPv4(parsed.host)
|
|
293
478
|
if (ipv4result === false) {
|
|
479
|
+
const bracketedIPLiteral = parsed.host[0] === '[' && parsed.host[parsed.host.length - 1] === ']'
|
|
294
480
|
const ipv6result = normalizeIPv6(parsed.host)
|
|
295
|
-
|
|
296
|
-
|
|
481
|
+
isIP = ipv6result.isIPV6 || ipv6result.isIPVFuture === true
|
|
482
|
+
malformedIPLiteral = bracketedIPLiteral && ipv6result.error === true
|
|
483
|
+
parsed.host = isIP ? ipv6result.host : ipv6result.host.toLowerCase()
|
|
484
|
+
|
|
485
|
+
if (malformedIPLiteral) {
|
|
486
|
+
parsed.error = parsed.error || 'URI host is malformed.'
|
|
487
|
+
malformedAuthorityOrPort = true
|
|
488
|
+
}
|
|
297
489
|
} else {
|
|
298
490
|
isIP = true
|
|
299
491
|
}
|
|
@@ -316,49 +508,38 @@ function parseWithStatus (uri, opts) {
|
|
|
316
508
|
// find scheme handler
|
|
317
509
|
const schemeHandler = getSchemeHandler(options.scheme || parsed.scheme)
|
|
318
510
|
|
|
319
|
-
//
|
|
320
|
-
|
|
321
|
-
// if host component is a domain name
|
|
322
|
-
if (parsed.host && (options.domainHost || (schemeHandler && schemeHandler.domainHost)) && isIP === false && nonSimpleDomain(parsed.host)) {
|
|
323
|
-
// convert Unicode IDN -> ASCII IDN
|
|
324
|
-
try {
|
|
325
|
-
parsed.host = new URL('http://' + parsed.host).hostname
|
|
326
|
-
} catch (e) {
|
|
327
|
-
parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
// convert IRI -> URI
|
|
331
|
-
}
|
|
511
|
+
// convert Unicode IDN -> ASCII IDN when the effective scheme uses domain hosts
|
|
512
|
+
malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP)
|
|
332
513
|
|
|
333
514
|
if (!schemeHandler || (schemeHandler && !schemeHandler.skipNormalize)) {
|
|
334
515
|
if (uri.indexOf('%') !== -1) {
|
|
335
|
-
if (parsed.
|
|
336
|
-
parsed.
|
|
337
|
-
|
|
338
|
-
if (parsed.host !== undefined) {
|
|
339
|
-
parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP)
|
|
516
|
+
if (parsed.host !== undefined && !malformedIPLiteral) {
|
|
517
|
+
const host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true)
|
|
518
|
+
parsed.host = reescapeHostDelimiters(host, isIP)
|
|
340
519
|
}
|
|
341
520
|
}
|
|
342
521
|
if (parsed.path) {
|
|
343
522
|
parsed.path = normalizePathEncoding(parsed.path)
|
|
344
523
|
}
|
|
524
|
+
if (parsed.query) {
|
|
525
|
+
parsed.query = normalizeQueryFragmentEncoding(parsed.query)
|
|
526
|
+
}
|
|
345
527
|
if (parsed.fragment) {
|
|
346
|
-
|
|
347
|
-
parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment))
|
|
348
|
-
} catch {
|
|
349
|
-
parsed.error = parsed.error || 'URI malformed'
|
|
350
|
-
}
|
|
528
|
+
parsed.fragment = normalizeQueryFragmentEncoding(parsed.fragment)
|
|
351
529
|
}
|
|
352
530
|
}
|
|
353
531
|
|
|
354
532
|
// perform scheme specific parsing
|
|
355
533
|
if (schemeHandler && schemeHandler.parse) {
|
|
356
534
|
schemeHandler.parse(parsed, options)
|
|
535
|
+
if (schemeHandler === SCHEMES.urn && parsed.nid === undefined) {
|
|
536
|
+
malformedSchemeSpecific = true
|
|
537
|
+
}
|
|
357
538
|
}
|
|
358
539
|
} else {
|
|
359
540
|
parsed.error = parsed.error || 'URI can not be parsed.'
|
|
360
541
|
}
|
|
361
|
-
return { parsed, malformedAuthorityOrPort }
|
|
542
|
+
return { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme }
|
|
362
543
|
}
|
|
363
544
|
|
|
364
545
|
/**
|
|
@@ -382,13 +563,17 @@ function normalizeString (uri, opts) {
|
|
|
382
563
|
/**
|
|
383
564
|
* @param {string} uri
|
|
384
565
|
* @param {import('./types/index').Options} [opts]
|
|
385
|
-
* @returns {{ normalized: string, malformedAuthorityOrPort: boolean }}
|
|
566
|
+
* @returns {{ normalized: string, malformedAuthorityOrPort: boolean, malformedPercentEncoding: boolean, malformedSchemeSpecific: boolean, malformedHost: boolean, malformedScheme: boolean }}
|
|
386
567
|
*/
|
|
387
568
|
function normalizeStringWithStatus (uri, opts) {
|
|
388
|
-
const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri, opts)
|
|
569
|
+
const { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = parseWithStatus(uri, opts)
|
|
389
570
|
return {
|
|
390
|
-
normalized: malformedAuthorityOrPort ? uri : serialize(parsed, opts),
|
|
391
|
-
malformedAuthorityOrPort
|
|
571
|
+
normalized: malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? uri : serialize(parsed, opts),
|
|
572
|
+
malformedAuthorityOrPort,
|
|
573
|
+
malformedPercentEncoding,
|
|
574
|
+
malformedSchemeSpecific,
|
|
575
|
+
malformedHost,
|
|
576
|
+
malformedScheme
|
|
392
577
|
}
|
|
393
578
|
}
|
|
394
579
|
|
|
@@ -398,14 +583,18 @@ function normalizeStringWithStatus (uri, opts) {
|
|
|
398
583
|
* @returns {string|undefined}
|
|
399
584
|
*/
|
|
400
585
|
function normalizeComparableURI (uri, opts) {
|
|
401
|
-
if (typeof uri
|
|
402
|
-
|
|
403
|
-
return malformedAuthorityOrPort ? undefined : normalized
|
|
586
|
+
if (typeof uri !== 'string' && typeof uri !== 'object') {
|
|
587
|
+
return undefined
|
|
404
588
|
}
|
|
405
589
|
|
|
406
|
-
|
|
407
|
-
|
|
590
|
+
let value
|
|
591
|
+
try {
|
|
592
|
+
value = typeof uri === 'string' ? uri : serialize(uri, opts)
|
|
593
|
+
} catch {
|
|
594
|
+
return undefined
|
|
408
595
|
}
|
|
596
|
+
const { normalized, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = normalizeStringWithStatus(value, opts)
|
|
597
|
+
return malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? undefined : normalized
|
|
409
598
|
}
|
|
410
599
|
|
|
411
600
|
const fastUri = {
|
package/lib/schemes.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { isUUID } = require('./utils')
|
|
4
|
-
const URN_REG =
|
|
4
|
+
const URN_REG = /^([\da-z][\d\-a-z]{0,31}):((?:[\w!$'()*+,\-./:;=@]|%[\da-f]{2})+)$/iu
|
|
5
5
|
|
|
6
6
|
const supportedSchemeNames = /** @type {const} */ (['http', 'https', 'ws',
|
|
7
7
|
'wss', 'urn', 'urn:uuid'])
|
|
@@ -113,9 +113,14 @@ function wsSerialize (wsComponent) {
|
|
|
113
113
|
|
|
114
114
|
// reconstruct path from resource name
|
|
115
115
|
if (wsComponent.resourceName) {
|
|
116
|
-
const
|
|
116
|
+
const queryIndex = wsComponent.resourceName.indexOf('?')
|
|
117
|
+
const path = queryIndex === -1
|
|
118
|
+
? wsComponent.resourceName
|
|
119
|
+
: wsComponent.resourceName.slice(0, queryIndex)
|
|
117
120
|
wsComponent.path = (path && path !== '/' ? path : undefined)
|
|
118
|
-
wsComponent.query =
|
|
121
|
+
wsComponent.query = queryIndex === -1
|
|
122
|
+
? undefined
|
|
123
|
+
: wsComponent.resourceName.slice(queryIndex + 1)
|
|
119
124
|
wsComponent.resourceName = undefined
|
|
120
125
|
}
|
|
121
126
|
|
|
@@ -132,7 +137,7 @@ function urnParse (urnComponent, options) {
|
|
|
132
137
|
return urnComponent
|
|
133
138
|
}
|
|
134
139
|
const matches = urnComponent.path.match(URN_REG)
|
|
135
|
-
if (matches) {
|
|
140
|
+
if (matches && matches[0] === urnComponent.path) {
|
|
136
141
|
const scheme = options.scheme || urnComponent.scheme || 'urn'
|
|
137
142
|
urnComponent.nid = matches[1].toLowerCase()
|
|
138
143
|
urnComponent.nss = matches[2]
|