fast-uri 4.1.2 → 4.1.4
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/.github/workflows/ci.yml +1 -1
- package/.github/workflows/lock-threads.yml +1 -1
- package/SECURITY.md +6 -1
- package/benchmark/benchmark.mjs +71 -0
- package/index.js +223 -43
- package/lib/schemes.js +384 -7
- package/lib/utils.js +310 -89
- package/package.json +1 -1
- package/test/component-safe-serialization.test.js +163 -0
- package/test/equal.test.js +43 -3
- package/test/fixtures/uri-js-parse.json +24 -1
- package/test/fixtures/uri-js-serialize.json +23 -1
- package/test/ipv6-canonical.test.js +34 -0
- package/test/ipv6-validation.test.js +154 -0
- package/test/malformed-percent.test.js +77 -0
- package/test/malformed-urn.test.js +61 -0
- package/test/parse.test.js +145 -0
- package/test/reserved-path-normalization.test.js +109 -0
- package/test/scheme-validation.test.js +122 -0
- package/test/security-normalization.test.js +101 -0
- package/test/security-utf8-encoding.test.js +0 -0
- package/test/security.test.js +75 -3
- package/test/serialize.test.js +144 -0
- package/test/uri-js-compatibility.test.js +0 -3
- package/test/uri-js.test.js +3 -3
- package/test/urn-full-input.test.js +29 -0
- package/test/websocket-query-preservation.test.js +24 -0
- package/types/index.d.ts +4 -0
- package/types/index.tst.ts +16 -0
package/.github/workflows/ci.yml
CHANGED
|
@@ -147,7 +147,7 @@ jobs:
|
|
|
147
147
|
permissions:
|
|
148
148
|
contents: write
|
|
149
149
|
pull-requests: write
|
|
150
|
-
uses: fastify/workflows/.github/workflows/plugins-ci.yml@
|
|
150
|
+
uses: fastify/workflows/.github/workflows/plugins-ci.yml@ef591e2186785d5ab36b9fe6a79c7ce2f1d94e57 # v7.0.0
|
|
151
151
|
with:
|
|
152
152
|
license-check: true
|
|
153
153
|
lint: true
|
|
@@ -16,4 +16,4 @@ jobs:
|
|
|
16
16
|
permissions:
|
|
17
17
|
issues: write
|
|
18
18
|
pull-requests: write
|
|
19
|
-
uses: fastify/workflows/.github/workflows/lock-threads.yml@
|
|
19
|
+
uses: fastify/workflows/.github/workflows/lock-threads.yml@ef591e2186785d5ab36b9fe6a79c7ce2f1d94e57 # v7.0.0
|
package/SECURITY.md
CHANGED
|
@@ -11,10 +11,15 @@ official Fastify project package. It supplements the Fastify project's
|
|
|
11
11
|
[WHATWG URL Standard](https://url.spec.whatwg.org/).
|
|
12
12
|
|
|
13
13
|
Differences between `fast-uri` and WHATWG URL implementations are expected.
|
|
14
|
-
Reports based on comparing or mixing parsers that follow these different
|
|
14
|
+
Reports based solely on comparing or mixing parsers that follow these different
|
|
15
15
|
standards are out of scope. Applications must use the same parsing and
|
|
16
16
|
normalization rules for both security decisions and subsequent URI use.
|
|
17
17
|
|
|
18
|
+
Reports are assessed based on reproducibility, documented supported usage, and
|
|
19
|
+
concrete security impact—not merely on whether they mention WHATWG URL
|
|
20
|
+
behavior. Reports demonstrating a parsing or normalization flaw in a supported
|
|
21
|
+
scheme remain eligible for evaluation.
|
|
22
|
+
|
|
18
23
|
## Threat model
|
|
19
24
|
|
|
20
25
|
`fast-uri`'s threat model extends the
|
package/benchmark/benchmark.mjs
CHANGED
|
@@ -10,6 +10,21 @@ const ipv6 = '//[2001:db8::7]'
|
|
|
10
10
|
const urn = 'urn:foo:a123,456'
|
|
11
11
|
const urnuuid = 'urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6'
|
|
12
12
|
|
|
13
|
+
const mailtoSimple = 'mailto:chris@example.com'
|
|
14
|
+
const mailtoWithSubject = 'mailto:infobot@example.com?subject=current-issue'
|
|
15
|
+
const mailtoWithBody = 'mailto:infobot@example.com?body=send%20current-issue%0D%0Asend%20index'
|
|
16
|
+
const mailtoWithHeaders = 'mailto:list@example.org?In-Reply-To=%3C3469A91.D10AF4C@example.com%3E'
|
|
17
|
+
const mailtoMultiple = 'mailto:joe@example.com,alice@example.org?subject=Test&body=NATTO'
|
|
18
|
+
const mailtoEncoded = 'mailto:%22oh%5C%5Cno%22@example.org'
|
|
19
|
+
|
|
20
|
+
const mailtoComponent = {
|
|
21
|
+
scheme: 'mailto',
|
|
22
|
+
to: ['chris@example.com'],
|
|
23
|
+
subject: 'current-issue',
|
|
24
|
+
body: 'send current-issue\r\nsend index',
|
|
25
|
+
headers: { 'In-Reply-To': '<3469A91.D10AF4C@example.com>' }
|
|
26
|
+
}
|
|
27
|
+
|
|
13
28
|
const urnuuidComponent = {
|
|
14
29
|
scheme: 'urn',
|
|
15
30
|
nid: 'uuid',
|
|
@@ -146,6 +161,62 @@ benchUriJs.add('urijs: equal', function () {
|
|
|
146
161
|
uriJsEqual('example://a/b/c/%7Bfoo%7D', 'eXAMPLE://a/./b/../b/%63/%7bfoo%7d')
|
|
147
162
|
})
|
|
148
163
|
|
|
164
|
+
benchFastUri.add('fast-uri: parse mailto simple', function () {
|
|
165
|
+
fastUriParse(mailtoSimple)
|
|
166
|
+
})
|
|
167
|
+
benchUriJs.add('urijs: parse mailto simple', function () {
|
|
168
|
+
uriJsParse(mailtoSimple)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
benchFastUri.add('fast-uri: parse mailto subject', function () {
|
|
172
|
+
fastUriParse(mailtoWithSubject)
|
|
173
|
+
})
|
|
174
|
+
benchUriJs.add('urijs: parse mailto subject', function () {
|
|
175
|
+
uriJsParse(mailtoWithSubject)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
benchFastUri.add('fast-uri: parse mailto body CRLF', function () {
|
|
179
|
+
fastUriParse(mailtoWithBody)
|
|
180
|
+
})
|
|
181
|
+
benchUriJs.add('urijs: parse mailto body CRLF', function () {
|
|
182
|
+
uriJsParse(mailtoWithBody)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
benchFastUri.add('fast-uri: parse mailto headers', function () {
|
|
186
|
+
fastUriParse(mailtoWithHeaders)
|
|
187
|
+
})
|
|
188
|
+
benchUriJs.add('urijs: parse mailto headers', function () {
|
|
189
|
+
uriJsParse(mailtoWithHeaders)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
benchFastUri.add('fast-uri: parse mailto multi recipient', function () {
|
|
193
|
+
fastUriParse(mailtoMultiple)
|
|
194
|
+
})
|
|
195
|
+
benchUriJs.add('urijs: parse mailto multi recipient', function () {
|
|
196
|
+
uriJsParse(mailtoMultiple)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
benchFastUri.add('fast-uri: parse mailto encoded local', function () {
|
|
200
|
+
fastUriParse(mailtoEncoded)
|
|
201
|
+
})
|
|
202
|
+
benchUriJs.add('urijs: parse mailto encoded local', function () {
|
|
203
|
+
uriJsParse(mailtoEncoded)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
benchFastUri.add('fast-uri: serialize mailto', function () {
|
|
207
|
+
fastUriSerialize(mailtoComponent)
|
|
208
|
+
})
|
|
209
|
+
benchUriJs.add('urijs: serialize mailto', function () {
|
|
210
|
+
uriJsSerialize(mailtoComponent)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
benchFastUri.add('fast-uri: serialize+parse mailto round-trip', function () {
|
|
214
|
+
fastUriSerialize(fastUriParse(mailtoWithBody))
|
|
215
|
+
})
|
|
216
|
+
benchUriJs.add('urijs: serialize+parse mailto round-trip', function () {
|
|
217
|
+
uriJsSerialize(uriJsParse(mailtoWithBody))
|
|
218
|
+
})
|
|
219
|
+
|
|
149
220
|
await benchFastUri.run()
|
|
150
221
|
console.log(benchFastUri.name)
|
|
151
222
|
console.table(benchFastUri.table())
|
package/index.js
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, normalizeQueryFragmentEncoding,
|
|
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,12 +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 {
|
|
30
|
-
|
|
31
|
-
|
|
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
|
+
) {
|
|
32
72
|
throw new Error(baseParsed.error || relativeParsed.error || 'URI is malformed.')
|
|
33
73
|
}
|
|
34
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
|
+
}
|
|
35
88
|
schemelessOptions.skipEscape = true
|
|
36
89
|
return serialize(resolved, schemelessOptions)
|
|
37
90
|
}
|
|
@@ -114,7 +167,7 @@ function equal (uriA, uriB, options) {
|
|
|
114
167
|
const normalizedA = normalizeComparableURI(uriA, options)
|
|
115
168
|
const normalizedB = normalizeComparableURI(uriB, options)
|
|
116
169
|
|
|
117
|
-
return normalizedA !== undefined && normalizedB !== undefined && normalizedA
|
|
170
|
+
return normalizedA !== undefined && normalizedB !== undefined && normalizedA === normalizedB
|
|
118
171
|
}
|
|
119
172
|
|
|
120
173
|
/**
|
|
@@ -137,30 +190,39 @@ function serialize (cmpts, opts) {
|
|
|
137
190
|
reference: cmpts.reference,
|
|
138
191
|
resourceName: cmpts.resourceName,
|
|
139
192
|
secure: cmpts.secure,
|
|
193
|
+
to: cmpts.to,
|
|
194
|
+
subject: cmpts.subject,
|
|
195
|
+
body: cmpts.body,
|
|
196
|
+
headers: cmpts.headers,
|
|
140
197
|
error: ''
|
|
141
198
|
}
|
|
142
199
|
const options = Object.assign({}, opts)
|
|
143
200
|
const uriTokens = []
|
|
144
201
|
|
|
202
|
+
if (component.scheme) {
|
|
203
|
+
component.scheme = decodeValidScheme(component.scheme)
|
|
204
|
+
}
|
|
205
|
+
|
|
145
206
|
// find scheme handler
|
|
146
207
|
const schemeHandler = getSchemeHandler(options.scheme || component.scheme)
|
|
147
208
|
|
|
148
209
|
// perform scheme specific serialization
|
|
149
210
|
if (schemeHandler && schemeHandler.serialize) schemeHandler.serialize(component, options)
|
|
150
211
|
|
|
212
|
+
const hasAuthority = component.userinfo !== undefined || component.host !== undefined || component.port !== undefined
|
|
213
|
+
const pathNoScheme = !options.skipEscape && component.scheme === undefined && !hasAuthority
|
|
214
|
+
|
|
151
215
|
if (component.path !== undefined) {
|
|
152
216
|
if (!options.skipEscape) {
|
|
153
|
-
component.path =
|
|
154
|
-
|
|
155
|
-
if (component.scheme !== undefined) {
|
|
156
|
-
component.path = component.path.split('%3A').join(':')
|
|
157
|
-
}
|
|
217
|
+
component.path = serializePathEncoding(component.path, pathNoScheme)
|
|
158
218
|
} else {
|
|
159
219
|
component.path = normalizePercentEncoding(component.path)
|
|
160
220
|
}
|
|
161
221
|
}
|
|
162
222
|
|
|
163
223
|
if (options.reference !== 'suffix' && component.scheme) {
|
|
224
|
+
// Scheme handlers may replace the scheme during serialization.
|
|
225
|
+
component.scheme = decodeValidScheme(component.scheme)
|
|
164
226
|
uriTokens.push(component.scheme, ':')
|
|
165
227
|
}
|
|
166
228
|
|
|
@@ -183,6 +245,13 @@ function serialize (cmpts, opts) {
|
|
|
183
245
|
s = removeDotSegments(s)
|
|
184
246
|
}
|
|
185
247
|
|
|
248
|
+
// Dot-segment removal can expose a colon that was not originally in the
|
|
249
|
+
// first segment (for example, "./a:b"). Reapply path-noscheme encoding so
|
|
250
|
+
// the serialized relative reference cannot be reparsed as a URI scheme.
|
|
251
|
+
if (pathNoScheme) {
|
|
252
|
+
s = serializePathEncoding(s, true)
|
|
253
|
+
}
|
|
254
|
+
|
|
186
255
|
if (
|
|
187
256
|
authority === undefined &&
|
|
188
257
|
s[0] === '/' &&
|
|
@@ -196,11 +265,11 @@ function serialize (cmpts, opts) {
|
|
|
196
265
|
}
|
|
197
266
|
|
|
198
267
|
if (component.query !== undefined) {
|
|
199
|
-
uriTokens.push('?', component.query)
|
|
268
|
+
uriTokens.push('?', encodeQuery(component.query))
|
|
200
269
|
}
|
|
201
270
|
|
|
202
271
|
if (component.fragment !== undefined) {
|
|
203
|
-
uriTokens.push('#', component.fragment)
|
|
272
|
+
uriTokens.push('#', encodeFragment(component.fragment))
|
|
204
273
|
}
|
|
205
274
|
return uriTokens.join('')
|
|
206
275
|
}
|
|
@@ -237,10 +306,93 @@ function getParseError (parsed, matches) {
|
|
|
237
306
|
return undefined
|
|
238
307
|
}
|
|
239
308
|
|
|
309
|
+
/**
|
|
310
|
+
* Checks percent syntax without decoding the represented octets. RFC 3986
|
|
311
|
+
* percent-encoding is byte-oriented, so sequences such as `%FF` are valid even
|
|
312
|
+
* though they are not independently valid UTF-8.
|
|
313
|
+
*
|
|
314
|
+
* @param {string|undefined} component
|
|
315
|
+
* @returns {boolean}
|
|
316
|
+
*/
|
|
317
|
+
function hasMalformedPercentEncoding (component) {
|
|
318
|
+
if (component === undefined) return false
|
|
319
|
+
|
|
320
|
+
let percent = component.indexOf('%')
|
|
321
|
+
while (percent !== -1) {
|
|
322
|
+
if (percent + 2 >= component.length || !/^[\da-f]{2}$/iu.test(component.slice(percent + 1, percent + 3))) {
|
|
323
|
+
return true
|
|
324
|
+
}
|
|
325
|
+
percent = component.indexOf('%', percent + 3)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return false
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Whether the host is a bracketed IP literal (RFC 3986 `IP-literal`).
|
|
333
|
+
* An unterminated `[` is not a literal, so it must still be validated as a
|
|
334
|
+
* reg-name instead of being waved through as an IP.
|
|
335
|
+
*
|
|
336
|
+
* @param {string} host
|
|
337
|
+
* @returns {boolean}
|
|
338
|
+
*/
|
|
339
|
+
function isIPLiteral (host) {
|
|
340
|
+
return host[0] === '[' && host[host.length - 1] === ']'
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* @param {RegExpMatchArray} matches
|
|
345
|
+
* @returns {boolean}
|
|
346
|
+
*/
|
|
347
|
+
function hasMalformedComponentPercentEncoding (matches) {
|
|
348
|
+
// Bracketed IP literals use a raw "%" as the zone separator for historical
|
|
349
|
+
// compatibility. Their parsing is intentionally left to normalizeIPv6.
|
|
350
|
+
const host = matches[4]
|
|
351
|
+
return hasMalformedPercentEncoding(matches[3]) ||
|
|
352
|
+
(host !== undefined && !isIPLiteral(host) && hasMalformedPercentEncoding(host)) ||
|
|
353
|
+
hasMalformedPercentEncoding(matches[6]) ||
|
|
354
|
+
hasMalformedPercentEncoding(matches[7]) ||
|
|
355
|
+
hasMalformedPercentEncoding(matches[8])
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* @param {import('./types/index').URIComponent} parsed
|
|
360
|
+
* @param {import('./types/index').Options} options
|
|
361
|
+
* @param {{ domainHost?: boolean, unicodeSupport?: boolean }|undefined} schemeHandler
|
|
362
|
+
* @param {boolean} isIP
|
|
363
|
+
* @returns {boolean} whether host conversion failed
|
|
364
|
+
*/
|
|
365
|
+
function canonicalizeHost (parsed, options, schemeHandler, isIP) {
|
|
366
|
+
const bracketedIPLiteral =
|
|
367
|
+
parsed.host &&
|
|
368
|
+
parsed.host[0] === '[' &&
|
|
369
|
+
parsed.host[parsed.host.length - 1] === ']'
|
|
370
|
+
|
|
371
|
+
if (
|
|
372
|
+
!options.unicodeSupport &&
|
|
373
|
+
(!schemeHandler || !schemeHandler.unicodeSupport) &&
|
|
374
|
+
parsed.host &&
|
|
375
|
+
!isIPLiteral(parsed.host) &&
|
|
376
|
+
(options.domainHost || (schemeHandler && schemeHandler.domainHost)) &&
|
|
377
|
+
isIP === false &&
|
|
378
|
+
!bracketedIPLiteral &&
|
|
379
|
+
nonSimpleDomain(parsed.host)
|
|
380
|
+
) {
|
|
381
|
+
try {
|
|
382
|
+
parsed.host = new URL('http://' + parsed.host).hostname
|
|
383
|
+
} catch (e) {
|
|
384
|
+
parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e
|
|
385
|
+
return true
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return false
|
|
390
|
+
}
|
|
391
|
+
|
|
240
392
|
/**
|
|
241
393
|
* @param {string} uri
|
|
242
394
|
* @param {import('./types/index').Options} [opts]
|
|
243
|
-
* @returns {{ parsed: import('./types/index').URIComponent, malformedAuthorityOrPort: boolean }}
|
|
395
|
+
* @returns {{ parsed: import('./types/index').URIComponent, malformedAuthorityOrPort: boolean, malformedPercentEncoding: boolean, malformedSchemeSpecific: boolean, malformedHost: boolean, malformedScheme: boolean }}
|
|
244
396
|
*/
|
|
245
397
|
function parseWithStatus (uri, opts) {
|
|
246
398
|
const options = Object.assign({}, opts)
|
|
@@ -256,6 +408,11 @@ function parseWithStatus (uri, opts) {
|
|
|
256
408
|
}
|
|
257
409
|
|
|
258
410
|
let malformedAuthorityOrPort = false
|
|
411
|
+
let malformedPercentEncoding = false
|
|
412
|
+
let malformedSchemeSpecific = false
|
|
413
|
+
let malformedHost = false
|
|
414
|
+
let malformedIPLiteral = false
|
|
415
|
+
let malformedScheme = false
|
|
259
416
|
|
|
260
417
|
let isIP = false
|
|
261
418
|
if (options.reference === 'suffix') {
|
|
@@ -305,7 +462,7 @@ function parseWithStatus (uri, opts) {
|
|
|
305
462
|
|
|
306
463
|
if (matches) {
|
|
307
464
|
// store each component
|
|
308
|
-
parsed.scheme = matches[1]
|
|
465
|
+
parsed.scheme = matches[1]
|
|
309
466
|
parsed.userinfo = matches[3]
|
|
310
467
|
parsed.host = matches[4]
|
|
311
468
|
parsed.port = parseInt(matches[5], 10)
|
|
@@ -313,6 +470,21 @@ function parseWithStatus (uri, opts) {
|
|
|
313
470
|
parsed.query = matches[7]
|
|
314
471
|
parsed.fragment = matches[8]
|
|
315
472
|
|
|
473
|
+
if (parsed.scheme !== undefined) {
|
|
474
|
+
const decodedScheme = unescape(parsed.scheme)
|
|
475
|
+
if (VALID_SCHEME.test(decodedScheme)) {
|
|
476
|
+
parsed.scheme = decodedScheme.toLowerCase()
|
|
477
|
+
} else {
|
|
478
|
+
parsed.error = parsed.error || MALFORMED_SCHEME_ERROR
|
|
479
|
+
malformedScheme = true
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
malformedPercentEncoding = hasMalformedComponentPercentEncoding(matches)
|
|
484
|
+
if (malformedPercentEncoding) {
|
|
485
|
+
parsed.error = parsed.error || 'URI contains malformed percent-encoding.'
|
|
486
|
+
}
|
|
487
|
+
|
|
316
488
|
// fix port number
|
|
317
489
|
if (isNaN(parsed.port)) {
|
|
318
490
|
parsed.port = matches[5]
|
|
@@ -327,9 +499,17 @@ function parseWithStatus (uri, opts) {
|
|
|
327
499
|
if (parsed.host) {
|
|
328
500
|
const ipv4result = isIPv4(parsed.host)
|
|
329
501
|
if (ipv4result === false) {
|
|
502
|
+
const bracketedIPLiteral = isIPLiteral(parsed.host)
|
|
503
|
+
const hasIPLiteralBracket = parsed.host.indexOf('[') !== -1 || parsed.host.indexOf(']') !== -1
|
|
330
504
|
const ipv6result = normalizeIPv6(parsed.host)
|
|
331
|
-
|
|
332
|
-
|
|
505
|
+
isIP = ipv6result.isIPV6 || ipv6result.isIPVFuture === true
|
|
506
|
+
malformedIPLiteral = hasIPLiteralBracket && (!bracketedIPLiteral || ipv6result.error === true)
|
|
507
|
+
parsed.host = isIP ? ipv6result.host : ipv6result.host.toLowerCase()
|
|
508
|
+
|
|
509
|
+
if (malformedIPLiteral) {
|
|
510
|
+
parsed.error = parsed.error || 'URI host is malformed.'
|
|
511
|
+
malformedAuthorityOrPort = true
|
|
512
|
+
}
|
|
333
513
|
} else {
|
|
334
514
|
isIP = true
|
|
335
515
|
}
|
|
@@ -352,27 +532,16 @@ function parseWithStatus (uri, opts) {
|
|
|
352
532
|
// find scheme handler
|
|
353
533
|
const schemeHandler = getSchemeHandler(options.scheme || parsed.scheme)
|
|
354
534
|
|
|
355
|
-
//
|
|
356
|
-
if (!
|
|
357
|
-
|
|
358
|
-
if (parsed.host && (options.domainHost || (schemeHandler && schemeHandler.domainHost)) && isIP === false && nonSimpleDomain(parsed.host)) {
|
|
359
|
-
// convert Unicode IDN -> ASCII IDN
|
|
360
|
-
try {
|
|
361
|
-
parsed.host = new URL('http://' + parsed.host).hostname
|
|
362
|
-
} catch (e) {
|
|
363
|
-
parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
// convert IRI -> URI
|
|
535
|
+
// convert Unicode IDN -> ASCII IDN when the effective scheme uses domain hosts
|
|
536
|
+
if (!malformedIPLiteral) {
|
|
537
|
+
malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP)
|
|
367
538
|
}
|
|
368
539
|
|
|
369
540
|
if (!schemeHandler || (schemeHandler && !schemeHandler.skipNormalize)) {
|
|
370
541
|
if (uri.indexOf('%') !== -1) {
|
|
371
|
-
if (parsed.
|
|
372
|
-
parsed.
|
|
373
|
-
|
|
374
|
-
if (parsed.host !== undefined) {
|
|
375
|
-
parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP)
|
|
542
|
+
if (parsed.host !== undefined && !malformedIPLiteral) {
|
|
543
|
+
const host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true)
|
|
544
|
+
parsed.host = reescapeHostDelimiters(host, isIP)
|
|
376
545
|
}
|
|
377
546
|
}
|
|
378
547
|
if (parsed.path) {
|
|
@@ -389,11 +558,14 @@ function parseWithStatus (uri, opts) {
|
|
|
389
558
|
// perform scheme specific parsing
|
|
390
559
|
if (schemeHandler && schemeHandler.parse) {
|
|
391
560
|
schemeHandler.parse(parsed, options)
|
|
561
|
+
if (schemeHandler === SCHEMES.urn && parsed.nid === undefined) {
|
|
562
|
+
malformedSchemeSpecific = true
|
|
563
|
+
}
|
|
392
564
|
}
|
|
393
565
|
} else {
|
|
394
566
|
parsed.error = parsed.error || 'URI can not be parsed.'
|
|
395
567
|
}
|
|
396
|
-
return { parsed, malformedAuthorityOrPort }
|
|
568
|
+
return { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme }
|
|
397
569
|
}
|
|
398
570
|
|
|
399
571
|
/**
|
|
@@ -417,13 +589,17 @@ function normalizeString (uri, opts) {
|
|
|
417
589
|
/**
|
|
418
590
|
* @param {string} uri
|
|
419
591
|
* @param {import('./types/index').Options} [opts]
|
|
420
|
-
* @returns {{ normalized: string, malformedAuthorityOrPort: boolean }}
|
|
592
|
+
* @returns {{ normalized: string, malformedAuthorityOrPort: boolean, malformedPercentEncoding: boolean, malformedSchemeSpecific: boolean, malformedHost: boolean, malformedScheme: boolean }}
|
|
421
593
|
*/
|
|
422
594
|
function normalizeStringWithStatus (uri, opts) {
|
|
423
|
-
const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri, opts)
|
|
595
|
+
const { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = parseWithStatus(uri, opts)
|
|
424
596
|
return {
|
|
425
|
-
normalized: malformedAuthorityOrPort ? uri : serialize(parsed, opts),
|
|
426
|
-
malformedAuthorityOrPort
|
|
597
|
+
normalized: malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? uri : serialize(parsed, opts),
|
|
598
|
+
malformedAuthorityOrPort,
|
|
599
|
+
malformedPercentEncoding,
|
|
600
|
+
malformedSchemeSpecific,
|
|
601
|
+
malformedHost,
|
|
602
|
+
malformedScheme
|
|
427
603
|
}
|
|
428
604
|
}
|
|
429
605
|
|
|
@@ -433,14 +609,18 @@ function normalizeStringWithStatus (uri, opts) {
|
|
|
433
609
|
* @returns {string|undefined}
|
|
434
610
|
*/
|
|
435
611
|
function normalizeComparableURI (uri, opts) {
|
|
436
|
-
if (typeof uri
|
|
437
|
-
|
|
438
|
-
return malformedAuthorityOrPort ? undefined : normalized
|
|
612
|
+
if (typeof uri !== 'string' && typeof uri !== 'object') {
|
|
613
|
+
return undefined
|
|
439
614
|
}
|
|
440
615
|
|
|
441
|
-
|
|
442
|
-
|
|
616
|
+
let value
|
|
617
|
+
try {
|
|
618
|
+
value = typeof uri === 'string' ? uri : serialize(uri, opts)
|
|
619
|
+
} catch {
|
|
620
|
+
return undefined
|
|
443
621
|
}
|
|
622
|
+
const { normalized, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = normalizeStringWithStatus(value, opts)
|
|
623
|
+
return malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? undefined : normalized
|
|
444
624
|
}
|
|
445
625
|
|
|
446
626
|
const fastUri = {
|