etwork location')); return; } return PromisePrototypeThen( PromiseResolve(fetchWithRedirects(location)), (entry) => { cacheForGET.set(parsed.href, entry); fulfill(entry); }); } catch (e) { dispose(); reject(e); } } } if (res.statusCode > 303 || res.statusCode < 200) { dispose(); reject( new ERR_NETWORK_IMPORT_BAD_RESPONSE( parsed.href, 'HTTP response returned status code of ' + res.statusCode)); return; } const { headers } = res; const contentType = headers['content-type']; if (!contentType) { dispose(); reject(new ERR_NETWORK_IMPORT_BAD_RESPONSE( parsed.href, 'the \'Content-Type\' header is required')); return; } /** * @type {CacheEntry} */ const entry = { resolvedHREF: parsed.href, headers: { 'content-type': res.headers['content-type'] }, body: new Promise((f, r) => { const buffers = []; let size = 0; let bodyStream = res; let onError; if (res.headers['content-encoding'] === 'br') { bodyStream = createBrotliDecompress(); onError = function onError(error) { bodyStream.close(); dispose(); reject(error); r(error); }; res.on('error', onError); res.pipe(bodyStream); } else if (res.headers['content-encoding'] === 'gzip' || res.headers['content-encoding'] === 'deflate') { bodyStream = createUnzip(); onError = function onError(error) { bodyStream.close(); dispose(); reject(error); r(error); }; res.on('error', onError); res.pipe(bodyStream); } else { onError = function onError(error) { dispose(); reject(error); r(error); }; } bodyStream.on('error', onError); bodyStream.on('data', (d) => { ArrayPrototypePush(buffers, d); size += d.length; }); bodyStream.on('end', () => { const body = entry.body = /** @type {Buffer} */( BufferConcat(buffers, size) ); f(body); }); }), }; cacheForGET.set(parsed.href, entry); fulfill(entry); }); }); cacheForGET.set(parsed.href, result); return result; } const allowList = new net.BlockList(); allowList.addAddress('::1', 'ipv6'); allowList.addRange('127.0.0.1', '127.255.255.255'); /** * Returns if an address has local status by if it is going to a local * interface or is an address resolved by DNS to be a local interface * @param {string} hostname url.hostname to test * @returns {Promise} */ async function isLocalAddress(hostname) { try { if (StringPrototypeStartsWith(hostname, '[') && StringPrototypeEndsWith(hostname, ']')) { hostname = StringPrototypeSlice(hostname, 1, -1); } const addr = await dnsLookup(hostname, { verbatim: true }); const ipv = addr.family === 4 ? 'ipv4' : 'ipv6'; return allowList.check(addr.address, ipv); } catch { // If it errored, the answer is no. } return false; } /** * Fetches a location with a shared cache following redirects. * Does not respect HTTP cache headers. * * This splits the header and body Promises so that things only needing * headers don't need to wait on the body. * * In cases where the request & response have already settled, this returns the * cache value synchronously. * * @param {URL} parsed * @param {ESModuleContext} context * @returns {ReturnType} */ function fetchModule(parsed, { parentURL }) { const { href } = parsed; const existing = cacheForGET.get(href); if (existing) { return existing; } if (parsed.protocol === 'http:') { return PromisePrototypeThen(isLocalAddress(parsed.hostname), (is) => { if (is !== true) { throw new ERR_NETWORK_IMPORT_DISALLOWED( href, parentURL, 'http can only be used to load local resources (use https instead).' ); } return fetchWithRedirects(parsed); }); } return fetchWithRedirects(parsed); } module.exports = { fetchModule: fetchModule };