fast-uri 4.1.2 → 4.1.3
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 +202 -44
- package/lib/schemes.js +384 -7
- package/lib/utils.js +302 -88
- package/package.json +1 -1
- package/test/component-safe-serialization.test.js +105 -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 +90 -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,74 @@ 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
|
+
* @param {RegExpMatchArray} matches
|
|
333
|
+
* @returns {boolean}
|
|
334
|
+
*/
|
|
335
|
+
function hasMalformedComponentPercentEncoding (matches) {
|
|
336
|
+
// Bracketed IP literals use a raw "%" as the zone separator for historical
|
|
337
|
+
// compatibility. Their parsing is intentionally left to normalizeIPv6.
|
|
338
|
+
const host = matches[4]
|
|
339
|
+
return hasMalformedPercentEncoding(matches[3]) ||
|
|
340
|
+
(host !== undefined && !(host[0] === '[' && host[host.length - 1] === ']') && hasMalformedPercentEncoding(host)) ||
|
|
341
|
+
hasMalformedPercentEncoding(matches[6]) ||
|
|
342
|
+
hasMalformedPercentEncoding(matches[7]) ||
|
|
343
|
+
hasMalformedPercentEncoding(matches[8])
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* @param {import('./types/index').URIComponent} parsed
|
|
348
|
+
* @param {import('./types/index').Options} options
|
|
349
|
+
* @param {{ domainHost?: boolean, unicodeSupport?: boolean }|undefined} schemeHandler
|
|
350
|
+
* @param {boolean} isIP
|
|
351
|
+
* @returns {boolean} whether host conversion failed
|
|
352
|
+
*/
|
|
353
|
+
function canonicalizeHost (parsed, options, schemeHandler, isIP) {
|
|
354
|
+
if (
|
|
355
|
+
!options.unicodeSupport &&
|
|
356
|
+
(!schemeHandler || !schemeHandler.unicodeSupport) &&
|
|
357
|
+
parsed.host &&
|
|
358
|
+
parsed.host[0] !== '[' &&
|
|
359
|
+
(options.domainHost || (schemeHandler && schemeHandler.domainHost)) &&
|
|
360
|
+
isIP === false &&
|
|
361
|
+
nonSimpleDomain(parsed.host)
|
|
362
|
+
) {
|
|
363
|
+
try {
|
|
364
|
+
parsed.host = new URL('http://' + parsed.host).hostname
|
|
365
|
+
} catch (e) {
|
|
366
|
+
parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e
|
|
367
|
+
return true
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return false
|
|
371
|
+
}
|
|
372
|
+
|
|
240
373
|
/**
|
|
241
374
|
* @param {string} uri
|
|
242
375
|
* @param {import('./types/index').Options} [opts]
|
|
243
|
-
* @returns {{ parsed: import('./types/index').URIComponent, malformedAuthorityOrPort: boolean }}
|
|
376
|
+
* @returns {{ parsed: import('./types/index').URIComponent, malformedAuthorityOrPort: boolean, malformedPercentEncoding: boolean, malformedSchemeSpecific: boolean, malformedHost: boolean, malformedScheme: boolean }}
|
|
244
377
|
*/
|
|
245
378
|
function parseWithStatus (uri, opts) {
|
|
246
379
|
const options = Object.assign({}, opts)
|
|
@@ -256,6 +389,11 @@ function parseWithStatus (uri, opts) {
|
|
|
256
389
|
}
|
|
257
390
|
|
|
258
391
|
let malformedAuthorityOrPort = false
|
|
392
|
+
let malformedPercentEncoding = false
|
|
393
|
+
let malformedSchemeSpecific = false
|
|
394
|
+
let malformedHost = false
|
|
395
|
+
let malformedIPLiteral = false
|
|
396
|
+
let malformedScheme = false
|
|
259
397
|
|
|
260
398
|
let isIP = false
|
|
261
399
|
if (options.reference === 'suffix') {
|
|
@@ -305,7 +443,7 @@ function parseWithStatus (uri, opts) {
|
|
|
305
443
|
|
|
306
444
|
if (matches) {
|
|
307
445
|
// store each component
|
|
308
|
-
parsed.scheme = matches[1]
|
|
446
|
+
parsed.scheme = matches[1]
|
|
309
447
|
parsed.userinfo = matches[3]
|
|
310
448
|
parsed.host = matches[4]
|
|
311
449
|
parsed.port = parseInt(matches[5], 10)
|
|
@@ -313,6 +451,21 @@ function parseWithStatus (uri, opts) {
|
|
|
313
451
|
parsed.query = matches[7]
|
|
314
452
|
parsed.fragment = matches[8]
|
|
315
453
|
|
|
454
|
+
if (parsed.scheme !== undefined) {
|
|
455
|
+
const decodedScheme = unescape(parsed.scheme)
|
|
456
|
+
if (VALID_SCHEME.test(decodedScheme)) {
|
|
457
|
+
parsed.scheme = decodedScheme.toLowerCase()
|
|
458
|
+
} else {
|
|
459
|
+
parsed.error = parsed.error || MALFORMED_SCHEME_ERROR
|
|
460
|
+
malformedScheme = true
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
malformedPercentEncoding = hasMalformedComponentPercentEncoding(matches)
|
|
465
|
+
if (malformedPercentEncoding) {
|
|
466
|
+
parsed.error = parsed.error || 'URI contains malformed percent-encoding.'
|
|
467
|
+
}
|
|
468
|
+
|
|
316
469
|
// fix port number
|
|
317
470
|
if (isNaN(parsed.port)) {
|
|
318
471
|
parsed.port = matches[5]
|
|
@@ -327,9 +480,16 @@ function parseWithStatus (uri, opts) {
|
|
|
327
480
|
if (parsed.host) {
|
|
328
481
|
const ipv4result = isIPv4(parsed.host)
|
|
329
482
|
if (ipv4result === false) {
|
|
483
|
+
const bracketedIPLiteral = parsed.host[0] === '[' && parsed.host[parsed.host.length - 1] === ']'
|
|
330
484
|
const ipv6result = normalizeIPv6(parsed.host)
|
|
331
|
-
|
|
332
|
-
|
|
485
|
+
isIP = ipv6result.isIPV6 || ipv6result.isIPVFuture === true
|
|
486
|
+
malformedIPLiteral = bracketedIPLiteral && ipv6result.error === true
|
|
487
|
+
parsed.host = isIP ? ipv6result.host : ipv6result.host.toLowerCase()
|
|
488
|
+
|
|
489
|
+
if (malformedIPLiteral) {
|
|
490
|
+
parsed.error = parsed.error || 'URI host is malformed.'
|
|
491
|
+
malformedAuthorityOrPort = true
|
|
492
|
+
}
|
|
333
493
|
} else {
|
|
334
494
|
isIP = true
|
|
335
495
|
}
|
|
@@ -352,27 +512,14 @@ function parseWithStatus (uri, opts) {
|
|
|
352
512
|
// find scheme handler
|
|
353
513
|
const schemeHandler = getSchemeHandler(options.scheme || parsed.scheme)
|
|
354
514
|
|
|
355
|
-
//
|
|
356
|
-
|
|
357
|
-
// if host component is a domain name
|
|
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
|
|
367
|
-
}
|
|
515
|
+
// convert Unicode IDN -> ASCII IDN when the effective scheme uses domain hosts
|
|
516
|
+
malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP)
|
|
368
517
|
|
|
369
518
|
if (!schemeHandler || (schemeHandler && !schemeHandler.skipNormalize)) {
|
|
370
519
|
if (uri.indexOf('%') !== -1) {
|
|
371
|
-
if (parsed.
|
|
372
|
-
parsed.
|
|
373
|
-
|
|
374
|
-
if (parsed.host !== undefined) {
|
|
375
|
-
parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP)
|
|
520
|
+
if (parsed.host !== undefined && !malformedIPLiteral) {
|
|
521
|
+
const host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true)
|
|
522
|
+
parsed.host = reescapeHostDelimiters(host, isIP)
|
|
376
523
|
}
|
|
377
524
|
}
|
|
378
525
|
if (parsed.path) {
|
|
@@ -389,11 +536,14 @@ function parseWithStatus (uri, opts) {
|
|
|
389
536
|
// perform scheme specific parsing
|
|
390
537
|
if (schemeHandler && schemeHandler.parse) {
|
|
391
538
|
schemeHandler.parse(parsed, options)
|
|
539
|
+
if (schemeHandler === SCHEMES.urn && parsed.nid === undefined) {
|
|
540
|
+
malformedSchemeSpecific = true
|
|
541
|
+
}
|
|
392
542
|
}
|
|
393
543
|
} else {
|
|
394
544
|
parsed.error = parsed.error || 'URI can not be parsed.'
|
|
395
545
|
}
|
|
396
|
-
return { parsed, malformedAuthorityOrPort }
|
|
546
|
+
return { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme }
|
|
397
547
|
}
|
|
398
548
|
|
|
399
549
|
/**
|
|
@@ -417,13 +567,17 @@ function normalizeString (uri, opts) {
|
|
|
417
567
|
/**
|
|
418
568
|
* @param {string} uri
|
|
419
569
|
* @param {import('./types/index').Options} [opts]
|
|
420
|
-
* @returns {{ normalized: string, malformedAuthorityOrPort: boolean }}
|
|
570
|
+
* @returns {{ normalized: string, malformedAuthorityOrPort: boolean, malformedPercentEncoding: boolean, malformedSchemeSpecific: boolean, malformedHost: boolean, malformedScheme: boolean }}
|
|
421
571
|
*/
|
|
422
572
|
function normalizeStringWithStatus (uri, opts) {
|
|
423
|
-
const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri, opts)
|
|
573
|
+
const { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = parseWithStatus(uri, opts)
|
|
424
574
|
return {
|
|
425
|
-
normalized: malformedAuthorityOrPort ? uri : serialize(parsed, opts),
|
|
426
|
-
malformedAuthorityOrPort
|
|
575
|
+
normalized: malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? uri : serialize(parsed, opts),
|
|
576
|
+
malformedAuthorityOrPort,
|
|
577
|
+
malformedPercentEncoding,
|
|
578
|
+
malformedSchemeSpecific,
|
|
579
|
+
malformedHost,
|
|
580
|
+
malformedScheme
|
|
427
581
|
}
|
|
428
582
|
}
|
|
429
583
|
|
|
@@ -433,14 +587,18 @@ function normalizeStringWithStatus (uri, opts) {
|
|
|
433
587
|
* @returns {string|undefined}
|
|
434
588
|
*/
|
|
435
589
|
function normalizeComparableURI (uri, opts) {
|
|
436
|
-
if (typeof uri
|
|
437
|
-
|
|
438
|
-
return malformedAuthorityOrPort ? undefined : normalized
|
|
590
|
+
if (typeof uri !== 'string' && typeof uri !== 'object') {
|
|
591
|
+
return undefined
|
|
439
592
|
}
|
|
440
593
|
|
|
441
|
-
|
|
442
|
-
|
|
594
|
+
let value
|
|
595
|
+
try {
|
|
596
|
+
value = typeof uri === 'string' ? uri : serialize(uri, opts)
|
|
597
|
+
} catch {
|
|
598
|
+
return undefined
|
|
443
599
|
}
|
|
600
|
+
const { normalized, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = normalizeStringWithStatus(value, opts)
|
|
601
|
+
return malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? undefined : normalized
|
|
444
602
|
}
|
|
445
603
|
|
|
446
604
|
const fastUri = {
|