1 | 'use strict'
|
---|
2 |
|
---|
3 | const { HEX } = require('./scopedChars')
|
---|
4 |
|
---|
5 | const IPV4_REG = /^(?:(?: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
|
---|
6 |
|
---|
7 | function normalizeIPv4 (host) {
|
---|
8 | if (findToken(host, '.') < 3) { return { host, isIPV4: false } }
|
---|
9 | const matches = host.match(IPV4_REG) || []
|
---|
10 | const [address] = matches
|
---|
11 | if (address) {
|
---|
12 | return { host: stripLeadingZeros(address, '.'), isIPV4: true }
|
---|
13 | } else {
|
---|
14 | return { host, isIPV4: false }
|
---|
15 | }
|
---|
16 | }
|
---|
17 |
|
---|
18 | /**
|
---|
19 | * @param {string[]} input
|
---|
20 | * @param {boolean} [keepZero=false]
|
---|
21 | * @returns {string|undefined}
|
---|
22 | */
|
---|
23 | function stringArrayToHexStripped (input, keepZero = false) {
|
---|
24 | let acc = ''
|
---|
25 | let strip = true
|
---|
26 | for (const c of input) {
|
---|
27 | if (HEX[c] === undefined) return undefined
|
---|
28 | if (c !== '0' && strip === true) strip = false
|
---|
29 | if (!strip) acc += c
|
---|
30 | }
|
---|
31 | if (keepZero && acc.length === 0) acc = '0'
|
---|
32 | return acc
|
---|
33 | }
|
---|
34 |
|
---|
35 | function getIPV6 (input) {
|
---|
36 | let tokenCount = 0
|
---|
37 | const output = { error: false, address: '', zone: '' }
|
---|
38 | const address = []
|
---|
39 | const buffer = []
|
---|
40 | let isZone = false
|
---|
41 | let endipv6Encountered = false
|
---|
42 | let endIpv6 = false
|
---|
43 |
|
---|
44 | function consume () {
|
---|
45 | if (buffer.length) {
|
---|
46 | if (isZone === false) {
|
---|
47 | const hex = stringArrayToHexStripped(buffer)
|
---|
48 | if (hex !== undefined) {
|
---|
49 | address.push(hex)
|
---|
50 | } else {
|
---|
51 | output.error = true
|
---|
52 | return false
|
---|
53 | }
|
---|
54 | }
|
---|
55 | buffer.length = 0
|
---|
56 | }
|
---|
57 | return true
|
---|
58 | }
|
---|
59 |
|
---|
60 | for (let i = 0; i < input.length; i++) {
|
---|
61 | const cursor = input[i]
|
---|
62 | if (cursor === '[' || cursor === ']') { continue }
|
---|
63 | if (cursor === ':') {
|
---|
64 | if (endipv6Encountered === true) {
|
---|
65 | endIpv6 = true
|
---|
66 | }
|
---|
67 | if (!consume()) { break }
|
---|
68 | tokenCount++
|
---|
69 | address.push(':')
|
---|
70 | if (tokenCount > 7) {
|
---|
71 | // not valid
|
---|
72 | output.error = true
|
---|
73 | break
|
---|
74 | }
|
---|
75 | if (i - 1 >= 0 && input[i - 1] === ':') {
|
---|
76 | endipv6Encountered = true
|
---|
77 | }
|
---|
78 | continue
|
---|
79 | } else if (cursor === '%') {
|
---|
80 | if (!consume()) { break }
|
---|
81 | // switch to zone detection
|
---|
82 | isZone = true
|
---|
83 | } else {
|
---|
84 | buffer.push(cursor)
|
---|
85 | continue
|
---|
86 | }
|
---|
87 | }
|
---|
88 | if (buffer.length) {
|
---|
89 | if (isZone) {
|
---|
90 | output.zone = buffer.join('')
|
---|
91 | } else if (endIpv6) {
|
---|
92 | address.push(buffer.join(''))
|
---|
93 | } else {
|
---|
94 | address.push(stringArrayToHexStripped(buffer))
|
---|
95 | }
|
---|
96 | }
|
---|
97 | output.address = address.join('')
|
---|
98 | return output
|
---|
99 | }
|
---|
100 |
|
---|
101 | function normalizeIPv6 (host) {
|
---|
102 | if (findToken(host, ':') < 2) { return { host, isIPV6: false } }
|
---|
103 | const ipv6 = getIPV6(host)
|
---|
104 |
|
---|
105 | if (!ipv6.error) {
|
---|
106 | let newHost = ipv6.address
|
---|
107 | let escapedHost = ipv6.address
|
---|
108 | if (ipv6.zone) {
|
---|
109 | newHost += '%' + ipv6.zone
|
---|
110 | escapedHost += '%25' + ipv6.zone
|
---|
111 | }
|
---|
112 | return { host: newHost, escapedHost, isIPV6: true }
|
---|
113 | } else {
|
---|
114 | return { host, isIPV6: false }
|
---|
115 | }
|
---|
116 | }
|
---|
117 |
|
---|
118 | function stripLeadingZeros (str, token) {
|
---|
119 | let out = ''
|
---|
120 | let skip = true
|
---|
121 | const l = str.length
|
---|
122 | for (let i = 0; i < l; i++) {
|
---|
123 | const c = str[i]
|
---|
124 | if (c === '0' && skip) {
|
---|
125 | if ((i + 1 <= l && str[i + 1] === token) || i + 1 === l) {
|
---|
126 | out += c
|
---|
127 | skip = false
|
---|
128 | }
|
---|
129 | } else {
|
---|
130 | if (c === token) {
|
---|
131 | skip = true
|
---|
132 | } else {
|
---|
133 | skip = false
|
---|
134 | }
|
---|
135 | out += c
|
---|
136 | }
|
---|
137 | }
|
---|
138 | return out
|
---|
139 | }
|
---|
140 |
|
---|
141 | function findToken (str, token) {
|
---|
142 | let ind = 0
|
---|
143 | for (let i = 0; i < str.length; i++) {
|
---|
144 | if (str[i] === token) ind++
|
---|
145 | }
|
---|
146 | return ind
|
---|
147 | }
|
---|
148 |
|
---|
149 | const RDS1 = /^\.\.?\//u
|
---|
150 | const RDS2 = /^\/\.(?:\/|$)/u
|
---|
151 | const RDS3 = /^\/\.\.(?:\/|$)/u
|
---|
152 | const RDS5 = /^\/?(?:.|\n)*?(?=\/|$)/u
|
---|
153 |
|
---|
154 | function removeDotSegments (input) {
|
---|
155 | const output = []
|
---|
156 |
|
---|
157 | while (input.length) {
|
---|
158 | if (input.match(RDS1)) {
|
---|
159 | input = input.replace(RDS1, '')
|
---|
160 | } else if (input.match(RDS2)) {
|
---|
161 | input = input.replace(RDS2, '/')
|
---|
162 | } else if (input.match(RDS3)) {
|
---|
163 | input = input.replace(RDS3, '/')
|
---|
164 | output.pop()
|
---|
165 | } else if (input === '.' || input === '..') {
|
---|
166 | input = ''
|
---|
167 | } else {
|
---|
168 | const im = input.match(RDS5)
|
---|
169 | if (im) {
|
---|
170 | const s = im[0]
|
---|
171 | input = input.slice(s.length)
|
---|
172 | output.push(s)
|
---|
173 | } else {
|
---|
174 | throw new Error('Unexpected dot segment condition')
|
---|
175 | }
|
---|
176 | }
|
---|
177 | }
|
---|
178 | return output.join('')
|
---|
179 | }
|
---|
180 |
|
---|
181 | function normalizeComponentEncoding (components, esc) {
|
---|
182 | const func = esc !== true ? escape : unescape
|
---|
183 | if (components.scheme !== undefined) {
|
---|
184 | components.scheme = func(components.scheme)
|
---|
185 | }
|
---|
186 | if (components.userinfo !== undefined) {
|
---|
187 | components.userinfo = func(components.userinfo)
|
---|
188 | }
|
---|
189 | if (components.host !== undefined) {
|
---|
190 | components.host = func(components.host)
|
---|
191 | }
|
---|
192 | if (components.path !== undefined) {
|
---|
193 | components.path = func(components.path)
|
---|
194 | }
|
---|
195 | if (components.query !== undefined) {
|
---|
196 | components.query = func(components.query)
|
---|
197 | }
|
---|
198 | if (components.fragment !== undefined) {
|
---|
199 | components.fragment = func(components.fragment)
|
---|
200 | }
|
---|
201 | return components
|
---|
202 | }
|
---|
203 |
|
---|
204 | function recomposeAuthority (components) {
|
---|
205 | const uriTokens = []
|
---|
206 |
|
---|
207 | if (components.userinfo !== undefined) {
|
---|
208 | uriTokens.push(components.userinfo)
|
---|
209 | uriTokens.push('@')
|
---|
210 | }
|
---|
211 |
|
---|
212 | if (components.host !== undefined) {
|
---|
213 | let host = unescape(components.host)
|
---|
214 | const ipV4res = normalizeIPv4(host)
|
---|
215 |
|
---|
216 | if (ipV4res.isIPV4) {
|
---|
217 | host = ipV4res.host
|
---|
218 | } else {
|
---|
219 | const ipV6res = normalizeIPv6(ipV4res.host)
|
---|
220 | if (ipV6res.isIPV6 === true) {
|
---|
221 | host = `[${ipV6res.escapedHost}]`
|
---|
222 | } else {
|
---|
223 | host = components.host
|
---|
224 | }
|
---|
225 | }
|
---|
226 | uriTokens.push(host)
|
---|
227 | }
|
---|
228 |
|
---|
229 | if (typeof components.port === 'number' || typeof components.port === 'string') {
|
---|
230 | uriTokens.push(':')
|
---|
231 | uriTokens.push(String(components.port))
|
---|
232 | }
|
---|
233 |
|
---|
234 | return uriTokens.length ? uriTokens.join('') : undefined
|
---|
235 | };
|
---|
236 |
|
---|
237 | module.exports = {
|
---|
238 | recomposeAuthority,
|
---|
239 | normalizeComponentEncoding,
|
---|
240 | removeDotSegments,
|
---|
241 | normalizeIPv4,
|
---|
242 | normalizeIPv6,
|
---|
243 | stringArrayToHexStripped
|
---|
244 | }
|
---|