GeezerVison is showing github.com — http,https: handle IPv6 with proxies · nodejs/node@6695067
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: lib/https.js +7-3Lines changed: 7 additions & 3 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -68,7 +68,7 @@ let debug = require('internal/util/debuglog').debuglog('https', (fn) => { | 68 | 68 | const net = require('net'); | 69 | 69 | const { URL, urlToHttpOptions, isURL } = require('internal/url'); | 70 | 70 | const { validateObject } = require('internal/validators'); | 71 | | -const { isIP, isIPv6 } = require('internal/net'); | | 71 | +const { isIP } = require('internal/net'); | 72 | 72 | const assert = require('internal/assert'); | 73 | 73 | const { getOptionValue } = require('internal/options'); | 74 | 74 |
| @@ -171,7 +171,11 @@ function getTunnelConfigForProxiedHttps(agent, reqOptions) { | 171 | 171 | } | 172 | 172 | const { auth, href } = agent[kProxyConfig]; | 173 | 173 | // The request is a HTTPS request, assemble the payload for establishing the tunnel. | 174 | | - const requestHost = isIPv6(reqOptions.host) ? `[${reqOptions.host}]` : reqOptions.host; | | 174 | + const ipType = isIP(reqOptions.host); | | 175 | + // The request target must put IPv6 address in square brackets. | | 176 | + // Here reqOptions is already processed by urlToHttpOptions so we'll add them back if necessary. | | 177 | + // See https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2 | | 178 | + const requestHost = ipType === 6 ? `[${reqOptions.host}]` : reqOptions.host; | 175 | 179 | const requestPort = reqOptions.port || agent.defaultPort; | 176 | 180 | const endpoint = `${requestHost}:${requestPort}`; | 177 | 181 | // The ClientRequest constructor should already have validated the host and the port. | @@ -198,7 +202,7 @@ function getTunnelConfigForProxiedHttps(agent, reqOptions) { | 198 | 202 | proxyTunnelPayload: payload, | 199 | 203 | requestOptions: { // Options used for the request sent after the tunnel is established. | 200 | 204 | __proto__: null, | 201 | | - servername: reqOptions.servername || (isIP(reqOptions.host) ? undefined : reqOptions.host), | | 205 | + servername: reqOptions.servername || ipType ? undefined : reqOptions.host, | 202 | 206 | ...reqOptions, | 203 | 207 | }, | 204 | 208 | }; | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/http.js +18-14Lines changed: 18 additions & 14 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 2 | 2 |
| 3 | 3 | const { | 4 | 4 | Date, | | 5 | + Number, | 5 | 6 | NumberParseInt, | 6 | 7 | Symbol, | 7 | 8 | decodeURIComponent, | @@ -99,24 +100,23 @@ function ipToInt(ip) { | 99 | 100 | * Represents the proxy configuration for an agent. The built-in http and https agent | 100 | 101 | * implementation have one of this when they are configured to use a proxy. | 101 | 102 | * @property {string} href - Full URL of the proxy server. | 102 | | - * @property {string} host - Full host including port, e.g. 'localhost:8080'. | 103 | | - * @property {string} hostname - Hostname without brackets for IPv6 addresses. | 104 | | - * @property {number} port - Port number of the proxy server. | 105 | | - * @property {string} protocol - Protocol of the proxy server, e.g. 'http:' or 'https:'. | | 103 | + * @property {string} protocol - Proxy protocol used to talk to the proxy server. | 106 | 104 | * @property {string|undefined} auth - proxy-authorization header value, if username or password is provided. | 107 | 105 | * @property {Array<string>} bypassList - List of hosts to bypass the proxy. | 108 | 106 | * @property {object} proxyConnectionOptions - Options for connecting to the proxy server. | 109 | 107 | */ | 110 | 108 | class ProxyConfig { | 111 | 109 | constructor(proxyUrl, keepAlive, noProxyList) { | 112 | | - const { host, hostname, port, protocol, username, password } = new URL(proxyUrl); | 113 | | - this.href = proxyUrl; // Full URL of the proxy server. | 114 | | - this.host = host; // Full host including port, e.g. 'localhost:8080'. | 115 | | - // Trim off the brackets from IPv6 addresses. As it's parsed from a valid URL, an opening | 116 | | - // "[" Must already have a matching "]" at the end. | 117 | | - this.hostname = hostname[0] === '[' ? hostname.slice(1, -1) : hostname; | 118 | | - this.port = port ? NumberParseInt(port, 10) : (protocol === 'https:' ? 443 : 80); | 119 | | - this.protocol = protocol; // Protocol of the proxy server, e.g. 'http:' or 'https:'. | | 110 | + let parsedURL; | | 111 | + try { | | 112 | + parsedURL = new URL(proxyUrl); | | 113 | + } catch { | | 114 | + throw new ERR_PROXY_INVALID_CONFIG(`Invalid proxy URL: ${proxyUrl}`); | | 115 | + } | | 116 | + const { hostname, port, protocol, username, password } = parsedURL; | | 117 | + | | 118 | + this.href = proxyUrl; | | 119 | + this.protocol = protocol; | 120 | 120 |
| 121 | 121 | if (username || password) { | 122 | 122 | // If username or password is provided, prepare the proxy-authorization header. | @@ -128,9 +128,13 @@ class ProxyConfig { | 128 | 128 | } else { | 129 | 129 | this.bypassList = []; // No bypass list provided. | 130 | 130 | } | | 131 | + | 131 | 132 | this.proxyConnectionOptions = { | 132 | | - host: this.hostname, | 133 | | - port: this.port, | | 133 | + // The host name comes from parsed URL so if it starts with '[' it must be an IPv6 address | | 134 | + // ending with ']'. Remove the brackets for net.connect(). | | 135 | + host: hostname[0] === '[' ? hostname.slice(1, -1) : hostname, | | 136 | + // The port comes from parsed URL so it is either '' or a valid number string. | | 137 | + port: port ? Number(port) : (protocol === 'https:' ? 443 : 80), | 134 | 138 | }; | 135 | 139 | } | 136 | 140 |
| |
Collapse file Copy file name to clipboard +36Lines changed: 36 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +// This tests that constructing agents with invalid proxy URLs throws ERR_PROXY_INVALID_CONFIG. | | 2 | +import '../common/index.mjs'; | | 3 | +import assert from 'node:assert'; | | 4 | +import http from 'node:http'; | | 5 | + | | 6 | +const testCases = [ | | 7 | + { | | 8 | + name: 'invalid IPv4 address', | | 9 | + proxyUrl: 'http://256.256.256.256:8080', | | 10 | + }, | | 11 | + { | | 12 | + name: 'invalid IPv6 address', | | 13 | + proxyUrl: 'http://::1:8080', | | 14 | + }, | | 15 | + { | | 16 | + name: 'missing host', | | 17 | + proxyUrl: 'http://:8080', | | 18 | + }, | | 19 | + { | | 20 | + name: 'non-numeric port', | | 21 | + proxyUrl: 'http://proxy.example.com:port', | | 22 | + }, | | 23 | +]; | | 24 | + | | 25 | +for (const testCase of testCases) { | | 26 | + assert.throws(() => { | | 27 | + new http.Agent({ | | 28 | + proxyEnv: { | | 29 | + HTTP_PROXY: testCase.proxyUrl, | | 30 | + }, | | 31 | + }); | | 32 | + }, { | | 33 | + code: 'ERR_PROXY_INVALID_CONFIG', | | 34 | + message: `Invalid proxy URL: ${testCase.proxyUrl}`, | | 35 | + }); | | 36 | +} |
Collapse file Copy file name to clipboard +51Lines changed: 51 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +// This tests making HTTP requests through an HTTP proxy using IPv6 addresses. | | 2 | + | | 3 | +import * as common from '../common/index.mjs'; | | 4 | +import assert from 'node:assert'; | | 5 | +import http from 'node:http'; | | 6 | +import { once } from 'events'; | | 7 | +import { createProxyServer, runProxiedRequest } from '../common/proxy-server.js'; | | 8 | + | | 9 | +if (!common.hasIPv6) { | | 10 | + common.skip('missing IPv6 support'); | | 11 | +} | | 12 | + | | 13 | +// Start a server to process the final request. | | 14 | +const server = http.createServer(common.mustCall((req, res) => { | | 15 | + res.end('Hello world'); | | 16 | +})); | | 17 | +server.on('error', common.mustNotCall((err) => { console.error('Server error', err); })); | | 18 | +server.listen(0); | | 19 | +await once(server, 'listening'); | | 20 | + | | 21 | +// Start a minimal proxy server. | | 22 | +const { proxy, logs } = createProxyServer(); | | 23 | +proxy.listen(0); | | 24 | +await once(proxy, 'listening'); | | 25 | + | | 26 | +{ | | 27 | + const serverHost = `[::1]:${server.address().port}`; | | 28 | + const requestUrl = `http://${serverHost}/test`; | | 29 | + const expectedLogs = [{ | | 30 | + method: 'GET', | | 31 | + url: requestUrl, | | 32 | + headers: { | | 33 | + 'connection': 'keep-alive', | | 34 | + 'proxy-connection': 'keep-alive', | | 35 | + 'host': serverHost, | | 36 | + }, | | 37 | + }]; | | 38 | + | | 39 | + const { code, signal, stdout } = await runProxiedRequest({ | | 40 | + NODE_USE_ENV_PROXY: 1, | | 41 | + REQUEST_URL: requestUrl, | | 42 | + HTTP_PROXY: `http://[::1]:${proxy.address().port}`, | | 43 | + }); | | 44 | + assert.deepStrictEqual(logs, expectedLogs); | | 45 | + assert.match(stdout, /Hello world/); | | 46 | + assert.strictEqual(code, 0); | | 47 | + assert.strictEqual(signal, null); | | 48 | +} | | 49 | + | | 50 | +proxy.close(); | | 51 | +server.close(); |
Collapse file Copy file name to clipboard +90Lines changed: 90 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +// This tests making HTTPS requests through an HTTP proxy using IPv6 addresses. | | 2 | + | | 3 | +import * as common from '../common/index.mjs'; | | 4 | +import fixtures from '../common/fixtures.js'; | | 5 | +import assert from 'node:assert'; | | 6 | +import { once } from 'events'; | | 7 | +import { createProxyServer, runProxiedRequest } from '../common/proxy-server.js'; | | 8 | + | | 9 | +if (!common.hasIPv6) { | | 10 | + common.skip('missing IPv6 support'); | | 11 | +} | | 12 | + | | 13 | +if (!common.hasCrypto) { | | 14 | + common.skip('missing crypto'); | | 15 | +} | | 16 | + | | 17 | +// https must be dynamically imported so that builds without crypto support | | 18 | +// can skip it. | | 19 | +const { default: https } = await import('node:https'); | | 20 | + | | 21 | +// Start a server to process the final request. | | 22 | +const server = https.createServer({ | | 23 | + cert: fixtures.readKey('agent8-cert.pem'), | | 24 | + key: fixtures.readKey('agent8-key.pem'), | | 25 | +}, common.mustCall((req, res) => { | | 26 | + res.end('Hello world'); | | 27 | +}, 2)); | | 28 | +server.on('error', common.mustNotCall((err) => { console.error('Server error', err); })); | | 29 | +server.listen(0); | | 30 | +await once(server, 'listening'); | | 31 | + | | 32 | +// Start a minimal proxy server. | | 33 | +const { proxy, logs } = createProxyServer(); | | 34 | +proxy.listen(0); | | 35 | +await once(proxy, 'listening'); | | 36 | + | | 37 | +{ | | 38 | + const serverHost = `localhost:${server.address().port}`; | | 39 | + const requestUrl = `https://${serverHost}/test`; | | 40 | + const expectedLogs = [{ | | 41 | + method: 'CONNECT', | | 42 | + url: serverHost, | | 43 | + headers: { | | 44 | + 'proxy-connection': 'keep-alive', | | 45 | + 'host': serverHost, | | 46 | + }, | | 47 | + }]; | | 48 | + | | 49 | + const { code, signal, stdout } = await runProxiedRequest({ | | 50 | + NODE_USE_ENV_PROXY: 1, | | 51 | + REQUEST_URL: requestUrl, | | 52 | + HTTPS_PROXY: `http://[::1]:${proxy.address().port}`, | | 53 | + NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'fake-startcom-root-cert.pem'), | | 54 | + }); | | 55 | + assert.deepStrictEqual(logs, expectedLogs); | | 56 | + assert.match(stdout, /Hello world/); | | 57 | + assert.strictEqual(code, 0); | | 58 | + assert.strictEqual(signal, null); | | 59 | +} | | 60 | + | | 61 | +// Test with IPv6 address in the request URL. | | 62 | +{ | | 63 | + logs.splice(0, logs.length); // Clear the logs. | | 64 | + const serverHost = `[::1]:${server.address().port}`; | | 65 | + const requestUrl = `https://${serverHost}/test`; | | 66 | + const expectedLogs = [{ | | 67 | + method: 'CONNECT', | | 68 | + url: serverHost, | | 69 | + headers: { | | 70 | + 'proxy-connection': 'keep-alive', | | 71 | + 'host': serverHost, | | 72 | + }, | | 73 | + }]; | | 74 | + | | 75 | + const { code, signal, stdout } = await runProxiedRequest({ | | 76 | + NODE_USE_ENV_PROXY: 1, | | 77 | + REQUEST_URL: requestUrl, | | 78 | + HTTPS_PROXY: `http://[::1]:${proxy.address().port}`, | | 79 | + // Disable certificate verification for this request, for we don't have | | 80 | + // a certificate for [::1]. | | 81 | + NODE_TLS_REJECT_UNAUTHORIZED: '0', | | 82 | + }); | | 83 | + assert.deepStrictEqual(logs, expectedLogs); | | 84 | + assert.match(stdout, /Hello world/); | | 85 | + assert.strictEqual(code, 0); | | 86 | + assert.strictEqual(signal, null); | | 87 | +} | | 88 | + | | 89 | +proxy.close(); | | 90 | +server.close(); |
Collapse file Copy file name to clipboardExpand all lines: test/common/proxy-server.js +6-4Lines changed: 6 additions & 4 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -32,12 +32,12 @@ exports.createProxyServer = function(options = {}) { | 32 | 32 | } | 33 | 33 | proxy.on('request', (req, res) => { | 34 | 34 | logRequest(logs, req); | 35 | | - const [hostname, port] = req.headers.host.split(':'); | | 35 | + const { hostname, port } = new URL(`http://${req.headers.host}`); | 36 | 36 | const targetPort = port || 80; | 37 | 37 |
| 38 | 38 | const url = new URL(req.url); | 39 | 39 | const options = { | 40 | | - hostname: hostname, | | 40 | + hostname: hostname.startsWith('[') ? hostname.slice(1, -1) : hostname, | 41 | 41 | port: targetPort, | 42 | 42 | path: url.pathname + url.search, // Convert back to relative URL. | 43 | 43 | method: req.method, | @@ -72,13 +72,15 @@ exports.createProxyServer = function(options = {}) { | 72 | 72 | proxy.on('connect', (req, res, head) => { | 73 | 73 | logRequest(logs, req); | 74 | 74 |
| 75 | | - const [hostname, port] = req.url.split(':'); | | 75 | + const { hostname, port } = new URL(`https://${req.url}`); | 76 | 76 |
| 77 | 77 | res.on('error', (err) => { | 78 | 78 | logs.push({ error: err, source: 'client response for connect' }); | 79 | 79 | }); | 80 | 80 |
| 81 | | - const proxyReq = net.connect(port, hostname, () => { | | 81 | + const normalizedHostname = hostname.startsWith('[') && hostname.endsWith(']') ? | | 82 | + hostname.slice(1, -1) : hostname; | | 83 | + const proxyReq = net.connect(port, normalizedHostname, () => { | 82 | 84 | res.write( | 83 | 85 | 'HTTP/1.1 200 Connection Established\r\n' + | 84 | 86 | 'Proxy-agent: Node.js-Proxy\r\n' + | |
|