httpUtil.ts (3957B)
1 /** 2 * @license 3 * Copyright 2023 Google Inc. 4 * SPDX-License-Identifier: Apache-2.0 5 */ 6 7 import {createWriteStream} from 'node:fs'; 8 import * as http from 'node:http'; 9 import * as https from 'node:https'; 10 import {URL, urlToHttpOptions} from 'node:url'; 11 12 import {ProxyAgent} from 'proxy-agent'; 13 14 export function headHttpRequest(url: URL): Promise<boolean> { 15 return new Promise(resolve => { 16 const request = httpRequest( 17 url, 18 'HEAD', 19 response => { 20 // consume response data free node process 21 response.resume(); 22 resolve(response.statusCode === 200); 23 }, 24 false, 25 ); 26 request.on('error', () => { 27 resolve(false); 28 }); 29 }); 30 } 31 32 export function httpRequest( 33 url: URL, 34 method: string, 35 response: (x: http.IncomingMessage) => void, 36 keepAlive = true, 37 ): http.ClientRequest { 38 const options: http.RequestOptions = { 39 protocol: url.protocol, 40 hostname: url.hostname, 41 port: url.port, 42 path: url.pathname + url.search, 43 method, 44 headers: keepAlive ? {Connection: 'keep-alive'} : undefined, 45 auth: urlToHttpOptions(url).auth, 46 agent: new ProxyAgent(), 47 }; 48 49 const requestCallback = (res: http.IncomingMessage): void => { 50 if ( 51 res.statusCode && 52 res.statusCode >= 300 && 53 res.statusCode < 400 && 54 res.headers.location 55 ) { 56 httpRequest(new URL(res.headers.location), method, response); 57 // consume response data to free up memory 58 // And prevents the connection from being kept alive 59 res.resume(); 60 } else { 61 response(res); 62 } 63 }; 64 const request = 65 options.protocol === 'https:' 66 ? https.request(options, requestCallback) 67 : http.request(options, requestCallback); 68 request.end(); 69 return request; 70 } 71 72 /** 73 * @internal 74 */ 75 export function downloadFile( 76 url: URL, 77 destinationPath: string, 78 progressCallback?: (downloadedBytes: number, totalBytes: number) => void, 79 ): Promise<void> { 80 return new Promise<void>((resolve, reject) => { 81 let downloadedBytes = 0; 82 let totalBytes = 0; 83 84 function onData(chunk: string): void { 85 downloadedBytes += chunk.length; 86 progressCallback!(downloadedBytes, totalBytes); 87 } 88 89 const request = httpRequest(url, 'GET', response => { 90 if (response.statusCode !== 200) { 91 const error = new Error( 92 `Download failed: server returned code ${response.statusCode}. URL: ${url}`, 93 ); 94 // consume response data to free up memory 95 response.resume(); 96 reject(error); 97 return; 98 } 99 const file = createWriteStream(destinationPath); 100 file.on('finish', () => { 101 return resolve(); 102 }); 103 file.on('error', error => { 104 return reject(error); 105 }); 106 response.pipe(file); 107 totalBytes = parseInt(response.headers['content-length']!, 10); 108 if (progressCallback) { 109 response.on('data', onData); 110 } 111 }); 112 request.on('error', error => { 113 return reject(error); 114 }); 115 }); 116 } 117 118 export async function getJSON(url: URL): Promise<unknown> { 119 const text = await getText(url); 120 try { 121 return JSON.parse(text); 122 } catch { 123 throw new Error('Could not parse JSON from ' + url.toString()); 124 } 125 } 126 127 export function getText(url: URL): Promise<string> { 128 return new Promise((resolve, reject) => { 129 const request = httpRequest( 130 url, 131 'GET', 132 response => { 133 let data = ''; 134 if (response.statusCode && response.statusCode >= 400) { 135 return reject(new Error(`Got status code ${response.statusCode}`)); 136 } 137 response.on('data', chunk => { 138 data += chunk; 139 }); 140 response.on('end', () => { 141 try { 142 return resolve(String(data)); 143 } catch { 144 return reject(new Error('Chrome version not found')); 145 } 146 }); 147 }, 148 false, 149 ); 150 request.on('error', err => { 151 reject(err); 152 }); 153 }); 154 }