| /** |
| * @license |
| * Copyright 2023 Google Inc. |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| import { createHash } from 'node:crypto'; |
| import { createWriteStream, unlinkSync } from 'node:fs'; |
| import * as http from 'node:http'; |
| import * as https from 'node:https'; |
| import { URL, urlToHttpOptions } from 'node:url'; |
| export async function headHttpRequest(url) { |
| return await new Promise(resolve => { |
| httpRequest(url, 'HEAD', response => { |
| // consume response data free node process |
| response.resume(); |
| resolve(response.statusCode === 200); |
| }, false) |
| .then(request => { |
| request.on('error', () => { |
| resolve(false); |
| }); |
| }) |
| .catch(() => { |
| resolve(false); |
| }); |
| }); |
| } |
| export async function httpRequest(url, method, response, keepAlive = true) { |
| let agent; |
| try { |
| const { ProxyAgent } = await import('proxy-agent'); |
| agent = new ProxyAgent(); |
| } |
| catch { |
| // Standard Node.js agents will be used. |
| } |
| const options = { |
| protocol: url.protocol, |
| hostname: url.hostname, |
| port: url.port, |
| path: url.pathname + url.search, |
| method, |
| headers: keepAlive ? { Connection: 'keep-alive' } : undefined, |
| auth: urlToHttpOptions(url).auth, |
| agent, |
| }; |
| const requestCallback = (res) => { |
| if (res.statusCode && |
| res.statusCode >= 300 && |
| res.statusCode < 400 && |
| res.headers.location) { |
| void httpRequest(new URL(res.headers.location), method, response); |
| // consume response data to free up memory |
| // And prevents the connection from being kept alive |
| res.resume(); |
| } |
| else { |
| response(res); |
| } |
| }; |
| const request = options.protocol === 'https:' |
| ? https.request(options, requestCallback) |
| : http.request(options, requestCallback); |
| request.end(); |
| return request; |
| } |
| class HashVerifier { |
| #hash = createHash('sha256'); |
| update(chunk) { |
| this.#hash.update(chunk); |
| } |
| verify(url, destinationPath, expectedHash) { |
| const actualHash = this.#hash.digest('hex'); |
| if (actualHash !== expectedHash.toLowerCase()) { |
| try { |
| unlinkSync(destinationPath); |
| } |
| catch { } |
| throw new Error(`Integrity check failed for downloaded browser archive.\n` + |
| ` URL: ${url}\n` + |
| ` Expected: ${expectedHash.toLowerCase()}\n` + |
| ` Actual: ${actualHash}`); |
| } |
| } |
| } |
| /** |
| * @internal |
| */ |
| export function downloadFile(url, destinationPath, progressCallback, expectedHash) { |
| return new Promise(async (resolve, reject) => { |
| let downloadedBytes = 0; |
| let totalBytes = 0; |
| const verifier = expectedHash ? new HashVerifier() : null; |
| try { |
| const request = await httpRequest(url, 'GET', response => { |
| if (response.statusCode !== 200) { |
| const error = new Error(`Download failed: server returned code ${response.statusCode}. URL: ${url}`); |
| // consume response data to free up memory |
| response.resume(); |
| reject(error); |
| return; |
| } |
| const file = createWriteStream(destinationPath); |
| file.on('close', () => { |
| if (verifier && expectedHash) { |
| try { |
| verifier.verify(url, destinationPath, expectedHash); |
| } |
| catch (err) { |
| reject(err); |
| return; |
| } |
| } |
| return resolve(); |
| }); |
| file.on('error', error => { |
| return reject(error); |
| }); |
| totalBytes = parseInt(response.headers['content-length'], 10); |
| response.on('data', (chunk) => { |
| downloadedBytes += chunk.length; |
| verifier?.update(chunk); |
| if (progressCallback) { |
| progressCallback(downloadedBytes, totalBytes); |
| } |
| }); |
| response.pipe(file); |
| }); |
| request.on('error', error => { |
| return reject(error); |
| }); |
| } |
| catch (error) { |
| reject(error); |
| } |
| }); |
| } |
| export async function getJSON(url) { |
| const text = await getText(url); |
| try { |
| return JSON.parse(text); |
| } |
| catch { |
| throw new Error('Could not parse JSON from ' + url.toString()); |
| } |
| } |
| export function getText(url) { |
| return new Promise(async (resolve, reject) => { |
| try { |
| const request = await httpRequest(url, 'GET', response => { |
| let data = ''; |
| if (response.statusCode && response.statusCode >= 400) { |
| return reject(new Error(`Got status code ${response.statusCode}`)); |
| } |
| response.on('data', chunk => { |
| data += chunk; |
| }); |
| response.on('end', () => { |
| try { |
| return resolve(String(data)); |
| } |
| catch { |
| return reject(new Error(`Failed to read text response from ${url}`)); |
| } |
| }); |
| }, false); |
| request.on('error', err => { |
| reject(err); |
| }); |
| } |
| catch (err) { |
| reject(err); |
| } |
| }); |
| } |
| //# sourceMappingURL=httpUtil.js.map |