fast-uri 2.3.1 → 2.4.1
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/README.md +13 -0
- package/index.js +65 -24
- package/lib/utils.js +137 -24
- package/package.json +1 -1
- package/test/compatibility.test.js +1 -0
- package/test/security-normalization.test.js +40 -0
- package/test/security.test.js +134 -0
- package/test/uri-js.test.js +940 -0
- package/test/util.test.js +24 -0
package/README.md
CHANGED
|
@@ -15,6 +15,8 @@ Dependency free RFC 3986 URI toolbox.
|
|
|
15
15
|
|
|
16
16
|
All of the above functions can accept an additional options argument that is an object that can contain one or more of the following properties:
|
|
17
17
|
|
|
18
|
+
Malformed authorities and out-of-range ports are reported through the parsed component's `error` field. `normalize()` leaves malformed string inputs unchanged, and `equal()` returns `false` when either string input is malformed.
|
|
19
|
+
|
|
18
20
|
* `scheme` (string)
|
|
19
21
|
Indicates the scheme that the URI should be treated as, overriding the URI's normal scheme parsing behavior.
|
|
20
22
|
|
|
@@ -69,6 +71,17 @@ uri.resolve("uri://a/b/c/d?q", "../../g")
|
|
|
69
71
|
"uri://a/g"
|
|
70
72
|
```
|
|
71
73
|
|
|
74
|
+
### Normalize
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
const uri = require('fast-uri')
|
|
78
|
+
uri.normalize('http://example.com/a%2Fb')
|
|
79
|
+
// Output
|
|
80
|
+
"http://example.com/a%2Fb"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Reserved path escapes such as `%2F` and `%2E` are preserved as path data during normalization and comparison.
|
|
84
|
+
|
|
72
85
|
### Equal
|
|
73
86
|
|
|
74
87
|
```js
|
package/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { normalizeIPv6, normalizeIPv4, removeDotSegments, recomposeAuthority,
|
|
3
|
+
const { normalizeIPv6, normalizeIPv4, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, escapePreservingEscapes, reescapeHostDelimiters } = require('./lib/utils')
|
|
4
4
|
const SCHEMES = require('./lib/schemes')
|
|
5
5
|
|
|
6
6
|
function normalize (uri, options) {
|
|
7
7
|
if (typeof uri === 'string') {
|
|
8
|
-
uri =
|
|
8
|
+
uri = normalizeString(uri, options)
|
|
9
9
|
} else if (typeof uri === 'object') {
|
|
10
10
|
uri = parse(serialize(uri, options), options)
|
|
11
11
|
}
|
|
@@ -79,21 +79,10 @@ function resolveComponents (base, relative, options, skipNormalization) {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
function equal (uriA, uriB, options) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
uriA = serialize(normalizeComponentEncoding(parse(uriA, options), true), { ...options, skipEscape: true })
|
|
85
|
-
} else if (typeof uriA === 'object') {
|
|
86
|
-
uriA = serialize(normalizeComponentEncoding(uriA, true), { ...options, skipEscape: true })
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (typeof uriB === 'string') {
|
|
90
|
-
uriB = unescape(uriB)
|
|
91
|
-
uriB = serialize(normalizeComponentEncoding(parse(uriB, options), true), { ...options, skipEscape: true })
|
|
92
|
-
} else if (typeof uriB === 'object') {
|
|
93
|
-
uriB = serialize(normalizeComponentEncoding(uriB, true), { ...options, skipEscape: true })
|
|
94
|
-
}
|
|
82
|
+
const normalizedA = normalizeComparableURI(uriA, options)
|
|
83
|
+
const normalizedB = normalizeComparableURI(uriB, options)
|
|
95
84
|
|
|
96
|
-
return
|
|
85
|
+
return normalizedA !== undefined && normalizedB !== undefined && normalizedA.toLowerCase() === normalizedB.toLowerCase()
|
|
97
86
|
}
|
|
98
87
|
|
|
99
88
|
function serialize (cmpts, opts) {
|
|
@@ -124,13 +113,13 @@ function serialize (cmpts, opts) {
|
|
|
124
113
|
|
|
125
114
|
if (components.path !== undefined) {
|
|
126
115
|
if (!options.skipEscape) {
|
|
127
|
-
components.path =
|
|
116
|
+
components.path = escapePreservingEscapes(components.path)
|
|
128
117
|
|
|
129
118
|
if (components.scheme !== undefined) {
|
|
130
119
|
components.path = components.path.split('%3A').join(':')
|
|
131
120
|
}
|
|
132
121
|
} else {
|
|
133
|
-
components.path =
|
|
122
|
+
components.path = normalizePercentEncoding(components.path)
|
|
134
123
|
}
|
|
135
124
|
}
|
|
136
125
|
|
|
@@ -192,7 +181,19 @@ function nonSimpleDomain (value) {
|
|
|
192
181
|
|
|
193
182
|
const URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u
|
|
194
183
|
|
|
195
|
-
function
|
|
184
|
+
function getParseError (parsed, matches) {
|
|
185
|
+
if (matches[2] !== undefined && parsed.path && parsed.path[0] !== '/') {
|
|
186
|
+
return 'URI path must start with "/" when authority is present.'
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (typeof parsed.port === 'number' && (parsed.port < 0 || parsed.port > 65535)) {
|
|
190
|
+
return 'URI port is malformed.'
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return undefined
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function parseWithStatus (uri, opts) {
|
|
196
197
|
const options = Object.assign({}, opts)
|
|
197
198
|
const parsed = {
|
|
198
199
|
scheme: undefined,
|
|
@@ -204,6 +205,8 @@ function parse (uri, opts) {
|
|
|
204
205
|
fragment: undefined
|
|
205
206
|
}
|
|
206
207
|
const gotEncoding = uri.indexOf('%') !== -1
|
|
208
|
+
let malformedAuthorityOrPort = false
|
|
209
|
+
let isIP = false
|
|
207
210
|
if (options.reference === 'suffix') uri = (options.scheme ? options.scheme + ':' : '') + '//' + uri
|
|
208
211
|
|
|
209
212
|
const matches = uri.match(URI_PARSE)
|
|
@@ -222,12 +225,22 @@ function parse (uri, opts) {
|
|
|
222
225
|
if (isNaN(parsed.port)) {
|
|
223
226
|
parsed.port = matches[5]
|
|
224
227
|
}
|
|
228
|
+
|
|
229
|
+
const parseError = getParseError(parsed, matches)
|
|
230
|
+
if (parseError !== undefined) {
|
|
231
|
+
parsed.error = parsed.error || parseError
|
|
232
|
+
malformedAuthorityOrPort = true
|
|
233
|
+
}
|
|
234
|
+
|
|
225
235
|
if (parsed.host) {
|
|
226
236
|
const ipv4result = normalizeIPv4(parsed.host)
|
|
227
237
|
if (ipv4result.isIPV4 === false) {
|
|
228
|
-
|
|
238
|
+
const ipv6result = normalizeIPv6(ipv4result.host, { isIPV4: false })
|
|
239
|
+
parsed.host = ipv6result.host.toLowerCase()
|
|
240
|
+
isIP = ipv6result.isIPV6
|
|
229
241
|
} else {
|
|
230
242
|
parsed.host = ipv4result.host
|
|
243
|
+
isIP = true
|
|
231
244
|
}
|
|
232
245
|
}
|
|
233
246
|
if (parsed.scheme === undefined && parsed.userinfo === undefined && parsed.host === undefined && parsed.port === undefined && !parsed.path && parsed.query === undefined) {
|
|
@@ -251,7 +264,7 @@ function parse (uri, opts) {
|
|
|
251
264
|
// check if scheme can't handle IRIs
|
|
252
265
|
if (!options.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport)) {
|
|
253
266
|
// if host component is a domain name
|
|
254
|
-
if (parsed.host && (options.domainHost || (schemeHandler && schemeHandler.domainHost)) && nonSimpleDomain(parsed.host)) {
|
|
267
|
+
if (parsed.host && (options.domainHost || (schemeHandler && schemeHandler.domainHost)) && isIP === false && nonSimpleDomain(parsed.host)) {
|
|
255
268
|
// convert Unicode IDN -> ASCII IDN
|
|
256
269
|
try {
|
|
257
270
|
parsed.host = URL.domainToASCII(parsed.host.toLowerCase())
|
|
@@ -270,10 +283,10 @@ function parse (uri, opts) {
|
|
|
270
283
|
parsed.userinfo = unescape(parsed.userinfo)
|
|
271
284
|
}
|
|
272
285
|
if (gotEncoding && parsed.host !== undefined) {
|
|
273
|
-
parsed.host = unescape(parsed.host)
|
|
286
|
+
parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP)
|
|
274
287
|
}
|
|
275
288
|
if (parsed.path !== undefined && parsed.path.length) {
|
|
276
|
-
parsed.path =
|
|
289
|
+
parsed.path = normalizePathEncoding(parsed.path)
|
|
277
290
|
}
|
|
278
291
|
if (parsed.fragment !== undefined && parsed.fragment.length) {
|
|
279
292
|
parsed.fragment = encodeURI(decodeURI(parsed.fragment))
|
|
@@ -287,10 +300,38 @@ function parse (uri, opts) {
|
|
|
287
300
|
} else {
|
|
288
301
|
parsed.error = parsed.error || 'URI can not be parsed.'
|
|
289
302
|
}
|
|
290
|
-
return parsed
|
|
303
|
+
return { parsed, malformedAuthorityOrPort }
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function parse (uri, opts) {
|
|
307
|
+
return parseWithStatus(uri, opts).parsed
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function normalizeString (uri, opts) {
|
|
311
|
+
return normalizeStringWithStatus(uri, opts).normalized
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function normalizeStringWithStatus (uri, opts) {
|
|
315
|
+
const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri, opts)
|
|
316
|
+
return {
|
|
317
|
+
normalized: malformedAuthorityOrPort ? uri : serialize(parsed, opts),
|
|
318
|
+
malformedAuthorityOrPort
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function normalizeComparableURI (uri, opts) {
|
|
323
|
+
if (typeof uri === 'string') {
|
|
324
|
+
const { normalized, malformedAuthorityOrPort } = normalizeStringWithStatus(uri, opts)
|
|
325
|
+
return malformedAuthorityOrPort ? undefined : normalized
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (typeof uri === 'object') {
|
|
329
|
+
return serialize(uri, opts)
|
|
330
|
+
}
|
|
291
331
|
}
|
|
292
332
|
|
|
293
333
|
const fastUri = {
|
|
334
|
+
SCHEMES,
|
|
294
335
|
normalize,
|
|
295
336
|
resolve,
|
|
296
337
|
resolveComponents,
|
package/lib/utils.js
CHANGED
|
@@ -13,14 +13,20 @@ function normalizeIPv4 (host) {
|
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* @param {string[]} input
|
|
18
|
+
* @param {boolean} [keepZero=false]
|
|
19
|
+
* @returns {string|undefined}
|
|
20
|
+
*/
|
|
21
|
+
function stringArrayToHexStripped (input, keepZero = false) {
|
|
17
22
|
let acc = ''
|
|
18
23
|
let strip = true
|
|
19
24
|
for (const c of input) {
|
|
20
|
-
if (c !== '0' && strip === true) strip = false
|
|
21
25
|
if (HEX[c] === undefined) return undefined
|
|
26
|
+
if (c !== '0' && strip === true) strip = false
|
|
22
27
|
if (!strip) acc += c
|
|
23
28
|
}
|
|
29
|
+
if (keepZero && acc.length === 0) acc = '0'
|
|
24
30
|
return acc
|
|
25
31
|
}
|
|
26
32
|
|
|
@@ -36,7 +42,7 @@ function getIPV6 (input) {
|
|
|
36
42
|
function consume () {
|
|
37
43
|
if (buffer.length) {
|
|
38
44
|
if (isZone === false) {
|
|
39
|
-
const hex =
|
|
45
|
+
const hex = stringArrayToHexStripped(buffer)
|
|
40
46
|
if (hex !== undefined) {
|
|
41
47
|
address.push(hex)
|
|
42
48
|
} else {
|
|
@@ -83,7 +89,7 @@ function getIPV6 (input) {
|
|
|
83
89
|
} else if (endIpv6) {
|
|
84
90
|
address.push(buffer.join(''))
|
|
85
91
|
} else {
|
|
86
|
-
address.push(
|
|
92
|
+
address.push(stringArrayToHexStripped(buffer))
|
|
87
93
|
}
|
|
88
94
|
}
|
|
89
95
|
output.address = address.join('')
|
|
@@ -170,27 +176,131 @@ function removeDotSegments (input) {
|
|
|
170
176
|
return output.join('')
|
|
171
177
|
}
|
|
172
178
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
179
|
+
/**
|
|
180
|
+
* Re-escape RFC 3986 gen-delims that must not appear literally in the host.
|
|
181
|
+
* After the URI regex parses, these characters cannot be literal in the host
|
|
182
|
+
* field, so any that appear after decoding came from percent-encoding and
|
|
183
|
+
* must be restored to prevent authority structure changes.
|
|
184
|
+
*
|
|
185
|
+
* @param {string} host
|
|
186
|
+
* @param {boolean} isIP - true for IPv4/IPv6 hosts (skip colon re-escaping)
|
|
187
|
+
* @returns {string}
|
|
188
|
+
*/
|
|
189
|
+
const HOST_DELIMS = { '@': '%40', '/': '%2F', '?': '%3F', '#': '%23', ':': '%3A' }
|
|
190
|
+
const HOST_DELIM_RE = /[@/?#:]/g
|
|
191
|
+
const HOST_DELIM_NO_COLON_RE = /[@/?#]/g
|
|
192
|
+
|
|
193
|
+
function reescapeHostDelimiters (host, isIP) {
|
|
194
|
+
const re = isIP ? HOST_DELIM_NO_COLON_RE : HOST_DELIM_RE
|
|
195
|
+
re.lastIndex = 0
|
|
196
|
+
return host.replace(re, (ch) => HOST_DELIMS[ch])
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu)
|
|
200
|
+
const isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu)
|
|
201
|
+
const isPathCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/]$/iu)
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Normalizes percent escapes and optionally decodes only unreserved ASCII bytes.
|
|
205
|
+
* Reserved delimiters such as `%2F` and `%2E` stay escaped.
|
|
206
|
+
*
|
|
207
|
+
* @param {string} input
|
|
208
|
+
* @param {boolean} [decodeUnreserved=false]
|
|
209
|
+
* @returns {string}
|
|
210
|
+
*/
|
|
211
|
+
function normalizePercentEncoding (input, decodeUnreserved = false) {
|
|
212
|
+
if (input.indexOf('%') === -1) {
|
|
213
|
+
return input
|
|
183
214
|
}
|
|
184
|
-
|
|
185
|
-
|
|
215
|
+
|
|
216
|
+
let output = ''
|
|
217
|
+
|
|
218
|
+
for (let i = 0; i < input.length; i++) {
|
|
219
|
+
if (input[i] === '%' && i + 2 < input.length) {
|
|
220
|
+
const hex = input.slice(i + 1, i + 3)
|
|
221
|
+
if (isHexPair(hex)) {
|
|
222
|
+
const normalizedHex = hex.toUpperCase()
|
|
223
|
+
const decoded = String.fromCharCode(parseInt(normalizedHex, 16))
|
|
224
|
+
|
|
225
|
+
if (decodeUnreserved && isUnreserved(decoded)) {
|
|
226
|
+
output += decoded
|
|
227
|
+
} else {
|
|
228
|
+
output += '%' + normalizedHex
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
i += 2
|
|
232
|
+
continue
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
output += input[i]
|
|
186
237
|
}
|
|
187
|
-
|
|
188
|
-
|
|
238
|
+
|
|
239
|
+
return output
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Normalizes path data without turning reserved escapes into live path syntax.
|
|
244
|
+
* Valid escapes are uppercased, raw unsafe characters are escaped, and only
|
|
245
|
+
* unreserved bytes that are not `.` are decoded.
|
|
246
|
+
*
|
|
247
|
+
* @param {string} input
|
|
248
|
+
* @returns {string}
|
|
249
|
+
*/
|
|
250
|
+
function normalizePathEncoding (input) {
|
|
251
|
+
let output = ''
|
|
252
|
+
|
|
253
|
+
for (let i = 0; i < input.length; i++) {
|
|
254
|
+
if (input[i] === '%' && i + 2 < input.length) {
|
|
255
|
+
const hex = input.slice(i + 1, i + 3)
|
|
256
|
+
if (isHexPair(hex)) {
|
|
257
|
+
const normalizedHex = hex.toUpperCase()
|
|
258
|
+
const decoded = String.fromCharCode(parseInt(normalizedHex, 16))
|
|
259
|
+
|
|
260
|
+
if (decoded !== '.' && isUnreserved(decoded)) {
|
|
261
|
+
output += decoded
|
|
262
|
+
} else {
|
|
263
|
+
output += '%' + normalizedHex
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
i += 2
|
|
267
|
+
continue
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (isPathCharacter(input[i])) {
|
|
272
|
+
output += input[i]
|
|
273
|
+
} else {
|
|
274
|
+
output += escape(input[i])
|
|
275
|
+
}
|
|
189
276
|
}
|
|
190
|
-
|
|
191
|
-
|
|
277
|
+
|
|
278
|
+
return output
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Escapes a component while preserving existing valid percent escapes.
|
|
283
|
+
*
|
|
284
|
+
* @param {string} input
|
|
285
|
+
* @returns {string}
|
|
286
|
+
*/
|
|
287
|
+
function escapePreservingEscapes (input) {
|
|
288
|
+
let output = ''
|
|
289
|
+
|
|
290
|
+
for (let i = 0; i < input.length; i++) {
|
|
291
|
+
if (input[i] === '%' && i + 2 < input.length) {
|
|
292
|
+
const hex = input.slice(i + 1, i + 3)
|
|
293
|
+
if (isHexPair(hex)) {
|
|
294
|
+
output += '%' + hex.toUpperCase()
|
|
295
|
+
i += 2
|
|
296
|
+
continue
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
output += escape(input[i])
|
|
192
301
|
}
|
|
193
|
-
|
|
302
|
+
|
|
303
|
+
return output
|
|
194
304
|
}
|
|
195
305
|
|
|
196
306
|
function recomposeAuthority (components, options) {
|
|
@@ -212,7 +322,7 @@ function recomposeAuthority (components, options) {
|
|
|
212
322
|
if (ipV6res.isIPV6 === true) {
|
|
213
323
|
host = `[${ipV6res.escapedHost}]`
|
|
214
324
|
} else {
|
|
215
|
-
host =
|
|
325
|
+
host = reescapeHostDelimiters(host, false)
|
|
216
326
|
}
|
|
217
327
|
}
|
|
218
328
|
uriTokens.push(host)
|
|
@@ -228,9 +338,12 @@ function recomposeAuthority (components, options) {
|
|
|
228
338
|
|
|
229
339
|
module.exports = {
|
|
230
340
|
recomposeAuthority,
|
|
231
|
-
|
|
341
|
+
reescapeHostDelimiters,
|
|
342
|
+
normalizePercentEncoding,
|
|
343
|
+
normalizePathEncoding,
|
|
344
|
+
escapePreservingEscapes,
|
|
232
345
|
removeDotSegments,
|
|
233
346
|
normalizeIPv4,
|
|
234
347
|
normalizeIPv6,
|
|
235
|
-
|
|
348
|
+
stringArrayToHexStripped
|
|
236
349
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const tap = require('tap')
|
|
4
|
+
const test = tap.test
|
|
5
|
+
const fastURI = require('..')
|
|
6
|
+
|
|
7
|
+
test('parse preserves reserved path escapes as data', (t) => {
|
|
8
|
+
const components = fastURI.parse('http://example.com/a%2Fb/public/%2e%2e/admin')
|
|
9
|
+
|
|
10
|
+
t.equal(components.path, '/a%2Fb/public/%2E%2E/admin')
|
|
11
|
+
t.end()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('normalize preserves percent-encoded path separators and dot segments', (t) => {
|
|
15
|
+
t.equal(
|
|
16
|
+
fastURI.normalize('http://example.com/public/%2e%2e/admin'),
|
|
17
|
+
'http://example.com/public/%2E%2E/admin'
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
t.equal(
|
|
21
|
+
fastURI.normalize('http://example.com/a%2Fb'),
|
|
22
|
+
'http://example.com/a%2Fb'
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
t.end()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('equal does not treat reserved path escapes as live path syntax', (t) => {
|
|
29
|
+
t.equal(
|
|
30
|
+
fastURI.equal('http://example.com/public/%2e%2e/admin', 'http://example.com/admin', {}),
|
|
31
|
+
false
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
t.equal(
|
|
35
|
+
fastURI.equal('http://example.com/a%2Fb', 'http://example.com/a/b', {}),
|
|
36
|
+
false
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
t.end()
|
|
40
|
+
})
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const tap = require('tap')
|
|
4
|
+
const test = tap.test
|
|
5
|
+
const fastURI = require('..')
|
|
6
|
+
|
|
7
|
+
test('parse marks malformed authority and port inputs as errors', (t) => {
|
|
8
|
+
const malformedCases = [
|
|
9
|
+
{
|
|
10
|
+
input: 'http://[::1]foo',
|
|
11
|
+
expectedError: 'URI path must start with "/" when authority is present.'
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
input: 'http://[::1]:80abc/path',
|
|
15
|
+
expectedError: 'URI path must start with "/" when authority is present.'
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
input: 'http://example.com:80abc/path',
|
|
19
|
+
expectedError: 'URI path must start with "/" when authority is present.'
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
input: 'http://[::1]:65536',
|
|
23
|
+
expectedError: 'URI port is malformed.'
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
t.plan(malformedCases.length)
|
|
28
|
+
|
|
29
|
+
malformedCases.forEach(({ input, expectedError }) => {
|
|
30
|
+
t.equal(fastURI.parse(input).error, expectedError, input)
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('normalize does not canonicalize malformed URLs into different valid URLs', (t) => {
|
|
35
|
+
const malformedCases = [
|
|
36
|
+
'http://[::1]foo',
|
|
37
|
+
'http://[::1]:80abc/path',
|
|
38
|
+
'http://example.com:80abc/path',
|
|
39
|
+
'http://[::1]:65536'
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
t.plan(malformedCases.length)
|
|
43
|
+
|
|
44
|
+
malformedCases.forEach((input) => {
|
|
45
|
+
t.equal(fastURI.normalize(input), input, input)
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('equal returns false when either side is malformed', (t) => {
|
|
50
|
+
const malformedPairs = [
|
|
51
|
+
['http://[::1]foo', 'http://[::1]/foo'],
|
|
52
|
+
['http://[::1]:80abc/path', 'http://[::1]/abc/path'],
|
|
53
|
+
['http://example.com:80abc/path', 'http://example.com/abc/path'],
|
|
54
|
+
['http://[::1]:65536', 'http://[::1]:65536/']
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
t.plan(malformedPairs.length)
|
|
58
|
+
|
|
59
|
+
malformedPairs.forEach(([left, right]) => {
|
|
60
|
+
t.equal(fastURI.equal(left, right), false, `${left} != ${right}`)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('normalize preserves encoded authority delimiters in host', (t) => {
|
|
65
|
+
const cases = [
|
|
66
|
+
['http://trusted.com%40evil.com/', 'http://trusted.com%40evil.com/'],
|
|
67
|
+
['http://example.com%3A8080/', 'http://example.com%3A8080/'],
|
|
68
|
+
['http://example.com%2Fevil.com/path', 'http://example.com%2Fevil.com/path'],
|
|
69
|
+
['http://example.com%23fragment/path', 'http://example.com%23fragment/path'],
|
|
70
|
+
['http://example.com%3Fq=evil/path', 'http://example.com%3Fq=evil/path'],
|
|
71
|
+
['http://user%3Apass%40evil.com/', 'http://user%3Apass%40evil.com/'],
|
|
72
|
+
['http://user@trusted.com%40evil.com/', 'http://user@trusted.com%40evil.com/'],
|
|
73
|
+
['https://trusted.com%40evil.com/', 'https://trusted.com%40evil.com/'],
|
|
74
|
+
['ws://trusted.com%40evil.com/chat', 'ws://trusted.com%40evil.com/chat'],
|
|
75
|
+
['wss://trusted.com%40evil.com/chat', 'wss://trusted.com%40evil.com/chat']
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
t.plan(cases.length)
|
|
79
|
+
|
|
80
|
+
cases.forEach(([input, expected]) => {
|
|
81
|
+
t.equal(fastURI.normalize(input), expected, input)
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('parse preserves encoded authority delimiters in host', (t) => {
|
|
86
|
+
const cases = [
|
|
87
|
+
['http://trusted.com%40evil.com/', 'trusted.com%40evil.com'],
|
|
88
|
+
['http://example.com%3A8080/', 'example.com%3A8080'],
|
|
89
|
+
['http://user%3Apass%40evil.com/', 'user%3Apass%40evil.com']
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
t.plan(cases.length)
|
|
93
|
+
|
|
94
|
+
cases.forEach(([input, expectedHost]) => {
|
|
95
|
+
t.equal(fastURI.parse(input).host, expectedHost, input)
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('equal returns false when encoded delimiters differ from live delimiters', (t) => {
|
|
100
|
+
const pairs = [
|
|
101
|
+
['http://trusted.com%40evil.com/', 'http://trusted.com@evil.com/'],
|
|
102
|
+
['http://example.com%3A8080/', 'http://example.com:8080/']
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
t.plan(pairs.length)
|
|
106
|
+
|
|
107
|
+
pairs.forEach(([left, right]) => {
|
|
108
|
+
t.equal(fastURI.equal(left, right, {}), false, `${left} != ${right}`)
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('resolve preserves encoded authority delimiters', (t) => {
|
|
113
|
+
const result = fastURI.resolve('http://base.com/', '//trusted.com%40evil.com/path')
|
|
114
|
+
const parsed = fastURI.parse(result)
|
|
115
|
+
|
|
116
|
+
t.plan(1)
|
|
117
|
+
t.not(parsed.host, 'evil.com', '//trusted.com%40evil.com/path')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test('serialize escapes authority delimiters in host field', (t) => {
|
|
121
|
+
const result = fastURI.serialize({ scheme: 'http', host: 'trusted.com@evil.com', path: '/' })
|
|
122
|
+
const parsed = fastURI.parse(result)
|
|
123
|
+
|
|
124
|
+
t.plan(1)
|
|
125
|
+
t.not(parsed.host, 'evil.com', 'host: trusted.com@evil.com')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test('normalize does not double-decode %2540 into a live @', (t) => {
|
|
129
|
+
const result = fastURI.normalize('http://trusted.com%2540evil.com/')
|
|
130
|
+
const parsed = fastURI.parse(result)
|
|
131
|
+
|
|
132
|
+
t.plan(1)
|
|
133
|
+
t.not(parsed.host, 'trusted.com@evil.com', 'http://trusted.com%2540evil.com/')
|
|
134
|
+
})
|