freebuff 0.0.122 → 0.0.124
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/http.js +231 -10
- package/index.js +26 -981
- package/launcher.js +1211 -0
- package/package.json +4 -4
- package/postinstall.js +0 -33
package/http.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
const http = require('http')
|
|
2
2
|
const https = require('https')
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const { pipeline } = require('stream/promises')
|
|
3
5
|
const tls = require('tls')
|
|
4
6
|
|
|
5
7
|
function createReleaseHttpClient({
|
|
@@ -8,9 +10,20 @@ function createReleaseHttpClient({
|
|
|
8
10
|
requestTimeout,
|
|
9
11
|
httpModule = http,
|
|
10
12
|
httpsModule = https,
|
|
13
|
+
fsModule = fs,
|
|
14
|
+
pipelineFn = pipeline,
|
|
11
15
|
tlsModule = tls,
|
|
12
16
|
}) {
|
|
13
|
-
function getProxyUrl() {
|
|
17
|
+
function getProxyUrl(protocol = 'https:') {
|
|
18
|
+
if (protocol === 'http:') {
|
|
19
|
+
return (
|
|
20
|
+
env.HTTP_PROXY ||
|
|
21
|
+
env.http_proxy ||
|
|
22
|
+
env.HTTPS_PROXY ||
|
|
23
|
+
env.https_proxy ||
|
|
24
|
+
null
|
|
25
|
+
)
|
|
26
|
+
}
|
|
14
27
|
return (
|
|
15
28
|
env.HTTPS_PROXY ||
|
|
16
29
|
env.https_proxy ||
|
|
@@ -87,11 +100,15 @@ function createReleaseHttpClient({
|
|
|
87
100
|
})
|
|
88
101
|
}
|
|
89
102
|
|
|
90
|
-
async function
|
|
103
|
+
async function buildRequest(url, options = {}) {
|
|
91
104
|
const parsedUrl = new URL(url)
|
|
105
|
+
if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
|
|
106
|
+
throw new Error(`Unsupported URL protocol: ${parsedUrl.protocol}`)
|
|
107
|
+
}
|
|
108
|
+
const isHttps = parsedUrl.protocol === 'https:'
|
|
92
109
|
const reqOptions = {
|
|
93
110
|
hostname: parsedUrl.hostname,
|
|
94
|
-
port: parsedUrl.port || 443,
|
|
111
|
+
port: parsedUrl.port || (isHttps ? 443 : 80),
|
|
95
112
|
path: parsedUrl.pathname + parsedUrl.search,
|
|
96
113
|
headers: {
|
|
97
114
|
'User-Agent': userAgent,
|
|
@@ -99,9 +116,31 @@ function createReleaseHttpClient({
|
|
|
99
116
|
},
|
|
100
117
|
}
|
|
101
118
|
|
|
102
|
-
const proxyUrl = getProxyUrl()
|
|
119
|
+
const proxyUrl = getProxyUrl(parsedUrl.protocol)
|
|
103
120
|
if (!proxyUrl || shouldBypassProxy(parsedUrl.hostname)) {
|
|
104
|
-
return reqOptions
|
|
121
|
+
return { transport: isHttps ? httpsModule : httpModule, reqOptions }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const proxy = new URL(proxyUrl)
|
|
125
|
+
if (!['http:', 'https:'].includes(proxy.protocol)) {
|
|
126
|
+
throw new Error(`Unsupported proxy protocol: ${proxy.protocol}`)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!isHttps) {
|
|
130
|
+
reqOptions.hostname = proxy.hostname
|
|
131
|
+
reqOptions.port = proxy.port || (proxy.protocol === 'https:' ? 443 : 80)
|
|
132
|
+
reqOptions.path = parsedUrl.href
|
|
133
|
+
reqOptions.headers.Host = parsedUrl.host
|
|
134
|
+
if (proxy.username || proxy.password) {
|
|
135
|
+
const auth = Buffer.from(
|
|
136
|
+
`${decodeURIComponent(proxy.username || '')}:${decodeURIComponent(proxy.password || '')}`,
|
|
137
|
+
).toString('base64')
|
|
138
|
+
reqOptions.headers['Proxy-Authorization'] = `Basic ${auth}`
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
transport: proxy.protocol === 'https:' ? httpsModule : httpModule,
|
|
142
|
+
reqOptions,
|
|
143
|
+
}
|
|
105
144
|
}
|
|
106
145
|
|
|
107
146
|
const tunnelSocket = await connectThroughProxy(
|
|
@@ -138,15 +177,15 @@ function createReleaseHttpClient({
|
|
|
138
177
|
}
|
|
139
178
|
|
|
140
179
|
reqOptions.agent = new TunnelAgent({ keepAlive: false })
|
|
141
|
-
return reqOptions
|
|
180
|
+
return { transport: httpsModule, reqOptions }
|
|
142
181
|
}
|
|
143
182
|
|
|
144
183
|
async function httpGet(url, options = {}) {
|
|
145
184
|
const redirectCount = options.redirectCount || 0
|
|
146
|
-
const reqOptions = await
|
|
185
|
+
const { transport, reqOptions } = await buildRequest(url, options)
|
|
147
186
|
|
|
148
187
|
return new Promise((resolve, reject) => {
|
|
149
|
-
const req =
|
|
188
|
+
const req = transport.get(reqOptions, (res) => {
|
|
150
189
|
if ([301, 302, 303, 307, 308].includes(res.statusCode)) {
|
|
151
190
|
res.resume()
|
|
152
191
|
|
|
@@ -170,17 +209,198 @@ function createReleaseHttpClient({
|
|
|
170
209
|
return
|
|
171
210
|
}
|
|
172
211
|
|
|
212
|
+
res.requestUrl = url
|
|
173
213
|
resolve(res)
|
|
174
214
|
})
|
|
175
215
|
|
|
176
|
-
req.on('error',
|
|
216
|
+
req.on('error', (error) => {
|
|
217
|
+
error.requestUrl = url
|
|
218
|
+
reject(error)
|
|
219
|
+
})
|
|
177
220
|
req.setTimeout(options.timeout || requestTimeout, () => {
|
|
178
221
|
req.destroy()
|
|
179
|
-
|
|
222
|
+
const error = new Error('Request timeout.')
|
|
223
|
+
error.code = 'ETIMEDOUT'
|
|
224
|
+
error.requestUrl = url
|
|
225
|
+
reject(error)
|
|
180
226
|
})
|
|
181
227
|
})
|
|
182
228
|
}
|
|
183
229
|
|
|
230
|
+
function getFileSize(filePath) {
|
|
231
|
+
try {
|
|
232
|
+
return fsModule.statSync(filePath).size
|
|
233
|
+
} catch (error) {
|
|
234
|
+
if (error.code === 'ENOENT') return 0
|
|
235
|
+
throw error
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function removeFileIfPresent(filePath) {
|
|
240
|
+
try {
|
|
241
|
+
fsModule.unlinkSync(filePath)
|
|
242
|
+
} catch (error) {
|
|
243
|
+
if (error.code !== 'ENOENT') throw error
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function throwResponseError(res, message, requestUrl, retryable) {
|
|
248
|
+
res.resume()
|
|
249
|
+
const error = new Error(message)
|
|
250
|
+
error.statusCode = res.statusCode
|
|
251
|
+
error.retryable = retryable
|
|
252
|
+
error.requestUrl = requestUrl
|
|
253
|
+
throw error
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function parseContentRange(value) {
|
|
257
|
+
if (!value) return null
|
|
258
|
+
|
|
259
|
+
const completeMatch = value.match(/^bytes (\d+)-(\d+)\/(\d+)$/i)
|
|
260
|
+
if (completeMatch) {
|
|
261
|
+
const start = Number(completeMatch[1])
|
|
262
|
+
const end = Number(completeMatch[2])
|
|
263
|
+
const total = Number(completeMatch[3])
|
|
264
|
+
if (start > end || end >= total) return null
|
|
265
|
+
return {
|
|
266
|
+
start,
|
|
267
|
+
end,
|
|
268
|
+
total,
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const unsatisfiedMatch = value.match(/^bytes \*\/(\d+)$/i)
|
|
273
|
+
if (unsatisfiedMatch) {
|
|
274
|
+
return { start: null, end: null, total: Number(unsatisfiedMatch[1]) }
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return null
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function isRetryableStatus(statusCode) {
|
|
281
|
+
return (
|
|
282
|
+
statusCode === 408 ||
|
|
283
|
+
statusCode === 425 ||
|
|
284
|
+
statusCode === 429 ||
|
|
285
|
+
statusCode >= 500
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function downloadFile(url, destinationPath, options = {}) {
|
|
290
|
+
let resumedFrom = getFileSize(destinationPath)
|
|
291
|
+
let totalBytes = null
|
|
292
|
+
|
|
293
|
+
const headers = { ...options.headers }
|
|
294
|
+
if (resumedFrom > 0) {
|
|
295
|
+
headers.Range = `bytes=${resumedFrom}-`
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const res = await httpGet(url, { ...options, headers })
|
|
299
|
+
const responseUrl = res.requestUrl || url
|
|
300
|
+
const contentRange = parseContentRange(res.headers['content-range'])
|
|
301
|
+
|
|
302
|
+
if (res.statusCode === 416 && contentRange?.total === resumedFrom) {
|
|
303
|
+
res.resume()
|
|
304
|
+
return {
|
|
305
|
+
downloadedBytes: resumedFrom,
|
|
306
|
+
totalBytes: resumedFrom,
|
|
307
|
+
resumedFrom,
|
|
308
|
+
responseUrl,
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (res.statusCode === 416) {
|
|
313
|
+
removeFileIfPresent(destinationPath)
|
|
314
|
+
throwResponseError(
|
|
315
|
+
res,
|
|
316
|
+
'Saved partial download is no longer valid',
|
|
317
|
+
responseUrl,
|
|
318
|
+
true,
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
let writeFlags
|
|
323
|
+
let expectedResponseBytes = null
|
|
324
|
+
if (res.statusCode === 206) {
|
|
325
|
+
if (!contentRange || contentRange.start !== resumedFrom) {
|
|
326
|
+
removeFileIfPresent(destinationPath)
|
|
327
|
+
throwResponseError(
|
|
328
|
+
res,
|
|
329
|
+
`Download resume mismatch: expected byte ${resumedFrom}`,
|
|
330
|
+
responseUrl,
|
|
331
|
+
true,
|
|
332
|
+
)
|
|
333
|
+
}
|
|
334
|
+
totalBytes = contentRange.total
|
|
335
|
+
expectedResponseBytes = contentRange.end - contentRange.start + 1
|
|
336
|
+
writeFlags = 'a'
|
|
337
|
+
} else if (res.statusCode === 200) {
|
|
338
|
+
// The server may ignore Range. Restart safely instead of appending a
|
|
339
|
+
// complete response to the existing partial archive.
|
|
340
|
+
resumedFrom = 0
|
|
341
|
+
totalBytes = Number(res.headers['content-length']) || null
|
|
342
|
+
writeFlags = 'w'
|
|
343
|
+
} else {
|
|
344
|
+
throwResponseError(
|
|
345
|
+
res,
|
|
346
|
+
`Download failed: HTTP ${res.statusCode}`,
|
|
347
|
+
responseUrl,
|
|
348
|
+
isRetryableStatus(res.statusCode),
|
|
349
|
+
)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
let downloadedBytes = resumedFrom
|
|
353
|
+
res.on('data', (chunk) => {
|
|
354
|
+
downloadedBytes += chunk.length
|
|
355
|
+
options.onProgress?.({ downloadedBytes, totalBytes, resumedFrom })
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
await pipelineFn(
|
|
360
|
+
res,
|
|
361
|
+
fsModule.createWriteStream(destinationPath, { flags: writeFlags }),
|
|
362
|
+
)
|
|
363
|
+
} catch (error) {
|
|
364
|
+
error.requestUrl ||= responseUrl
|
|
365
|
+
error.downloadedBytes = getFileSize(destinationPath)
|
|
366
|
+
error.totalBytes = totalBytes
|
|
367
|
+
throw error
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
downloadedBytes = getFileSize(destinationPath)
|
|
371
|
+
const responseBytes = downloadedBytes - resumedFrom
|
|
372
|
+
if (
|
|
373
|
+
expectedResponseBytes !== null &&
|
|
374
|
+
responseBytes !== expectedResponseBytes
|
|
375
|
+
) {
|
|
376
|
+
if (responseBytes > expectedResponseBytes) {
|
|
377
|
+
removeFileIfPresent(destinationPath)
|
|
378
|
+
}
|
|
379
|
+
const error = new Error(
|
|
380
|
+
`Download incomplete: response contained ${responseBytes} of ${expectedResponseBytes} bytes`,
|
|
381
|
+
)
|
|
382
|
+
error.code = 'EINCOMPLETE'
|
|
383
|
+
error.retryable = true
|
|
384
|
+
error.requestUrl = responseUrl
|
|
385
|
+
error.downloadedBytes = getFileSize(destinationPath)
|
|
386
|
+
error.totalBytes = totalBytes
|
|
387
|
+
throw error
|
|
388
|
+
}
|
|
389
|
+
if (totalBytes !== null && downloadedBytes !== totalBytes) {
|
|
390
|
+
const error = new Error(
|
|
391
|
+
`Download incomplete: received ${downloadedBytes} of ${totalBytes} bytes`,
|
|
392
|
+
)
|
|
393
|
+
error.code = 'EINCOMPLETE'
|
|
394
|
+
error.retryable = true
|
|
395
|
+
error.requestUrl = responseUrl
|
|
396
|
+
error.downloadedBytes = downloadedBytes
|
|
397
|
+
error.totalBytes = totalBytes
|
|
398
|
+
throw error
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return { downloadedBytes, totalBytes, resumedFrom, responseUrl }
|
|
402
|
+
}
|
|
403
|
+
|
|
184
404
|
async function withRetries(
|
|
185
405
|
operation,
|
|
186
406
|
{
|
|
@@ -208,6 +428,7 @@ function createReleaseHttpClient({
|
|
|
208
428
|
|
|
209
429
|
return {
|
|
210
430
|
getProxyUrl,
|
|
431
|
+
downloadFile,
|
|
211
432
|
httpGet,
|
|
212
433
|
withRetries,
|
|
213
434
|
}
|