GeezerVison is showing github.com — http: support http proxy for fetch under NODE_USE_ENV_PROXY · nodejs/node@ada34bd
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/cli.md +15Lines changed: 15 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -3545,6 +3545,21 @@ If `value` equals `'0'`, certificate validation is disabled for TLS connections. | 3545 | 3545 | This makes TLS, and HTTPS by extension, insecure. The use of this environment | 3546 | 3546 | variable is strongly discouraged. | 3547 | 3547 |
| | 3548 | +### `NODE_USE_ENV_PROXY=1` | | 3549 | + | | 3550 | +<!-- YAML | | 3551 | +added: REPLACEME | | 3552 | +--> | | 3553 | + | | 3554 | +> Stability: 1.1 - Active Development | | 3555 | +
| | 3556 | +When enabled, Node.js parses the `HTTP_PROXY`, `HTTPS_PROXY` and `NO_PROXY` | | 3557 | +environment variables during startup, and tunnels requests over the | | 3558 | +specified proxy. | | 3559 | + | | 3560 | +This currently only affects requests sent over `fetch()`. Support for other | | 3561 | +built-in `http` and `https` methods is under way. | | 3562 | + | 3548 | 3563 | ### `NODE_V8_COVERAGE=dir` | 3549 | 3564 |
| 3550 | 3565 | When set, Node.js will begin outputting [V8 JavaScript code coverage][] and | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/process/pre_execution.js +16Lines changed: 16 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -119,6 +119,7 @@ function prepareExecution(options) { | 119 | 119 | initializeConfigFileSupport(); | 120 | 120 |
| 121 | 121 | require('internal/dns/utils').initializeDns(); | | 122 | + setupHttpProxy(); | 122 | 123 |
| 123 | 124 | if (isMainThread) { | 124 | 125 | assert(internalBinding('worker').isMainThread); | @@ -154,6 +155,21 @@ function prepareExecution(options) { | 154 | 155 | return mainEntry; | 155 | 156 | } | 156 | 157 |
| | 158 | +function setupHttpProxy() { | | 159 | + if (process.env.NODE_USE_ENV_PROXY && | | 160 | + (process.env.HTTP_PROXY || process.env.HTTPS_PROXY || | | 161 | + process.env.http_proxy || process.env.https_proxy)) { | | 162 | + const { setGlobalDispatcher, EnvHttpProxyAgent } = require('internal/deps/undici/undici'); | | 163 | + const envHttpProxyAgent = new EnvHttpProxyAgent(); | | 164 | + setGlobalDispatcher(envHttpProxyAgent); | | 165 | + // TODO(joyeecheung): This currently only affects fetch. Implement handling in the | | 166 | + // http/https Agent constructor too. | | 167 | + // TODO(joyeecheung): This is currently guarded with NODE_USE_ENV_PROXY. Investigate whether | | 168 | + // it's possible to enable it by default without stepping on other existing libraries that | | 169 | + // sets the global dispatcher or monkey patches the global agent. | | 170 | + } | | 171 | +} | | 172 | + | 157 | 173 | function setupUserModules(forceDefaultLoader = false) { | 158 | 174 | initializeCJSLoader(); | 159 | 175 | initializeESMLoader(forceDefaultLoader); | |
Collapse file Copy file name to clipboard +100Lines changed: 100 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +'use strict'; | | 2 | + | | 3 | +const net = require('net'); | | 4 | +const http = require('http'); | | 5 | +const assert = require('assert'); | | 6 | + | | 7 | +function logRequest(logs, req) { | | 8 | + logs.push({ | | 9 | + method: req.method, | | 10 | + url: req.url, | | 11 | + headers: { ...req.headers }, | | 12 | + }); | | 13 | +} | | 14 | + | | 15 | +// This creates a minimal proxy server that logs the requests it gets | | 16 | +// to an array before performing proxying. | | 17 | +exports.createProxyServer = function() { | | 18 | + const logs = []; | | 19 | + | | 20 | + const proxy = http.createServer(); | | 21 | + proxy.on('request', (req, res) => { | | 22 | + logRequest(logs, req); | | 23 | + const [hostname, port] = req.headers.host.split(':'); | | 24 | + const targetPort = port || 80; | | 25 | + | | 26 | + const options = { | | 27 | + hostname: hostname, | | 28 | + port: targetPort, | | 29 | + path: req.url, | | 30 | + method: req.method, | | 31 | + headers: req.headers, | | 32 | + }; | | 33 | + | | 34 | + const proxyReq = http.request(options, (proxyRes) => { | | 35 | + res.writeHead(proxyRes.statusCode, proxyRes.headers); | | 36 | + proxyRes.pipe(res, { end: true }); | | 37 | + }); | | 38 | + | | 39 | + proxyReq.on('error', (err) => { | | 40 | + logs.push({ error: err, source: 'proxy request' }); | | 41 | + res.writeHead(500); | | 42 | + res.end('Proxy error: ' + err.message); | | 43 | + }); | | 44 | + | | 45 | + req.pipe(proxyReq, { end: true }); | | 46 | + }); | | 47 | + | | 48 | + proxy.on('connect', (req, res, head) => { | | 49 | + logRequest(logs, req); | | 50 | + | | 51 | + const [hostname, port] = req.url.split(':'); | | 52 | + const proxyReq = net.connect(port, hostname, () => { | | 53 | + res.write( | | 54 | + 'HTTP/1.1 200 Connection Established\r\n' + | | 55 | + 'Proxy-agent: Node.js-Proxy\r\n' + | | 56 | + '\r\n', | | 57 | + ); | | 58 | + proxyReq.write(head); | | 59 | + res.pipe(proxyReq); | | 60 | + proxyReq.pipe(res); | | 61 | + }); | | 62 | + | | 63 | + proxyReq.on('error', (err) => { | | 64 | + logs.push({ error: err, source: 'proxy request' }); | | 65 | + res.write('HTTP/1.1 500 Connection Error\r\n\r\n'); | | 66 | + res.end('Proxy error: ' + err.message); | | 67 | + }); | | 68 | + }); | | 69 | + | | 70 | + proxy.on('error', (err) => { | | 71 | + logs.push({ error: err, source: 'proxy server' }); | | 72 | + }); | | 73 | + | | 74 | + return { proxy, logs }; | | 75 | +}; | | 76 | + | | 77 | +exports.checkProxiedRequest = async function(envExtension, expectation) { | | 78 | + const { spawnPromisified } = require('./'); | | 79 | + const fixtures = require('./fixtures'); | | 80 | + const { code, signal, stdout, stderr } = await spawnPromisified( | | 81 | + process.execPath, | | 82 | + [fixtures.path('fetch-and-log.mjs')], { | | 83 | + env: { | | 84 | + ...process.env, | | 85 | + ...envExtension, | | 86 | + }, | | 87 | + }); | | 88 | + | | 89 | + assert.deepStrictEqual({ | | 90 | + stderr: stderr.trim(), | | 91 | + stdout: stdout.trim(), | | 92 | + code, | | 93 | + signal, | | 94 | + }, { | | 95 | + stderr: '', | | 96 | + code: 0, | | 97 | + signal: null, | | 98 | + ...expectation, | | 99 | + }); | | 100 | +}; |
Collapse file Copy file name to clipboard +3Lines changed: 3 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +const response = await fetch(process.env.FETCH_URL); | | 2 | +const body = await response.text(); | | 3 | +console.log(body); |
Collapse file Copy file name to clipboard +61Lines changed: 61 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +'use strict'; | | 2 | + | | 3 | +const common = require('../common'); | | 4 | +const assert = require('assert'); | | 5 | +const { once } = require('events'); | | 6 | +const http = require('http'); | | 7 | +const { createProxyServer, checkProxiedRequest } = require('../common/proxy-server'); | | 8 | + | | 9 | +(async () => { | | 10 | + // Start a server to process the final request. | | 11 | + const server = http.createServer(common.mustCall((req, res) => { | | 12 | + res.end('Hello world'); | | 13 | + }, 2)); | | 14 | + server.on('error', common.mustNotCall((err) => { console.error('Server error', err); })); | | 15 | + server.listen(0); | | 16 | + await once(server, 'listening'); | | 17 | + | | 18 | + // Start a minimal proxy server. | | 19 | + const { proxy, logs } = createProxyServer(); | | 20 | + proxy.listen(0); | | 21 | + await once(proxy, 'listening'); | | 22 | + | | 23 | + const serverHost = `localhost:${server.address().port}`; | | 24 | + | | 25 | + // FIXME(undici:4083): undici currently always tunnels the request over | | 26 | + // CONNECT, no matter it's HTTP traffic or not, which is different from e.g. | | 27 | + // how curl behaves. | | 28 | + const expectedLogs = [{ | | 29 | + method: 'CONNECT', | | 30 | + url: serverHost, | | 31 | + headers: { | | 32 | + // FIXME(undici:4086): this should be keep-alive. | | 33 | + connection: 'close', | | 34 | + host: serverHost | | 35 | + } | | 36 | + }]; | | 37 | + | | 38 | + // Check upper-cased HTTPS_PROXY environment variable. | | 39 | + await checkProxiedRequest({ | | 40 | + NODE_USE_ENV_PROXY: 1, | | 41 | + FETCH_URL: `http://${serverHost}/test`, | | 42 | + HTTP_PROXY: `http://localhost:${proxy.address().port}`, | | 43 | + }, { | | 44 | + stdout: 'Hello world', | | 45 | + }); | | 46 | + assert.deepStrictEqual(logs, expectedLogs); | | 47 | + | | 48 | + // Check lower-cased https_proxy environment variable. | | 49 | + logs.splice(0, logs.length); | | 50 | + await checkProxiedRequest({ | | 51 | + NODE_USE_ENV_PROXY: 1, | | 52 | + FETCH_URL: `http://${serverHost}/test`, | | 53 | + http_proxy: `http://localhost:${proxy.address().port}`, | | 54 | + }, { | | 55 | + stdout: 'Hello world', | | 56 | + }); | | 57 | + assert.deepStrictEqual(logs, expectedLogs); | | 58 | + | | 59 | + proxy.close(); | | 60 | + server.close(); | | 61 | +})().then(common.mustCall()); |
Collapse file Copy file name to clipboard +67Lines changed: 67 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +'use strict'; | | 2 | + | | 3 | +const common = require('../common'); | | 4 | +if (!common.hasCrypto) | | 5 | + common.skip('missing crypto'); | | 6 | + | | 7 | +const fixtures = require('../common/fixtures'); | | 8 | +const assert = require('assert'); | | 9 | +const https = require('https'); | | 10 | +const { once } = require('events'); | | 11 | +const { createProxyServer, checkProxiedRequest } = require('../common/proxy-server'); | | 12 | + | | 13 | +(async () => { | | 14 | + // Start a server to process the final request. | | 15 | + const server = https.createServer({ | | 16 | + cert: fixtures.readKey('agent8-cert.pem'), | | 17 | + key: fixtures.readKey('agent8-key.pem'), | | 18 | + }, common.mustCall((req, res) => { | | 19 | + res.end('Hello world'); | | 20 | + }, 2)); | | 21 | + server.on('error', common.mustNotCall((err) => { console.error('Server error', err); })); | | 22 | + server.listen(0); | | 23 | + await once(server, 'listening'); | | 24 | + | | 25 | + // Start a minimal proxy server. | | 26 | + const { proxy, logs } = createProxyServer(); | | 27 | + proxy.listen(0); | | 28 | + await once(proxy, 'listening'); | | 29 | + | | 30 | + const serverHost = `localhost:${server.address().port}`; | | 31 | + | | 32 | + const expectedLogs = [{ | | 33 | + method: 'CONNECT', | | 34 | + url: serverHost, | | 35 | + headers: { | | 36 | + // FIXME(undici:4086): this should be keep-alive. | | 37 | + connection: 'close', | | 38 | + host: serverHost | | 39 | + } | | 40 | + }]; | | 41 | + | | 42 | + // Check upper-cased HTTPS_PROXY environment variable. | | 43 | + await checkProxiedRequest({ | | 44 | + NODE_USE_ENV_PROXY: 1, | | 45 | + FETCH_URL: `https://${serverHost}/test`, | | 46 | + HTTPS_PROXY: `http://localhost:${proxy.address().port}`, | | 47 | + NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'fake-startcom-root-cert.pem'), | | 48 | + }, { | | 49 | + stdout: 'Hello world', | | 50 | + }); | | 51 | + assert.deepStrictEqual(logs, expectedLogs); | | 52 | + | | 53 | + // Check lower-cased https_proxy environment variable. | | 54 | + logs.splice(0, logs.length); | | 55 | + await checkProxiedRequest({ | | 56 | + NODE_USE_ENV_PROXY: 1, | | 57 | + FETCH_URL: `https://${serverHost}/test`, | | 58 | + https_proxy: `http://localhost:${proxy.address().port}`, | | 59 | + NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'fake-startcom-root-cert.pem'), | | 60 | + }, { | | 61 | + stdout: 'Hello world', | | 62 | + }); | | 63 | + assert.deepStrictEqual(logs, expectedLogs); | | 64 | + | | 65 | + proxy.close(); | | 66 | + server.close(); | | 67 | +})().then(common.mustCall()); |
|