fast-uri 2.4.5 → 2.4.7

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 CHANGED
@@ -283,7 +283,7 @@ function canonicalizeHost (parsed, options, schemeHandler, isIP) {
283
283
  !options.unicodeSupport &&
284
284
  (!schemeHandler || !schemeHandler.unicodeSupport) &&
285
285
  parsed.host &&
286
- parsed.host[0] !== '[' &&
286
+ !isIPLiteral(parsed.host) &&
287
287
  (options.domainHost || (schemeHandler && schemeHandler.domainHost)) &&
288
288
  isIP === false &&
289
289
  nonSimpleDomain(parsed.host)
@@ -312,6 +312,18 @@ function hasMalformedPercentEncoding (component) {
312
312
  return false
313
313
  }
314
314
 
315
+ /**
316
+ * Whether the host is a bracketed IP literal (RFC 3986 `IP-literal`).
317
+ * An unterminated `[` is not a literal, so it must still be validated as a
318
+ * reg-name instead of being waved through as an IP.
319
+ *
320
+ * @param {string} host
321
+ * @returns {boolean}
322
+ */
323
+ function isIPLiteral (host) {
324
+ return host[0] === '[' && host[host.length - 1] === ']'
325
+ }
326
+
315
327
  /**
316
328
  * @param {RegExpMatchArray} matches
317
329
  * @returns {boolean}
@@ -321,7 +333,7 @@ function hasMalformedComponentPercentEncoding (matches) {
321
333
  // compatibility. Their parsing is intentionally left to normalizeIPv6.
322
334
  const host = matches[4]
323
335
  return hasMalformedPercentEncoding(matches[3]) ||
324
- (host !== undefined && !(host[0] === '[' && host[host.length - 1] === ']') && hasMalformedPercentEncoding(host)) ||
336
+ (host !== undefined && !isIPLiteral(host) && hasMalformedPercentEncoding(host)) ||
325
337
  hasMalformedPercentEncoding(matches[6]) ||
326
338
  hasMalformedPercentEncoding(matches[7]) ||
327
339
  hasMalformedPercentEncoding(matches[8])
@@ -423,10 +435,11 @@ function parseWithStatus (uri, opts) {
423
435
  if (parsed.host) {
424
436
  const ipv4result = isIPv4(parsed.host)
425
437
  if (ipv4result === false) {
426
- const bracketedIPLiteral = parsed.host[0] === '[' && parsed.host[parsed.host.length - 1] === ']'
438
+ const bracketedIPLiteral = isIPLiteral(parsed.host)
439
+ const hasIPLiteralBracket = parsed.host.indexOf('[') !== -1 || parsed.host.indexOf(']') !== -1
427
440
  const ipv6result = normalizeIPv6(parsed.host)
428
441
  isIP = ipv6result.isIPV6 || ipv6result.isIPVFuture === true
429
- malformedIPLiteral = bracketedIPLiteral && ipv6result.error === true
442
+ malformedIPLiteral = hasIPLiteralBracket && (!bracketedIPLiteral || ipv6result.error === true)
430
443
  parsed.host = isIP ? ipv6result.host : ipv6result.host.toLowerCase()
431
444
 
432
445
  if (malformedIPLiteral) {
@@ -456,13 +469,21 @@ function parseWithStatus (uri, opts) {
456
469
  const schemeHandler = SCHEMES[(options.scheme || parsed.scheme || '').toLowerCase()]
457
470
 
458
471
  // convert Unicode IDN -> ASCII IDN when the effective scheme uses domain hosts
459
- malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP)
472
+ if (!malformedIPLiteral) {
473
+ malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP)
474
+ }
460
475
 
461
- if (!schemeHandler || (schemeHandler && !schemeHandler.skipNormalize)) {
462
- if (parsed.host !== undefined && !malformedIPLiteral) {
463
- const host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true)
464
- parsed.host = reescapeHostDelimiters(host, isIP)
476
+ if (uri.indexOf('%') !== -1 && parsed.host !== undefined && !malformedIPLiteral) {
477
+ let host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true)
478
+ if (!isIP) {
479
+ // Fold reg-name case after decoding unreserved octets. The second
480
+ // pass only restores uppercase hex in escapes that remain encoded.
481
+ host = normalizePercentEncoding(host.toLowerCase())
465
482
  }
483
+ parsed.host = reescapeHostDelimiters(host, isIP)
484
+ }
485
+
486
+ if (!schemeHandler || (schemeHandler && !schemeHandler.skipNormalize)) {
466
487
  if (parsed.path !== undefined && parsed.path.length) {
467
488
  parsed.path = normalizePathEncoding(parsed.path)
468
489
  }
package/lib/utils.js CHANGED
@@ -5,6 +5,9 @@ const { HEX } = require('./scopedChars')
5
5
  /** @type {(value: string) => boolean} */
6
6
  const isIPv4 = RegExp.prototype.test.bind(/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u)
7
7
 
8
+ /** @type {(value: string) => boolean} */
9
+ const isPort = RegExp.prototype.test.bind(/^\d*$/u)
10
+
8
11
  /**
9
12
  * @param {string[]} input
10
13
  * @param {boolean} [keepZero=false]
@@ -630,8 +633,12 @@ function recomposeAuthority (components, options) {
630
633
  }
631
634
 
632
635
  if (typeof components.port === 'number' || typeof components.port === 'string') {
636
+ const port = String(components.port)
637
+ if (!isPort(port)) {
638
+ throw new TypeError('URI port is malformed.')
639
+ }
633
640
  uriTokens.push(':')
634
- uriTokens.push(String(components.port))
641
+ uriTokens.push(port)
635
642
  }
636
643
 
637
644
  return uriTokens.length ? uriTokens.join('') : undefined
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "fast-uri",
3
3
  "description": "Dependency free RFC 3986 URI toolbox",
4
- "version": "2.4.5",
4
+ "version": "2.4.7",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
7
7
  "types": "types/index.d.ts",
@@ -24,6 +24,64 @@ test('userinfo serialization cannot terminate the authority', (t) => {
24
24
  t.end()
25
25
  })
26
26
 
27
+ test('port serialization rejects non-digit values', (t) => {
28
+ const malformedPorts = [
29
+ '@127.0.0.1:8124',
30
+ '8080@evil.example',
31
+ '8080/path',
32
+ '8080?query',
33
+ '8080#fragment',
34
+ '8080:9000',
35
+ '-1',
36
+ '1.5',
37
+ 1.5,
38
+ NaN,
39
+ Infinity,
40
+ '\u0661'
41
+ ]
42
+
43
+ for (const port of malformedPorts) {
44
+ t.throws(
45
+ () => fastURI.serialize({ scheme: 'http', host: 'trusted.example', port, path: '/app' }),
46
+ /URI port is malformed\./,
47
+ String(port)
48
+ )
49
+ }
50
+
51
+ t.throws(
52
+ () => fastURI.normalize({ scheme: 'http', host: 'trusted.example', port: '@evil.example' }),
53
+ /URI port is malformed\./,
54
+ 'object normalization rejects a malformed port'
55
+ )
56
+ t.equal(
57
+ fastURI.equal(
58
+ { scheme: 'http', host: 'trusted.example', port: '@evil.example' },
59
+ { scheme: 'http', host: 'trusted.example', port: '@evil.example' }
60
+ ),
61
+ false,
62
+ 'object equality fails closed for a malformed port'
63
+ )
64
+ t.end()
65
+ })
66
+
67
+ test('port serialization preserves RFC 3986 digit values', (t) => {
68
+ const validPorts = [
69
+ [8080, '8080'],
70
+ ['8080', '8080'],
71
+ ['00080', '00080'],
72
+ ['', '']
73
+ ]
74
+
75
+ for (const [port, expected] of validPorts) {
76
+ t.equal(
77
+ fastURI.serialize({ scheme: 'uri', host: 'example.test', port }),
78
+ `uri://example.test:${expected}`,
79
+ JSON.stringify(port)
80
+ )
81
+ }
82
+ t.end()
83
+ })
84
+
27
85
  test('query and fragment serialization cannot inject component delimiters', (t) => {
28
86
  const uri = fastURI.serialize({
29
87
  scheme: 'x',
@@ -0,0 +1,40 @@
1
+ 'use strict'
2
+
3
+ const test = require('tape')
4
+ const fastURI = require('..')
5
+
6
+ const HOST_ERROR = 'URI host is malformed.'
7
+
8
+ test('hosts with unbalanced or misplaced IP-literal brackets are rejected', (t) => {
9
+ const malformed = [
10
+ 'http://[fe80',
11
+ 'http://[',
12
+ 'http://[not-an-ip',
13
+ 'http://[\u65e5\u672c',
14
+ 'http://user@[@127.0.0.1:8123/admin',
15
+ 'http://user@]127.0.0.1:8123/admin',
16
+ 'http://user@prefix[@127.0.0.1:8123/admin',
17
+ 'http://user@prefix]@127.0.0.1:8123/admin'
18
+ ]
19
+ const modes = [
20
+ ['default', undefined],
21
+ ['Unicode', { unicodeSupport: true }]
22
+ ]
23
+
24
+ for (const [mode, options] of modes) {
25
+ for (const uri of malformed) {
26
+ const parsed = fastURI.parse(uri, options)
27
+ const message = `${mode} mode rejects ${uri}`
28
+
29
+ t.equal(parsed.error, HOST_ERROR, `parse ${message}`)
30
+ t.equal(fastURI.normalize(uri, options), uri, `normalize preserves ${uri} in ${mode} mode`)
31
+ t.equal(fastURI.equal(uri, uri, options), false, `equal ${message}`)
32
+ t.throws(
33
+ () => fastURI.resolve('http://example.com/', uri, options),
34
+ /URI host is malformed\./,
35
+ `resolve ${message}`
36
+ )
37
+ }
38
+ }
39
+ t.end()
40
+ })
@@ -126,6 +126,44 @@ test('hostname normalization decodes only current unreserved escapes', (t) => {
126
126
  t.end()
127
127
  })
128
128
 
129
+ test('scheme-relative hostname normalization folds decoded ASCII case', (t) => {
130
+ const encodedHost = '//%41.com'
131
+ const normalizedHost = fastURI.normalize(encodedHost)
132
+ const encodedMetadata = '//%4Detadata.internal/private'
133
+ const literalMetadata = '//metadata.internal/private'
134
+
135
+ t.equal(fastURI.parse(encodedHost).host, 'a.com', 'parse folds an encoded uppercase host letter')
136
+ t.equal(normalizedHost, '//a.com', 'normalize emits a lowercase host')
137
+ t.equal(fastURI.normalize(normalizedHost), normalizedHost, 'host normalization is idempotent')
138
+ t.equal(fastURI.equal(encodedHost, '//a.com'), true, 'encoded and literal host spellings compare equal')
139
+ t.equal(fastURI.equal(encodedMetadata, literalMetadata), true, 'encoded metadata host compares equal')
140
+ t.equal(
141
+ fastURI.resolve('x://trusted.example/', encodedMetadata),
142
+ fastURI.resolve('x://trusted.example/', literalMetadata),
143
+ 'encoded and literal metadata hosts resolve identically'
144
+ )
145
+ t.equal(
146
+ fastURI.normalize('//example.com%2fpath'),
147
+ '//example.com%2Fpath',
148
+ 'reserved host escapes remain encoded with uppercase hex'
149
+ )
150
+ t.equal(fastURI.normalize('//%2541.com'), '//%2541.com', 'nested host escapes are not decoded twice')
151
+ t.equal(fastURI.equal('//User@%41.com/path', '//user@a.com/path'), false, 'userinfo remains case-sensitive')
152
+ t.equal(fastURI.equal('//%41.com/Path', '//a.com/path'), false, 'path remains case-sensitive')
153
+ t.equal(fastURI.equal('//%41.com/?Token=Value', '//a.com/?token=value'), false, 'query remains case-sensitive')
154
+ t.end()
155
+ })
156
+
157
+ test('host normalization applies to schemes that skip path normalization', (t) => {
158
+ const encodedHost = 'mailto://%41.com'
159
+ const literalHost = 'mailto://a.com'
160
+
161
+ t.equal(fastURI.parse(encodedHost).host, fastURI.parse(literalHost).host, 'parse results have consistent hosts')
162
+ t.equal(fastURI.normalize(encodedHost), fastURI.normalize(literalHost), 'normalize results are consistent')
163
+ t.equal(fastURI.equal(encodedHost, literalHost), true, 'encoded and literal hosts compare equal')
164
+ t.end()
165
+ })
166
+
129
167
  test('host conversion failures are not treated as comparable URLs', (t) => {
130
168
  const malformedHost = 'http://trusted.test%2540evil.test/'
131
169