fast-uri 2.4.3 → 2.4.5
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 +236 -61
- package/lib/schemes.js +9 -4
- package/lib/utils.js +424 -116
- package/package.json +2 -1
- package/test/compatibility.test.js +12 -0
- package/test/component-safe-serialization.test.js +105 -0
- package/test/equal.test.js +32 -0
- package/test/malformed-percent.test.js +77 -0
- package/test/malformed-urn.test.js +61 -0
- package/test/parse.test.js +3 -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 +102 -2
- package/test/security.test.js +214 -7
- 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,
|
|
3
|
+
const { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, serializePathEncoding, normalizeQueryFragmentEncoding, encodeQuery, encodeFragment, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require('./lib/utils')
|
|
4
4
|
const SCHEMES = 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
|
function normalize (uri, options) {
|
|
7
22
|
if (typeof uri === 'string') {
|
|
8
23
|
uri = normalizeString(uri, options)
|
|
@@ -14,7 +29,50 @@ function normalize (uri, options) {
|
|
|
14
29
|
|
|
15
30
|
function resolve (baseURI, relativeURI, options) {
|
|
16
31
|
const schemelessOptions = Object.assign({ scheme: 'null' }, options)
|
|
17
|
-
const
|
|
32
|
+
const {
|
|
33
|
+
parsed: baseParsed,
|
|
34
|
+
malformedAuthorityOrPort: baseMalformed,
|
|
35
|
+
malformedPercentEncoding: baseMalformedPercentEncoding,
|
|
36
|
+
malformedSchemeSpecific: baseMalformedSchemeSpecific,
|
|
37
|
+
malformedHost: baseMalformedHost,
|
|
38
|
+
malformedScheme: baseMalformedScheme
|
|
39
|
+
} = parseWithStatus(baseURI, schemelessOptions)
|
|
40
|
+
const {
|
|
41
|
+
parsed: relativeParsed,
|
|
42
|
+
malformedAuthorityOrPort: relativeMalformed,
|
|
43
|
+
malformedPercentEncoding: relativeMalformedPercentEncoding,
|
|
44
|
+
malformedSchemeSpecific: relativeMalformedSchemeSpecific,
|
|
45
|
+
malformedHost: relativeMalformedHost,
|
|
46
|
+
malformedScheme: relativeMalformedScheme
|
|
47
|
+
} = parseWithStatus(relativeURI, schemelessOptions)
|
|
48
|
+
if (
|
|
49
|
+
baseMalformed ||
|
|
50
|
+
relativeMalformed ||
|
|
51
|
+
baseMalformedPercentEncoding ||
|
|
52
|
+
relativeMalformedPercentEncoding ||
|
|
53
|
+
baseMalformedSchemeSpecific ||
|
|
54
|
+
relativeMalformedSchemeSpecific ||
|
|
55
|
+
baseMalformedHost ||
|
|
56
|
+
relativeMalformedHost ||
|
|
57
|
+
baseMalformedScheme ||
|
|
58
|
+
relativeMalformedScheme
|
|
59
|
+
) {
|
|
60
|
+
throw new Error(baseParsed.error || relativeParsed.error || 'URI is malformed.')
|
|
61
|
+
}
|
|
62
|
+
const resolved = resolveComponents(baseParsed, relativeParsed, schemelessOptions, true)
|
|
63
|
+
const resolvedSchemeHandler = SCHEMES[((options && options.scheme) || resolved.scheme || '').toLowerCase()]
|
|
64
|
+
const resolvedHost = resolved.host
|
|
65
|
+
const resolvedHostIsIP = resolvedHost !== undefined && resolvedHost !== '' &&
|
|
66
|
+
(isIPv4(resolvedHost) || normalizeIPv6(resolvedHost).isIPV6)
|
|
67
|
+
canonicalizeHost(resolved, options || {}, resolvedSchemeHandler, resolvedHostIsIP)
|
|
68
|
+
// Percent escapes in an ASCII reg-name are encoded data. The WHATWG hostname
|
|
69
|
+
// parser can reject them even though fast-uri preserves them safely as RFC
|
|
70
|
+
// 3986 data. A raw non-ASCII host must still fail closed if conversion fails.
|
|
71
|
+
const encodedASCIIHost = resolvedHost && resolvedHost.indexOf('%') !== -1 &&
|
|
72
|
+
!/\P{ASCII}/u.test(resolvedHost)
|
|
73
|
+
if (resolved.error && !encodedASCIIHost) {
|
|
74
|
+
throw new Error(resolved.error)
|
|
75
|
+
}
|
|
18
76
|
return serialize(resolved, { ...schemelessOptions, skipEscape: true })
|
|
19
77
|
}
|
|
20
78
|
|
|
@@ -82,7 +140,7 @@ function equal (uriA, uriB, options) {
|
|
|
82
140
|
const normalizedA = normalizeComparableURI(uriA, options)
|
|
83
141
|
const normalizedB = normalizeComparableURI(uriB, options)
|
|
84
142
|
|
|
85
|
-
return normalizedA !== undefined && normalizedB !== undefined && normalizedA
|
|
143
|
+
return normalizedA !== undefined && normalizedB !== undefined && normalizedA === normalizedB
|
|
86
144
|
}
|
|
87
145
|
|
|
88
146
|
function serialize (cmpts, opts) {
|
|
@@ -105,25 +163,30 @@ function serialize (cmpts, opts) {
|
|
|
105
163
|
const options = Object.assign({}, opts)
|
|
106
164
|
const uriTokens = []
|
|
107
165
|
|
|
166
|
+
if (components.scheme) {
|
|
167
|
+
components.scheme = decodeValidScheme(components.scheme)
|
|
168
|
+
}
|
|
169
|
+
|
|
108
170
|
// find scheme handler
|
|
109
171
|
const schemeHandler = SCHEMES[(options.scheme || components.scheme || '').toLowerCase()]
|
|
110
172
|
|
|
111
173
|
// perform scheme specific serialization
|
|
112
174
|
if (schemeHandler && schemeHandler.serialize) schemeHandler.serialize(components, options)
|
|
113
175
|
|
|
176
|
+
const hasAuthority = components.userinfo !== undefined || components.host !== undefined || components.port !== undefined
|
|
177
|
+
const pathNoScheme = !options.skipEscape && components.scheme === undefined && !hasAuthority
|
|
178
|
+
|
|
114
179
|
if (components.path !== undefined) {
|
|
115
180
|
if (!options.skipEscape) {
|
|
116
|
-
components.path =
|
|
117
|
-
|
|
118
|
-
if (components.scheme !== undefined) {
|
|
119
|
-
components.path = components.path.split('%3A').join(':')
|
|
120
|
-
}
|
|
181
|
+
components.path = serializePathEncoding(components.path, pathNoScheme)
|
|
121
182
|
} else {
|
|
122
183
|
components.path = normalizePercentEncoding(components.path)
|
|
123
184
|
}
|
|
124
185
|
}
|
|
125
186
|
|
|
126
187
|
if (options.reference !== 'suffix' && components.scheme) {
|
|
188
|
+
// Scheme handlers may replace the scheme during serialization.
|
|
189
|
+
components.scheme = decodeValidScheme(components.scheme)
|
|
127
190
|
uriTokens.push(components.scheme)
|
|
128
191
|
uriTokens.push(':')
|
|
129
192
|
}
|
|
@@ -147,6 +210,13 @@ function serialize (cmpts, opts) {
|
|
|
147
210
|
s = removeDotSegments(s)
|
|
148
211
|
}
|
|
149
212
|
|
|
213
|
+
// Dot-segment removal can expose a colon that was not originally in the
|
|
214
|
+
// first segment (for example, "./a:b"). Reapply path-noscheme encoding so
|
|
215
|
+
// the serialized relative reference cannot be reparsed as a URI scheme.
|
|
216
|
+
if (pathNoScheme) {
|
|
217
|
+
s = serializePathEncoding(s, true)
|
|
218
|
+
}
|
|
219
|
+
|
|
150
220
|
if (authority === undefined) {
|
|
151
221
|
s = s.replace(/^\/\//u, '/%2F') // don't allow the path to start with "//"
|
|
152
222
|
}
|
|
@@ -156,35 +226,31 @@ function serialize (cmpts, opts) {
|
|
|
156
226
|
|
|
157
227
|
if (components.query !== undefined) {
|
|
158
228
|
uriTokens.push('?')
|
|
159
|
-
uriTokens.push(components.query)
|
|
229
|
+
uriTokens.push(encodeQuery(components.query))
|
|
160
230
|
}
|
|
161
231
|
|
|
162
232
|
if (components.fragment !== undefined) {
|
|
163
233
|
uriTokens.push('#')
|
|
164
|
-
uriTokens.push(components.fragment)
|
|
234
|
+
uriTokens.push(encodeFragment(components.fragment))
|
|
165
235
|
}
|
|
166
236
|
return uriTokens.join('')
|
|
167
237
|
}
|
|
168
238
|
|
|
169
|
-
const hexLookUp = Array.from({ length: 127 }, (v, k) => /[^!"$&'()*+,\-.;=_`a-z{}~]/u.test(String.fromCharCode(k)))
|
|
170
|
-
|
|
171
|
-
function nonSimpleDomain (value) {
|
|
172
|
-
let code = 0
|
|
173
|
-
for (let i = 0, len = value.length; i < len; ++i) {
|
|
174
|
-
code = value.charCodeAt(i)
|
|
175
|
-
if (code > 126 || hexLookUp[code]) {
|
|
176
|
-
return true
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
return false
|
|
180
|
-
}
|
|
181
|
-
|
|
182
239
|
const URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u
|
|
183
240
|
|
|
184
241
|
// Captures the authority component (between "//" and the next "/", "?" or "#"),
|
|
185
242
|
// with or without a scheme prefix, for the literal-backslash rejection below.
|
|
186
243
|
const AUTHORITY_PREFIX = /^(?:[^#/:?]+:)?\/\/([^/?#]*)/
|
|
187
244
|
|
|
245
|
+
// Captures the leading authority-introducer region after an optional scheme: a
|
|
246
|
+
// run of forward slashes, backslashes, and the characters the WHATWG URL parser
|
|
247
|
+
// removes before parsing (TAB U+0009, LF U+000A, CR U+000D). A valid introducer
|
|
248
|
+
// is exactly "//". Node treats "\\" as "/" on special schemes and strips those
|
|
249
|
+
// characters first, so forms like "\\", "/\", "\/", "/<TAB>/", or a leading
|
|
250
|
+
// "<TAB>//" reach an authority in Node while fast-uri's URI_PARSE folds them into
|
|
251
|
+
// the path group (host confusion / SSRF / redirect bypass).
|
|
252
|
+
const AUTHORITY_INTRODUCER_REGION = /^(?:[^#/:?]+:)?([/\\\t\n\r]*)/
|
|
253
|
+
|
|
188
254
|
function getParseError (parsed, matches) {
|
|
189
255
|
if (matches[2] !== undefined && parsed.path && parsed.path[0] !== '/') {
|
|
190
256
|
return 'URI path must start with "/" when authority is present.'
|
|
@@ -197,6 +263,70 @@ function getParseError (parsed, matches) {
|
|
|
197
263
|
return undefined
|
|
198
264
|
}
|
|
199
265
|
|
|
266
|
+
/**
|
|
267
|
+
* Checks percent syntax without decoding the represented octets. RFC 3986
|
|
268
|
+
* percent-encoding is byte-oriented, so sequences such as `%FF` are valid even
|
|
269
|
+
* though they are not independently valid UTF-8.
|
|
270
|
+
*
|
|
271
|
+
* @param {string|undefined} component
|
|
272
|
+
* @returns {boolean}
|
|
273
|
+
*/
|
|
274
|
+
/**
|
|
275
|
+
* @param {import('./types/index').URIComponent} parsed
|
|
276
|
+
* @param {import('./types/index').Options} options
|
|
277
|
+
* @param {{ domainHost?: boolean, unicodeSupport?: boolean }|undefined} schemeHandler
|
|
278
|
+
* @param {boolean} isIP
|
|
279
|
+
* @returns {boolean} whether host conversion failed
|
|
280
|
+
*/
|
|
281
|
+
function canonicalizeHost (parsed, options, schemeHandler, isIP) {
|
|
282
|
+
if (
|
|
283
|
+
!options.unicodeSupport &&
|
|
284
|
+
(!schemeHandler || !schemeHandler.unicodeSupport) &&
|
|
285
|
+
parsed.host &&
|
|
286
|
+
parsed.host[0] !== '[' &&
|
|
287
|
+
(options.domainHost || (schemeHandler && schemeHandler.domainHost)) &&
|
|
288
|
+
isIP === false &&
|
|
289
|
+
nonSimpleDomain(parsed.host)
|
|
290
|
+
) {
|
|
291
|
+
try {
|
|
292
|
+
parsed.host = new URL('http://' + parsed.host).hostname
|
|
293
|
+
} catch (e) {
|
|
294
|
+
parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e
|
|
295
|
+
return true
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return false
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function hasMalformedPercentEncoding (component) {
|
|
302
|
+
if (component === undefined) return false
|
|
303
|
+
|
|
304
|
+
let percent = component.indexOf('%')
|
|
305
|
+
while (percent !== -1) {
|
|
306
|
+
if (percent + 2 >= component.length || !/^[\da-f]{2}$/iu.test(component.slice(percent + 1, percent + 3))) {
|
|
307
|
+
return true
|
|
308
|
+
}
|
|
309
|
+
percent = component.indexOf('%', percent + 3)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return false
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* @param {RegExpMatchArray} matches
|
|
317
|
+
* @returns {boolean}
|
|
318
|
+
*/
|
|
319
|
+
function hasMalformedComponentPercentEncoding (matches) {
|
|
320
|
+
// Bracketed IP literals use a raw "%" as the zone separator for historical
|
|
321
|
+
// compatibility. Their parsing is intentionally left to normalizeIPv6.
|
|
322
|
+
const host = matches[4]
|
|
323
|
+
return hasMalformedPercentEncoding(matches[3]) ||
|
|
324
|
+
(host !== undefined && !(host[0] === '[' && host[host.length - 1] === ']') && hasMalformedPercentEncoding(host)) ||
|
|
325
|
+
hasMalformedPercentEncoding(matches[6]) ||
|
|
326
|
+
hasMalformedPercentEncoding(matches[7]) ||
|
|
327
|
+
hasMalformedPercentEncoding(matches[8])
|
|
328
|
+
}
|
|
329
|
+
|
|
200
330
|
function parseWithStatus (uri, opts) {
|
|
201
331
|
const options = Object.assign({}, opts)
|
|
202
332
|
const parsed = {
|
|
@@ -208,8 +338,12 @@ function parseWithStatus (uri, opts) {
|
|
|
208
338
|
query: undefined,
|
|
209
339
|
fragment: undefined
|
|
210
340
|
}
|
|
211
|
-
const gotEncoding = uri.indexOf('%') !== -1
|
|
212
341
|
let malformedAuthorityOrPort = false
|
|
342
|
+
let malformedPercentEncoding = false
|
|
343
|
+
let malformedSchemeSpecific = false
|
|
344
|
+
let malformedHost = false
|
|
345
|
+
let malformedIPLiteral = false
|
|
346
|
+
let malformedScheme = false
|
|
213
347
|
let isIP = false
|
|
214
348
|
if (options.reference === 'suffix') uri = (options.scheme ? options.scheme + ':' : '') + '//' + uri
|
|
215
349
|
|
|
@@ -226,6 +360,28 @@ function parseWithStatus (uri, opts) {
|
|
|
226
360
|
malformedAuthorityOrPort = true
|
|
227
361
|
}
|
|
228
362
|
|
|
363
|
+
// Reject a malformed or whitespace-smuggled authority introducer. fast-uri
|
|
364
|
+
// only recognizes a literal "//"; anything else in the leading separator run
|
|
365
|
+
// (a backslash, or a "//" that appears only after removing the TAB/LF/CR that
|
|
366
|
+
// Node strips) means the authority fast-uri parses differs from the one Node's
|
|
367
|
+
// URL resolves. Reject rather than rewrite, mirroring the literal-backslash
|
|
368
|
+
// guard above. Percent-encoded forms (%5C, %09) are untouched, valid data.
|
|
369
|
+
const introducerMatch = uri.match(AUTHORITY_INTRODUCER_REGION)
|
|
370
|
+
if (introducerMatch !== null) {
|
|
371
|
+
const region = introducerMatch[1]
|
|
372
|
+
const normalizedRegion = region.replace(/[\t\n\r]/g, '')
|
|
373
|
+
// Two or more leading separators introduce an authority.
|
|
374
|
+
if (normalizedRegion.length >= 2) {
|
|
375
|
+
if (normalizedRegion.slice(0, 2) !== '//') {
|
|
376
|
+
parsed.error = parsed.error || 'URI authority must not contain a literal backslash.'
|
|
377
|
+
malformedAuthorityOrPort = true
|
|
378
|
+
} else if (region.length !== normalizedRegion.length) {
|
|
379
|
+
parsed.error = parsed.error || 'URI authority introducer must not contain whitespace.'
|
|
380
|
+
malformedAuthorityOrPort = true
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
229
385
|
const matches = uri.match(URI_PARSE)
|
|
230
386
|
|
|
231
387
|
if (matches) {
|
|
@@ -238,6 +394,21 @@ function parseWithStatus (uri, opts) {
|
|
|
238
394
|
parsed.query = matches[7]
|
|
239
395
|
parsed.fragment = matches[8]
|
|
240
396
|
|
|
397
|
+
if (parsed.scheme !== undefined) {
|
|
398
|
+
const decodedScheme = unescape(parsed.scheme)
|
|
399
|
+
if (VALID_SCHEME.test(decodedScheme)) {
|
|
400
|
+
parsed.scheme = decodedScheme.toLowerCase()
|
|
401
|
+
} else {
|
|
402
|
+
parsed.error = parsed.error || MALFORMED_SCHEME_ERROR
|
|
403
|
+
malformedScheme = true
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
malformedPercentEncoding = hasMalformedComponentPercentEncoding(matches)
|
|
408
|
+
if (malformedPercentEncoding) {
|
|
409
|
+
parsed.error = parsed.error || 'URI contains malformed percent-encoding.'
|
|
410
|
+
}
|
|
411
|
+
|
|
241
412
|
// fix port number
|
|
242
413
|
if (isNaN(parsed.port)) {
|
|
243
414
|
parsed.port = matches[5]
|
|
@@ -250,13 +421,19 @@ function parseWithStatus (uri, opts) {
|
|
|
250
421
|
}
|
|
251
422
|
|
|
252
423
|
if (parsed.host) {
|
|
253
|
-
const ipv4result =
|
|
254
|
-
if (ipv4result
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
isIP = ipv6result.isIPV6
|
|
424
|
+
const ipv4result = isIPv4(parsed.host)
|
|
425
|
+
if (ipv4result === false) {
|
|
426
|
+
const bracketedIPLiteral = parsed.host[0] === '[' && parsed.host[parsed.host.length - 1] === ']'
|
|
427
|
+
const ipv6result = normalizeIPv6(parsed.host)
|
|
428
|
+
isIP = ipv6result.isIPV6 || ipv6result.isIPVFuture === true
|
|
429
|
+
malformedIPLiteral = bracketedIPLiteral && ipv6result.error === true
|
|
430
|
+
parsed.host = isIP ? ipv6result.host : ipv6result.host.toLowerCase()
|
|
431
|
+
|
|
432
|
+
if (malformedIPLiteral) {
|
|
433
|
+
parsed.error = parsed.error || 'URI host is malformed.'
|
|
434
|
+
malformedAuthorityOrPort = true
|
|
435
|
+
}
|
|
258
436
|
} else {
|
|
259
|
-
parsed.host = ipv4result.host
|
|
260
437
|
isIP = true
|
|
261
438
|
}
|
|
262
439
|
}
|
|
@@ -278,46 +455,36 @@ function parseWithStatus (uri, opts) {
|
|
|
278
455
|
// find scheme handler
|
|
279
456
|
const schemeHandler = SCHEMES[(options.scheme || parsed.scheme || '').toLowerCase()]
|
|
280
457
|
|
|
281
|
-
//
|
|
282
|
-
|
|
283
|
-
// if host component is a domain name
|
|
284
|
-
if (parsed.host && (options.domainHost || (schemeHandler && schemeHandler.domainHost)) && isIP === false && nonSimpleDomain(parsed.host)) {
|
|
285
|
-
// convert Unicode IDN -> ASCII IDN
|
|
286
|
-
try {
|
|
287
|
-
parsed.host = new URL('http://' + parsed.host).hostname
|
|
288
|
-
} catch (e) {
|
|
289
|
-
parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
// convert IRI -> URI
|
|
293
|
-
}
|
|
458
|
+
// convert Unicode IDN -> ASCII IDN when the effective scheme uses domain hosts
|
|
459
|
+
malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP)
|
|
294
460
|
|
|
295
461
|
if (!schemeHandler || (schemeHandler && !schemeHandler.skipNormalize)) {
|
|
296
|
-
if (
|
|
297
|
-
parsed.
|
|
298
|
-
|
|
299
|
-
if (gotEncoding && parsed.userinfo !== undefined) {
|
|
300
|
-
parsed.userinfo = unescape(parsed.userinfo)
|
|
301
|
-
}
|
|
302
|
-
if (gotEncoding && parsed.host !== undefined) {
|
|
303
|
-
parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP)
|
|
462
|
+
if (parsed.host !== undefined && !malformedIPLiteral) {
|
|
463
|
+
const host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true)
|
|
464
|
+
parsed.host = reescapeHostDelimiters(host, isIP)
|
|
304
465
|
}
|
|
305
466
|
if (parsed.path !== undefined && parsed.path.length) {
|
|
306
467
|
parsed.path = normalizePathEncoding(parsed.path)
|
|
307
468
|
}
|
|
469
|
+
if (parsed.query !== undefined && parsed.query.length) {
|
|
470
|
+
parsed.query = normalizeQueryFragmentEncoding(parsed.query)
|
|
471
|
+
}
|
|
308
472
|
if (parsed.fragment !== undefined && parsed.fragment.length) {
|
|
309
|
-
parsed.fragment =
|
|
473
|
+
parsed.fragment = normalizeQueryFragmentEncoding(parsed.fragment)
|
|
310
474
|
}
|
|
311
475
|
}
|
|
312
476
|
|
|
313
477
|
// perform scheme specific parsing
|
|
314
478
|
if (schemeHandler && schemeHandler.parse) {
|
|
315
479
|
schemeHandler.parse(parsed, options)
|
|
480
|
+
if (schemeHandler === SCHEMES.urn && parsed.nid === undefined) {
|
|
481
|
+
malformedSchemeSpecific = true
|
|
482
|
+
}
|
|
316
483
|
}
|
|
317
484
|
} else {
|
|
318
485
|
parsed.error = parsed.error || 'URI can not be parsed.'
|
|
319
486
|
}
|
|
320
|
-
return { parsed, malformedAuthorityOrPort }
|
|
487
|
+
return { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme }
|
|
321
488
|
}
|
|
322
489
|
|
|
323
490
|
function parse (uri, opts) {
|
|
@@ -329,22 +496,30 @@ function normalizeString (uri, opts) {
|
|
|
329
496
|
}
|
|
330
497
|
|
|
331
498
|
function normalizeStringWithStatus (uri, opts) {
|
|
332
|
-
const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri, opts)
|
|
499
|
+
const { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = parseWithStatus(uri, opts)
|
|
333
500
|
return {
|
|
334
|
-
normalized: malformedAuthorityOrPort ? uri : serialize(parsed, opts),
|
|
335
|
-
malformedAuthorityOrPort
|
|
501
|
+
normalized: malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? uri : serialize(parsed, opts),
|
|
502
|
+
malformedAuthorityOrPort,
|
|
503
|
+
malformedPercentEncoding,
|
|
504
|
+
malformedSchemeSpecific,
|
|
505
|
+
malformedHost,
|
|
506
|
+
malformedScheme
|
|
336
507
|
}
|
|
337
508
|
}
|
|
338
509
|
|
|
339
510
|
function normalizeComparableURI (uri, opts) {
|
|
340
|
-
if (typeof uri
|
|
341
|
-
|
|
342
|
-
return malformedAuthorityOrPort ? undefined : normalized
|
|
511
|
+
if (typeof uri !== 'string' && typeof uri !== 'object') {
|
|
512
|
+
return undefined
|
|
343
513
|
}
|
|
344
514
|
|
|
345
|
-
|
|
346
|
-
|
|
515
|
+
let value
|
|
516
|
+
try {
|
|
517
|
+
value = typeof uri === 'string' ? uri : serialize(uri, opts)
|
|
518
|
+
} catch {
|
|
519
|
+
return undefined
|
|
347
520
|
}
|
|
521
|
+
const { normalized, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = normalizeStringWithStatus(value, opts)
|
|
522
|
+
return malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? undefined : normalized
|
|
348
523
|
}
|
|
349
524
|
|
|
350
525
|
const fastUri = {
|
package/lib/schemes.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const UUID_REG = /^[\da-f]{8}\b-[\da-f]{4}\b-[\da-f]{4}\b-[\da-f]{4}\b-[\da-f]{12}$/iu
|
|
4
|
-
const URN_REG =
|
|
4
|
+
const URN_REG = /^([\da-z][\d\-a-z]{0,31}):((?:[\w!$'()*+,\-./:;=@]|%[\da-f]{2})+)$/iu
|
|
5
5
|
|
|
6
6
|
function isSecure (wsComponents) {
|
|
7
7
|
return typeof wsComponents.secure === 'boolean' ? wsComponents.secure : String(wsComponents.scheme).toLowerCase() === 'wss'
|
|
@@ -61,9 +61,14 @@ function wsSerialize (wsComponents) {
|
|
|
61
61
|
|
|
62
62
|
// reconstruct path from resource name
|
|
63
63
|
if (wsComponents.resourceName) {
|
|
64
|
-
const
|
|
64
|
+
const queryIndex = wsComponents.resourceName.indexOf('?')
|
|
65
|
+
const path = queryIndex === -1
|
|
66
|
+
? wsComponents.resourceName
|
|
67
|
+
: wsComponents.resourceName.slice(0, queryIndex)
|
|
65
68
|
wsComponents.path = (path && path !== '/' ? path : undefined)
|
|
66
|
-
wsComponents.query =
|
|
69
|
+
wsComponents.query = queryIndex === -1
|
|
70
|
+
? undefined
|
|
71
|
+
: wsComponents.resourceName.slice(queryIndex + 1)
|
|
67
72
|
wsComponents.resourceName = undefined
|
|
68
73
|
}
|
|
69
74
|
|
|
@@ -79,7 +84,7 @@ function urnParse (urnComponents, options) {
|
|
|
79
84
|
return urnComponents
|
|
80
85
|
}
|
|
81
86
|
const matches = urnComponents.path.match(URN_REG)
|
|
82
|
-
if (matches) {
|
|
87
|
+
if (matches && matches[0] === urnComponents.path) {
|
|
83
88
|
const scheme = options.scheme || urnComponents.scheme || 'urn'
|
|
84
89
|
urnComponents.nid = matches[1].toLowerCase()
|
|
85
90
|
urnComponents.nss = matches[2]
|