(url, dest, maxBytes)
| 108 | } |
| 109 | |
| 110 | function downloadHop(url, dest, maxBytes) { |
| 111 | return new Promise((resolve, reject) => { |
| 112 | let settled = false; |
| 113 | let response = null; |
| 114 | let timer = null; |
| 115 | |
| 116 | function finish(err, result) { |
| 117 | if (settled) return; |
| 118 | settled = true; |
| 119 | if (timer) clearTimeout(timer); |
| 120 | if (err) reject(err); |
| 121 | else resolve(result); |
| 122 | } |
| 123 | |
| 124 | const req = https.get(url, (res) => { |
| 125 | response = res; |
| 126 | res.on('error', (err) => finish(err)); |
| 127 | const redirectCodes = new Set([301, 302, 303, 307, 308]); |
| 128 | if (redirectCodes.has(res.statusCode)) { |
| 129 | const location = res.headers.location; |
| 130 | if (!location) { |
| 131 | res.destroy(); |
| 132 | finish(new Error(`Redirect with no location for ${url}`)); |
| 133 | return; |
| 134 | } |
| 135 | let next; |
| 136 | try { |
| 137 | next = validateUrl(new URL(location, url).href); |
| 138 | } catch (err) { |
| 139 | res.destroy(); |
| 140 | finish(err); |
| 141 | return; |
| 142 | } |
| 143 | res.destroy(); |
| 144 | finish(null, { redirect: next }); |
| 145 | return; |
| 146 | } |
| 147 | if (res.statusCode !== 200) { |
| 148 | res.destroy(); |
| 149 | finish(new Error(`HTTP ${res.statusCode} for ${url}`)); |
| 150 | return; |
| 151 | } |
| 152 | |
| 153 | let received = 0; |
| 154 | res.on('data', (chunk) => { |
| 155 | received += chunk.length; |
| 156 | if (maxBytes && received > maxBytes) { |
| 157 | res.destroy(new Error(`Download exceeds the ${maxBytes}-byte safety limit`)); |
| 158 | } |
| 159 | }); |
| 160 | const file = fs.createWriteStream(dest, { flags: 'w' }); |
| 161 | pipeline(res, file, (err) => { |
| 162 | finish(err, { redirect: null }); |
| 163 | }); |
| 164 | }); |
| 165 | req.on('error', (err) => finish(err)); |
| 166 | timer = setTimeout(() => { |
| 167 | const err = new Error(`Download hop timed out after ${DOWNLOAD_HOP_TIMEOUT_MS} ms: ${url}`); |
no test coverage detected