codebuff 1.0.684 → 1.0.685
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 +275 -12
- package/index.js +23 -938
- package/launcher.js +1217 -0
- package/package.json +4 -4
- package/postinstall.js +0 -34
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,36 +177,260 @@ 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
|
-
const
|
|
184
|
+
const redirectCount = options.redirectCount || 0
|
|
185
|
+
const { transport, reqOptions } = await buildRequest(url, options)
|
|
146
186
|
|
|
147
187
|
return new Promise((resolve, reject) => {
|
|
148
|
-
const req =
|
|
149
|
-
if (
|
|
188
|
+
const req = transport.get(reqOptions, (res) => {
|
|
189
|
+
if ([301, 302, 303, 307, 308].includes(res.statusCode)) {
|
|
150
190
|
res.resume()
|
|
151
|
-
|
|
191
|
+
|
|
192
|
+
if (!res.headers.location) {
|
|
193
|
+
reject(
|
|
194
|
+
new Error(`Redirect ${res.statusCode} missing Location header.`),
|
|
195
|
+
)
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
if (redirectCount >= (options.maxRedirects ?? 10)) {
|
|
199
|
+
reject(new Error('Too many redirects.'))
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
httpGet(new URL(res.headers.location, url).href, {
|
|
204
|
+
...options,
|
|
205
|
+
redirectCount: redirectCount + 1,
|
|
206
|
+
})
|
|
152
207
|
.then(resolve)
|
|
153
208
|
.catch(reject)
|
|
154
209
|
return
|
|
155
210
|
}
|
|
156
211
|
|
|
212
|
+
res.requestUrl = url
|
|
157
213
|
resolve(res)
|
|
158
214
|
})
|
|
159
215
|
|
|
160
|
-
req.on('error',
|
|
216
|
+
req.on('error', (error) => {
|
|
217
|
+
error.requestUrl = url
|
|
218
|
+
reject(error)
|
|
219
|
+
})
|
|
161
220
|
req.setTimeout(options.timeout || requestTimeout, () => {
|
|
162
221
|
req.destroy()
|
|
163
|
-
|
|
222
|
+
const error = new Error('Request timeout.')
|
|
223
|
+
error.code = 'ETIMEDOUT'
|
|
224
|
+
error.requestUrl = url
|
|
225
|
+
reject(error)
|
|
164
226
|
})
|
|
165
227
|
})
|
|
166
228
|
}
|
|
167
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
|
+
|
|
404
|
+
async function withRetries(
|
|
405
|
+
operation,
|
|
406
|
+
{
|
|
407
|
+
maxAttempts = 1,
|
|
408
|
+
baseDelayMs = 1000,
|
|
409
|
+
shouldRetry = () => true,
|
|
410
|
+
onRetry = () => {},
|
|
411
|
+
sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
412
|
+
},
|
|
413
|
+
) {
|
|
414
|
+
for (let attempt = 1; ; attempt++) {
|
|
415
|
+
try {
|
|
416
|
+
return await operation(attempt)
|
|
417
|
+
} catch (error) {
|
|
418
|
+
if (attempt >= maxAttempts || !shouldRetry(error)) {
|
|
419
|
+
throw error
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const delayMs = baseDelayMs * 2 ** (attempt - 1)
|
|
423
|
+
await onRetry({ error, attempt, nextAttempt: attempt + 1, delayMs })
|
|
424
|
+
await sleep(delayMs)
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
168
429
|
return {
|
|
169
430
|
getProxyUrl,
|
|
431
|
+
downloadFile,
|
|
170
432
|
httpGet,
|
|
433
|
+
withRetries,
|
|
171
434
|
}
|
|
172
435
|
}
|
|
173
436
|
|