mirror of
https://github.com/zebrajr/node.git
synced 2025-12-06 12:20:27 +01:00
http,https: add built-in proxy support in http/https.request and Agent
This patch implements proxy support for HTTP and HTTPS clients and agents in the `http` and `https` built-ins`. When NODE_USE_ENV_PROXY is set to 1, the default global agent would parse the HTTP_PROXY/http_proxy, HTTPS_PROXY/https_proxy, NO_PROXY/no_proxy settings from the environment variables, and proxy the requests sent through the built-in http/https client accordingly. To support this, `http.Agent` and `https.Agent` now accept a few new options: - `proxyEnv`: when it's an object, the agent would read and parse the HTTP_PROXY/http_proxy, HTTPS_PROXY/https_proxy, NO_PROXY/no_proxy properties from it, and apply them based on the protocol it uses to send requests. This option allows custom agents to reuse built-in proxy support by composing options. Global agents set this to `process.env` when NODE_USE_ENV_PROXY is 1. - `defaultPort` and `protocol`: these allow setting of the default port and protocol of the agents. We also need these when configuring proxy settings and deciding whether a request should be proxied. Implementation-wise, this adds a `ProxyConfig` internal class to handle parsing and application of proxy configurations. The configuration is parsed during agent construction. When requests are made, the `createConnection()` methods on the agents would check whether the request should be proxied. If yes, they either connect to the proxy server (in the case of HTTP reqeusts) or establish a tunnel (in the case of HTTPS requests) through either a TCP socket (if the proxy uses HTTP) or a TLS socket (if the proxy uses HTTPS). When proxying HTTPS requests through a tunnel, the connection listener is invoked after the tunnel is established. Tunnel establishment uses the timeout of the request options, if there is one. Otherwise it uses the timeout of the agent. If an error is encountered during tunnel establishment, an ERR_PROXY_TUNNEL would be emitted on the returned socket. If the proxy server sends a errored status code, the error would contain an `statusCode` property. If the error is caused by timeout, the error would contain a `proxyTunnelTimeout` property. This implementation honors the built-in socket pool and socket limits. Pooled sockets are still keyed by request endpoints, they are just connected to the proxy server instead, and the persistence of the connection can be maintained as long as the proxy server respects connection/proxy-connection or persist by default (HTTP/1.1) PR-URL: https://github.com/nodejs/node/pull/58980 Refs: https://github.com/nodejs/node/issues/57872 Refs: https://github.com/nodejs/node/issues/8381 Refs: https://github.com/nodejs/node/issues/15620 Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
This commit is contained in:
parent
0221d6b652
commit
036b1fd66d
|
|
@ -2491,6 +2491,18 @@ Accessing `Object.prototype.__proto__` has been forbidden using
|
|||
[`Object.setPrototypeOf`][] should be used to get and set the prototype of an
|
||||
object.
|
||||
|
||||
<a id="ERR_PROXY_INVALID_CONFIG"></a>
|
||||
|
||||
### `ERR_PROXY_INVALID_CONFIG`
|
||||
|
||||
Failed to proxy a request because the proxy configuration is invalid.
|
||||
|
||||
<a id="ERR_PROXY_TUNNEL"></a>
|
||||
|
||||
### `ERR_PROXY_TUNNEL`
|
||||
|
||||
Failed to establish proxy tunnel when `NODE_USE_ENV_PROXY` is enabled.
|
||||
|
||||
<a id="ERR_QUIC_APPLICATION_ERROR"></a>
|
||||
|
||||
### `ERR_QUIC_APPLICATION_ERROR`
|
||||
|
|
|
|||
114
doc/api/http.md
114
doc/api/http.md
|
|
@ -116,6 +116,14 @@ http.get({
|
|||
<!-- YAML
|
||||
added: v0.3.4
|
||||
changes:
|
||||
- version:
|
||||
- REPLACEME
|
||||
pr-url: https://github.com/nodejs/node/pull/58980
|
||||
description: Add support for `proxyEnv`.
|
||||
- version:
|
||||
- REPLACEME
|
||||
pr-url: https://github.com/nodejs/node/pull/58980
|
||||
description: Add support for `defaultPort` and `protocol`.
|
||||
- version:
|
||||
- v15.6.0
|
||||
- v14.17.0
|
||||
|
|
@ -178,6 +186,20 @@ changes:
|
|||
**Default:** `'lifo'`.
|
||||
* `timeout` {number} Socket timeout in milliseconds.
|
||||
This will set the timeout when the socket is created.
|
||||
* `proxyEnv` {Object|undefined} Environment variables for proxy configuration.
|
||||
See [Built-in Proxy Support][] for details. **Default:** `undefined`
|
||||
* `HTTP_PROXY` {string|undefined} URL for the proxy server that HTTP requests should use.
|
||||
If undefined, no proxy is used for HTTP requests.
|
||||
* `HTTPS_PROXY` {string|undefined} URL for the proxy server that HTTPS requests should use.
|
||||
If undefined, no proxy is used for HTTPS requests.
|
||||
* `NO_PROXY` {string|undefined} Patterns specifying the endpoints
|
||||
that should not be routed through a proxy.
|
||||
* `http_proxy` {string|undefined} Same as `HTTP_PROXY`. If both are set, `http_proxy` takes precedence.
|
||||
* `https_proxy` {string|undefined} Same as `HTTPS_PROXY`. If both are set, `https_proxy` takes precedence.
|
||||
* `no_proxy` {string|undefined} Same as `NO_PROXY`. If both are set, `no_proxy` takes precedence.
|
||||
* `defaultPort` {number} Default port to use when the port is not specified
|
||||
in requests. **Default:** `80`.
|
||||
* `protocol` {string} The protocol to use for the agent. **Default:** `'http:'`.
|
||||
|
||||
`options` in [`socket.connect()`][] are also supported.
|
||||
|
||||
|
|
@ -4243,6 +4265,98 @@ added:
|
|||
|
||||
A browser-compatible implementation of {WebSocket}.
|
||||
|
||||
## Built-in Proxy Support
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
> Stability: 1.1 - Active development
|
||||
|
||||
When Node.js creates the global agent, it checks the `NODE_USE_ENV_PROXY`
|
||||
environment variable. If it is set to `1`, the global agent will be constructed
|
||||
with `proxyEnv: process.env`, enabling proxy support based on the environment variables.
|
||||
|
||||
Custom agents can also be created with proxy support by passing a
|
||||
`proxyEnv` option when constructing the agent. The value can be `process.env`
|
||||
if they just want to inherit the configuration from the environment variables,
|
||||
or an object with specific setting overriding the environment.
|
||||
|
||||
The following properties of the `proxyEnv` are checked to configure proxy
|
||||
support.
|
||||
|
||||
* `HTTP_PROXY` or `http_proxy`: Proxy server URL for HTTP requests. If both are set,
|
||||
`http_proxy` takes precedence.
|
||||
* `HTTPS_PROXY` or `https_proxy`: Proxy server URL for HTTPS requests. If both are set,
|
||||
`https_proxy` takes precedence.
|
||||
* `NO_PROXY` or `no_proxy`: Comma-separated list of hosts to bypass the proxy. If both are set,
|
||||
`no_proxy` takes precedence.
|
||||
|
||||
If the request is made to a Unix domain socket, the proxy settings will be ignored.
|
||||
|
||||
### Proxy URL Format
|
||||
|
||||
Proxy URLs can use either HTTP or HTTPS protocols:
|
||||
|
||||
* HTTP proxy: `http://proxy.example.com:8080`
|
||||
* HTTPS proxy: `https://proxy.example.com:8080`
|
||||
* Proxy with authentication: `http://username:password@proxy.example.com:8080`
|
||||
|
||||
### `NO_PROXY` Format
|
||||
|
||||
The `NO_PROXY` environment variable supports several formats:
|
||||
|
||||
* `*` - Bypass proxy for all hosts
|
||||
* `example.com` - Exact host name match
|
||||
* `.example.com` - Domain suffix match (matches `sub.example.com`)
|
||||
* `*.example.com` - Wildcard domain match
|
||||
* `192.168.1.100` - Exact IP address match
|
||||
* `192.168.1.1-192.168.1.100` - IP address range
|
||||
* `example.com:8080` - Hostname with specific port
|
||||
|
||||
Multiple entries should be separated by commas.
|
||||
|
||||
### Example
|
||||
|
||||
Starting a Node.js process with proxy support enabled for all requests sent
|
||||
through the default global agent:
|
||||
|
||||
```console
|
||||
NODE_USE_ENV_PROXY=1 HTTP_PROXY=http://proxy.example.com:8080 NO_PROXY=localhost,127.0.0.1 node client.js
|
||||
```
|
||||
|
||||
To create a custom agent with built-in proxy support:
|
||||
|
||||
```cjs
|
||||
const http = require('node:http');
|
||||
|
||||
// Creating a custom agent with custom proxy support.
|
||||
const agent = new http.Agent({ proxyEnv: { HTTP_PROXY: 'http://proxy.example.com:8080' } });
|
||||
|
||||
http.request({
|
||||
hostname: 'www.example.com',
|
||||
port: 80,
|
||||
path: '/',
|
||||
agent,
|
||||
}, (res) => {
|
||||
// This request will be proxied through proxy.example.com:8080 using the HTTP protocol.
|
||||
console.log(`STATUS: ${res.statusCode}`);
|
||||
});
|
||||
```
|
||||
|
||||
Alternatively, the following also works:
|
||||
|
||||
```cjs
|
||||
const http = require('node:http');
|
||||
// Use lower-cased option name.
|
||||
const agent1 = new http.Agent({ proxyEnv: { http_proxy: 'http://proxy.example.com:8080' } });
|
||||
// Use values inherited from the environment variables, if the process is started with
|
||||
// HTTP_PROXY=http://proxy.example.com:8080 this will use the proxy server specified
|
||||
// in process.env.HTTP_PROXY.
|
||||
const agent2 = new http.Agent({ proxyEnv: process.env });
|
||||
```
|
||||
|
||||
[Built-in Proxy Support]: #built-in-proxy-support
|
||||
[RFC 8187]: https://www.rfc-editor.org/rfc/rfc8187.txt
|
||||
[`'ERR_HTTP_CONTENT_LENGTH_MISMATCH'`]: errors.md#err_http_content_length_mismatch
|
||||
[`'checkContinue'`]: #event-checkcontinue
|
||||
|
|
|
|||
|
|
@ -65,6 +65,14 @@ An [`Agent`][] object for HTTPS similar to [`http.Agent`][]. See
|
|||
|
||||
<!-- YAML
|
||||
changes:
|
||||
- version:
|
||||
- REPLACEME
|
||||
pr-url: https://github.com/nodejs/node/pull/58980
|
||||
description: Add support for `proxyEnv`.
|
||||
- version:
|
||||
- REPLACEME
|
||||
pr-url: https://github.com/nodejs/node/pull/58980
|
||||
description: Add support for `defaultPort` and `protocol`.
|
||||
- version: v12.5.0
|
||||
pr-url: https://github.com/nodejs/node/pull/28209
|
||||
description: do not automatically set servername if the target host was
|
||||
|
|
|
|||
|
|
@ -34,9 +34,17 @@ const EventEmitter = require('events');
|
|||
let debug = require('internal/util/debuglog').debuglog('http', (fn) => {
|
||||
debug = fn;
|
||||
});
|
||||
const {
|
||||
parseProxyConfigFromEnv,
|
||||
kProxyConfig,
|
||||
checkShouldUseProxy,
|
||||
kWaitForProxyTunnel,
|
||||
filterEnvForProxies,
|
||||
} = require('internal/http');
|
||||
const { AsyncResource } = require('async_hooks');
|
||||
const { async_id_symbol } = require('internal/async_hooks').symbols;
|
||||
const {
|
||||
getLazy,
|
||||
kEmptyObject,
|
||||
once,
|
||||
} = require('internal/util');
|
||||
|
|
@ -45,6 +53,7 @@ const {
|
|||
validateOneOf,
|
||||
validateString,
|
||||
} = require('internal/validators');
|
||||
const assert = require('internal/assert');
|
||||
|
||||
const kOnKeylog = Symbol('onkeylog');
|
||||
const kRequestOptions = Symbol('requestOptions');
|
||||
|
|
@ -84,11 +93,11 @@ function Agent(options) {
|
|||
|
||||
EventEmitter.call(this);
|
||||
|
||||
this.defaultPort = 80;
|
||||
this.protocol = 'http:';
|
||||
|
||||
this.options = { __proto__: null, ...options };
|
||||
|
||||
this.defaultPort = this.options.defaultPort || 80;
|
||||
this.protocol = this.options.protocol || 'http:';
|
||||
|
||||
if (this.options.noDelay === undefined)
|
||||
this.options.noDelay = true;
|
||||
|
||||
|
|
@ -104,6 +113,11 @@ function Agent(options) {
|
|||
this.scheduling = this.options.scheduling || 'lifo';
|
||||
this.maxTotalSockets = this.options.maxTotalSockets;
|
||||
this.totalSocketCount = 0;
|
||||
const proxyEnv = this.options.proxyEnv;
|
||||
if (typeof proxyEnv === 'object' && proxyEnv !== null) {
|
||||
this[kProxyConfig] = parseProxyConfigFromEnv(proxyEnv, this.protocol, this.keepAlive);
|
||||
debug(`new ${this.protocol} agent with proxy config`, this[kProxyConfig]);
|
||||
}
|
||||
|
||||
validateOneOf(this.scheduling, 'scheduling', ['fifo', 'lifo']);
|
||||
|
||||
|
|
@ -200,9 +214,40 @@ function maybeEnableKeylog(eventName) {
|
|||
}
|
||||
}
|
||||
|
||||
const lazyTLS = getLazy(() => require('tls'));
|
||||
|
||||
Agent.defaultMaxSockets = Infinity;
|
||||
|
||||
Agent.prototype.createConnection = net.createConnection;
|
||||
// See ProxyConfig in internal/http.js for how the connection should be handled
|
||||
// when the agent is configured to use a proxy server.
|
||||
Agent.prototype.createConnection = function createConnection(...args) {
|
||||
const normalized = net._normalizeArgs(args);
|
||||
const options = normalized[0];
|
||||
const cb = normalized[1];
|
||||
|
||||
// Check if this specific request should bypass the proxy
|
||||
const shouldUseProxy = checkShouldUseProxy(this[kProxyConfig], options);
|
||||
debug(`http createConnection should use proxy for ${options.host}:${options.port}:`, shouldUseProxy);
|
||||
if (!shouldUseProxy) { // Forward to net.createConnection if no proxying is needed.
|
||||
return net.createConnection(...args);
|
||||
}
|
||||
|
||||
// Create a copy of the shared proxy connection options and connect
|
||||
// to the proxy server instead of the endpoint. For Agent.prototype.createConnection
|
||||
// which is used by the http agent, this is enough
|
||||
const connectOptions = {
|
||||
...this[kProxyConfig].proxyConnectionOptions,
|
||||
};
|
||||
const proxyProtocol = this[kProxyConfig].protocol;
|
||||
if (proxyProtocol === 'http:') {
|
||||
return net.connect(connectOptions, cb);
|
||||
} else if (proxyProtocol === 'https:') {
|
||||
return lazyTLS().connect(connectOptions, cb);
|
||||
}
|
||||
// This should be unreachable because proxy config should be null for other protocols.
|
||||
assert.fail(`Unexpected proxy protocol ${proxyProtocol}`);
|
||||
|
||||
};
|
||||
|
||||
// Get the key for a given set of request options
|
||||
Agent.prototype.getName = function getName(options = kEmptyObject) {
|
||||
|
|
@ -227,6 +272,16 @@ Agent.prototype.getName = function getName(options = kEmptyObject) {
|
|||
return name;
|
||||
};
|
||||
|
||||
function handleSocketAfterProxy(err, req) {
|
||||
if (err.code === 'ERR_PROXY_TUNNEL') {
|
||||
if (err.proxyTunnelTimeout) {
|
||||
req.emit('timeout'); // Propagate the timeout from the tunnel to the request.
|
||||
} else {
|
||||
req.emit('error', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Agent.prototype.addRequest = function addRequest(req, options, port/* legacy */,
|
||||
localAddress/* legacy */) {
|
||||
// Legacy API: addRequest(req, host, port, localAddress)
|
||||
|
|
@ -239,6 +294,7 @@ Agent.prototype.addRequest = function addRequest(req, options, port/* legacy */,
|
|||
};
|
||||
}
|
||||
|
||||
// Here the agent options will override per-request options.
|
||||
options = { __proto__: null, ...options, ...this.options };
|
||||
if (options.socketPath)
|
||||
options.path = options.socketPath;
|
||||
|
|
@ -264,6 +320,7 @@ Agent.prototype.addRequest = function addRequest(req, options, port/* legacy */,
|
|||
const freeLen = freeSockets ? freeSockets.length : 0;
|
||||
const sockLen = freeLen + this.sockets[name].length;
|
||||
|
||||
// Reusing a socket from the pool.
|
||||
if (socket) {
|
||||
asyncResetHandle(socket);
|
||||
this.reuseSocket(socket, req);
|
||||
|
|
@ -271,13 +328,16 @@ Agent.prototype.addRequest = function addRequest(req, options, port/* legacy */,
|
|||
this.sockets[name].push(socket);
|
||||
} else if (sockLen < this.maxSockets &&
|
||||
this.totalSocketCount < this.maxTotalSockets) {
|
||||
debug('call onSocket', sockLen, freeLen);
|
||||
// If we are under maxSockets create a new one.
|
||||
this.createSocket(req, options, (err, socket) => {
|
||||
if (err)
|
||||
if (err) {
|
||||
handleSocketAfterProxy(err, req);
|
||||
debug('call onSocket', sockLen, freeLen);
|
||||
req.onSocket(socket, err);
|
||||
else
|
||||
setRequestSocket(this, req, socket);
|
||||
return;
|
||||
}
|
||||
|
||||
setRequestSocket(this, req, socket);
|
||||
});
|
||||
} else {
|
||||
debug('wait for socket');
|
||||
|
|
@ -294,16 +354,23 @@ Agent.prototype.addRequest = function addRequest(req, options, port/* legacy */,
|
|||
};
|
||||
|
||||
Agent.prototype.createSocket = function createSocket(req, options, cb) {
|
||||
// Here the agent options will override per-request options.
|
||||
options = { __proto__: null, ...options, ...this.options };
|
||||
if (options.socketPath)
|
||||
options.path = options.socketPath;
|
||||
|
||||
normalizeServerName(options, req);
|
||||
|
||||
// Make sure per-request timeout is respected.
|
||||
const timeout = req.timeout || this.options.timeout || undefined;
|
||||
if (timeout) {
|
||||
options.timeout = timeout;
|
||||
}
|
||||
|
||||
const name = this.getName(options);
|
||||
options._agentKey = name;
|
||||
|
||||
debug('createConnection', name, options);
|
||||
debug('createConnection', name);
|
||||
options.encoding = null;
|
||||
|
||||
const oncreate = once((err, s) => {
|
||||
|
|
@ -321,8 +388,15 @@ Agent.prototype.createSocket = function createSocket(req, options, cb) {
|
|||
options.keepAlive = this.keepAlive;
|
||||
options.keepAliveInitialDelay = this.keepAliveMsecs;
|
||||
}
|
||||
|
||||
const newSocket = this.createConnection(options, oncreate);
|
||||
if (newSocket)
|
||||
// In the case where we are proxying through a tunnel for HTTPS, only add
|
||||
// the socket to the pool and install/invoke the listeners after
|
||||
// the tunnel is successfully established, so that actual operations
|
||||
// on the socket all go through the tunnel. Errors emitted during
|
||||
// tunnel establishment will be handled in the createConnection method
|
||||
// in lib/https.js.
|
||||
if (newSocket && !newSocket[kWaitForProxyTunnel])
|
||||
oncreate(null, newSocket);
|
||||
};
|
||||
|
||||
|
|
@ -456,10 +530,13 @@ Agent.prototype.removeSocket = function removeSocket(s, options) {
|
|||
req[kRequestOptions] = undefined;
|
||||
// If we have pending requests and a socket gets closed make a new one
|
||||
this.createSocket(req, options, (err, socket) => {
|
||||
if (err)
|
||||
req.onSocket(socket, err);
|
||||
else
|
||||
socket.emit('free');
|
||||
if (err) {
|
||||
handleSocketAfterProxy(err, req);
|
||||
req.onSocket(null, err);
|
||||
return;
|
||||
}
|
||||
|
||||
socket.emit('free');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -543,5 +620,8 @@ function asyncResetHandle(socket) {
|
|||
|
||||
module.exports = {
|
||||
Agent,
|
||||
globalAgent: new Agent({ keepAlive: true, scheduling: 'lifo', timeout: 5000 }),
|
||||
globalAgent: new Agent({
|
||||
keepAlive: true, scheduling: 'lifo', timeout: 5000,
|
||||
proxyEnv: process.env.NODE_USE_ENV_PROXY ? filterEnvForProxies(process.env) : undefined,
|
||||
}),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -64,6 +64,8 @@ const {
|
|||
traceBegin,
|
||||
traceEnd,
|
||||
getNextTraceEventId,
|
||||
kProxyConfig,
|
||||
checkShouldUseProxy,
|
||||
} = require('internal/http');
|
||||
const {
|
||||
ConnResetException,
|
||||
|
|
@ -134,6 +136,56 @@ class HTTPClientAsyncResource {
|
|||
}
|
||||
}
|
||||
|
||||
// When proxying a HTTP request, the following needs to be done:
|
||||
// https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.2
|
||||
// 1. Rewrite the request path to absolute-form.
|
||||
// 2. Add proxy-connection and proxy-authorization headers appropriately.
|
||||
//
|
||||
// This function checks whether the request should be rewritten for proxying
|
||||
// and modifies the headers as well as req.path if necessary.
|
||||
// The handling of the proxy server connection is done in createConnection.
|
||||
function rewriteForProxiedHttp(req, reqOptions) {
|
||||
if (req._header) {
|
||||
debug('request._header is already sent, skipping rewriteForProxiedHttp', reqOptions);
|
||||
return false;
|
||||
}
|
||||
const agent = req.agent;
|
||||
if (!agent || !agent[kProxyConfig]) {
|
||||
return false;
|
||||
}
|
||||
if ((reqOptions.protocol || agent.protocol) !== 'http:') {
|
||||
return false;
|
||||
}
|
||||
// TODO(joyeecheung): cache this on the req or on the agent.
|
||||
const shouldUseProxy = checkShouldUseProxy(agent[kProxyConfig], reqOptions);
|
||||
debug(`rewriteForProxiedHttp should use proxy for ${reqOptions.host}:${reqOptions.port}:`, shouldUseProxy);
|
||||
if (!shouldUseProxy) {
|
||||
return false;
|
||||
}
|
||||
// Add proxy headers.
|
||||
const { auth, href } = agent[kProxyConfig];
|
||||
if (auth) {
|
||||
req.setHeader('proxy-authorization', auth);
|
||||
}
|
||||
if (req.shouldKeepAlive) {
|
||||
req.setHeader('proxy-connection', 'keep-alive');
|
||||
} else {
|
||||
req.setHeader('proxy-connection', 'close');
|
||||
}
|
||||
|
||||
// Convert the path to absolute-form.
|
||||
// https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.2
|
||||
const requestHost = req.getHeader('host') || 'localhost';
|
||||
const requestBase = `http://${requestHost}`;
|
||||
const requestURL = new URL(req.path, requestBase);
|
||||
if (reqOptions.port) {
|
||||
requestURL.port = reqOptions.port;
|
||||
}
|
||||
req.path = requestURL.href;
|
||||
debug(`updated request for HTTP proxy ${href} with ${req.path} `, req[kOutHeaders]);
|
||||
return true;
|
||||
};
|
||||
|
||||
function ClientRequest(input, options, cb) {
|
||||
OutgoingMessage.call(this);
|
||||
|
||||
|
|
@ -332,10 +384,14 @@ function ClientRequest(input, options, cb) {
|
|||
throw new ERR_HTTP_HEADERS_SENT('render');
|
||||
}
|
||||
|
||||
rewriteForProxiedHttp(this, optsWithoutSignal);
|
||||
this._storeHeader(this.method + ' ' + this.path + ' HTTP/1.1\r\n',
|
||||
this[kOutHeaders]);
|
||||
} else {
|
||||
rewriteForProxiedHttp(this, optsWithoutSignal);
|
||||
}
|
||||
} else {
|
||||
rewriteForProxiedHttp(this, optsWithoutSignal);
|
||||
this._storeHeader(this.method + ' ' + this.path + ' HTTP/1.1\r\n',
|
||||
options.headers);
|
||||
}
|
||||
|
|
|
|||
282
lib/https.js
282
lib/https.js
|
|
@ -29,6 +29,7 @@ const {
|
|||
ArrayPrototypeUnshift,
|
||||
FunctionPrototypeCall,
|
||||
JSONStringify,
|
||||
NumberParseInt,
|
||||
ObjectAssign,
|
||||
ObjectSetPrototypeOf,
|
||||
ReflectApply,
|
||||
|
|
@ -40,10 +41,18 @@ const {
|
|||
assertCrypto,
|
||||
kEmptyObject,
|
||||
promisify,
|
||||
once,
|
||||
} = require('internal/util');
|
||||
const { ERR_PROXY_TUNNEL } = require('internal/errors').codes;
|
||||
assertCrypto();
|
||||
|
||||
const tls = require('tls');
|
||||
const {
|
||||
kProxyConfig,
|
||||
checkShouldUseProxy,
|
||||
filterEnvForProxies,
|
||||
kWaitForProxyTunnel,
|
||||
} = require('internal/http');
|
||||
const { Agent: HttpAgent } = require('_http_agent');
|
||||
const {
|
||||
httpServerPreClose,
|
||||
|
|
@ -56,8 +65,11 @@ const { ClientRequest } = require('_http_client');
|
|||
let debug = require('internal/util/debuglog').debuglog('https', (fn) => {
|
||||
debug = fn;
|
||||
});
|
||||
const net = require('net');
|
||||
const { URL, urlToHttpOptions, isURL } = require('internal/url');
|
||||
const { validateObject } = require('internal/validators');
|
||||
const { isIP, isIPv6 } = require('internal/net');
|
||||
const assert = require('internal/assert');
|
||||
|
||||
function Server(opts, requestListener) {
|
||||
if (!(this instanceof Server)) return new Server(opts, requestListener);
|
||||
|
|
@ -135,26 +147,198 @@ function createServer(opts, requestListener) {
|
|||
return new Server(opts, requestListener);
|
||||
}
|
||||
|
||||
// When proxying a HTTPS request, the following needs to be done:
|
||||
// https://datatracker.ietf.org/doc/html/rfc9110#CONNECT
|
||||
// 1. Send a CONNECT request to the proxy server.
|
||||
// 2. Wait for 200 connection established response to establish the tunnel.
|
||||
// 3. Perform TLS handshake with the endpoint over the socket.
|
||||
// 4. Tunnel the request using the established connection.
|
||||
//
|
||||
// This function computes the tunnel configuration for HTTPS requests.
|
||||
// The handling of the tunnel connection is done in createConnection.
|
||||
function getTunnelConfigForProxiedHttps(agent, reqOptions) {
|
||||
if (!agent[kProxyConfig]) {
|
||||
return null;
|
||||
}
|
||||
if ((reqOptions.protocol || agent.protocol) !== 'https:') {
|
||||
return null;
|
||||
}
|
||||
const shouldUseProxy = checkShouldUseProxy(agent[kProxyConfig], reqOptions);
|
||||
debug(`getTunnelConfigForProxiedHttps should use proxy for ${reqOptions.host}:${reqOptions.port}:`, shouldUseProxy);
|
||||
if (!shouldUseProxy) {
|
||||
return null;
|
||||
}
|
||||
const { auth, href } = agent[kProxyConfig];
|
||||
// The request is a HTTPS request, assemble the payload for establishing the tunnel.
|
||||
const requestHost = isIPv6(reqOptions.host) ? `[${reqOptions.host}]` : reqOptions.host;
|
||||
const requestPort = reqOptions.port || agent.defaultPort;
|
||||
const endpoint = `${requestHost}:${requestPort}`;
|
||||
// The ClientRequest constructor should already have validated the host and the port.
|
||||
// When the request options come from a string invalid characters would be stripped away,
|
||||
// when it's an object ERR_INVALID_CHAR would be thrown. Here we just assert in case
|
||||
// agent.createConnection() is called with invalid options.
|
||||
assert(!endpoint.includes('\r'));
|
||||
assert(!endpoint.includes('\n'));
|
||||
|
||||
let payload = `CONNECT ${endpoint} HTTP/1.1\r\n`;
|
||||
// The parseProxyConfigFromEnv() method should have already validated the authorization header
|
||||
// value.
|
||||
if (auth) {
|
||||
payload += `proxy-authorization: ${auth}\r\n`;
|
||||
}
|
||||
if (agent.keepAlive || agent.maxSockets !== Infinity) {
|
||||
payload += 'proxy-connection: keep-alive\r\n';
|
||||
}
|
||||
payload += `host: ${endpoint}`;
|
||||
payload += '\r\n\r\n';
|
||||
|
||||
const result = {
|
||||
__proto__: null,
|
||||
proxyTunnelPayload: payload,
|
||||
requestOptions: { // Options used for the request sent after the tunnel is established.
|
||||
__proto__: null,
|
||||
servername: reqOptions.servername || (isIP(reqOptions.host) ? undefined : reqOptions.host),
|
||||
...reqOptions,
|
||||
},
|
||||
};
|
||||
debug(`updated request for HTTPS proxy ${href} with`, result);
|
||||
return result;
|
||||
};
|
||||
|
||||
function establishTunnel(agent, socket, options, tunnelConfig, afterSocket) {
|
||||
const { proxyTunnelPayload } = tunnelConfig;
|
||||
// By default, the socket is in paused mode. Read to look for the 200
|
||||
// connection established response.
|
||||
function read() {
|
||||
let chunk;
|
||||
while ((chunk = socket.read()) !== null) {
|
||||
if (onProxyData(chunk) !== -1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
socket.on('readable', read);
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
socket.removeListener('end', onProxyEnd);
|
||||
socket.removeListener('error', onProxyError);
|
||||
socket.removeListener('readable', read);
|
||||
socket.setTimeout(0); // Clear the timeout for the tunnel establishment.
|
||||
}
|
||||
|
||||
function onProxyError(err) {
|
||||
debug('onProxyError', err);
|
||||
cleanup();
|
||||
afterSocket(err, socket);
|
||||
}
|
||||
|
||||
// Read the headers from the chunks and check for the status code. If it fails we
|
||||
// clean up the socket and return an error. Otherwise we establish the tunnel.
|
||||
let buffer = '';
|
||||
function onProxyData(chunk) {
|
||||
const str = chunk.toString();
|
||||
debug('onProxyData', str);
|
||||
buffer += str;
|
||||
const headerEndIndex = buffer.indexOf('\r\n\r\n');
|
||||
if (headerEndIndex === -1) return headerEndIndex;
|
||||
const statusLine = buffer.substring(0, buffer.indexOf('\r\n'));
|
||||
const statusCode = statusLine.split(' ')[1];
|
||||
if (statusCode !== '200') {
|
||||
debug(`onProxyData receives ${statusCode}, cleaning up`);
|
||||
cleanup();
|
||||
const targetHost = proxyTunnelPayload.split('\r')[0].split(' ')[1];
|
||||
const message = `Failed to establish tunnel to ${targetHost} via ${agent[kProxyConfig].href}: ${statusLine}`;
|
||||
const err = new ERR_PROXY_TUNNEL(message);
|
||||
err.statusCode = NumberParseInt(statusCode);
|
||||
afterSocket(err, socket);
|
||||
} else {
|
||||
// https://datatracker.ietf.org/doc/html/rfc9110#CONNECT
|
||||
// RFC 9110 says that it can be 2xx but in the real world, proxy clients generally only
|
||||
// accepts 200.
|
||||
// Proxy servers are not supposed to send anything after the headers - the payload must be
|
||||
// be empty. So after this point we will proceed with the tunnel e.g. starting TLS handshake.
|
||||
debug('onProxyData receives 200, establishing tunnel');
|
||||
cleanup();
|
||||
|
||||
// Reuse the tunneled socket to perform the TLS handshake with the endpoint,
|
||||
// then send the request.
|
||||
const { requestOptions } = tunnelConfig;
|
||||
tunnelConfig.requestOptions = null;
|
||||
requestOptions.socket = socket;
|
||||
let tunneldSocket;
|
||||
const onTLSHandshakeError = (err) => {
|
||||
debug('Propagate error event from tunneled socket to tunnel socket');
|
||||
afterSocket(err, tunneldSocket);
|
||||
};
|
||||
tunneldSocket = tls.connect(requestOptions, () => {
|
||||
debug('TLS handshake over tunnel succeeded');
|
||||
tunneldSocket.removeListener('error', onTLSHandshakeError);
|
||||
afterSocket(null, tunneldSocket);
|
||||
});
|
||||
tunneldSocket.on('free', () => {
|
||||
debug('Propagate free event from tunneled socket to tunnel socket');
|
||||
socket.emit('free');
|
||||
});
|
||||
tunneldSocket.on('error', onTLSHandshakeError);
|
||||
}
|
||||
return headerEndIndex;
|
||||
}
|
||||
|
||||
function onProxyEnd() {
|
||||
cleanup();
|
||||
const err = new ERR_PROXY_TUNNEL('Connection to establish proxy tunnel ended unexpectedly');
|
||||
afterSocket(err, socket);
|
||||
}
|
||||
|
||||
const proxyTunnelTimeout = tunnelConfig.requestOptions.timeout;
|
||||
debug('proxyTunnelTimeout', proxyTunnelTimeout, options.timeout);
|
||||
// It may be worth a separate timeout error/event.
|
||||
// But it also makes sense to treat the tunnel establishment timeout as
|
||||
// a normal timeout for the request.
|
||||
function onProxyTimeout() {
|
||||
debug('onProxyTimeout', proxyTunnelTimeout);
|
||||
cleanup();
|
||||
const err = new ERR_PROXY_TUNNEL(`Connection to establish proxy tunnel timed out after ${proxyTunnelTimeout}ms`);
|
||||
err.proxyTunnelTimeout = proxyTunnelTimeout;
|
||||
afterSocket(err, socket);
|
||||
}
|
||||
|
||||
if (proxyTunnelTimeout && proxyTunnelTimeout > 0) {
|
||||
debug('proxy tunnel setTimeout', proxyTunnelTimeout);
|
||||
socket.setTimeout(proxyTunnelTimeout, onProxyTimeout);
|
||||
}
|
||||
|
||||
socket.on('error', onProxyError);
|
||||
socket.on('end', onProxyEnd);
|
||||
socket.write(proxyTunnelPayload);
|
||||
|
||||
read();
|
||||
}
|
||||
|
||||
// HTTPS agents.
|
||||
|
||||
function createConnection(port, host, options) {
|
||||
if (port !== null && typeof port === 'object') {
|
||||
options = port;
|
||||
} else if (host !== null && typeof host === 'object') {
|
||||
options = { ...host };
|
||||
} else if (options === null || typeof options !== 'object') {
|
||||
// See ProxyConfig in internal/http.js for how the connection should be handled
|
||||
// when the agent is configured to use a proxy server.
|
||||
function createConnection(...args) {
|
||||
// XXX: This signature (port, host, options) is different from all the other
|
||||
// createConnection() methods.
|
||||
let options, cb;
|
||||
if (args[0] !== null && typeof args[0] === 'object') {
|
||||
options = args[0];
|
||||
} else if (args[1] !== null && typeof args[1] === 'object') {
|
||||
options = { ...args[1] };
|
||||
} else if (args[2] === null || typeof args[2] !== 'object') {
|
||||
options = {};
|
||||
} else {
|
||||
options = { ...options };
|
||||
options = { ...args[2] };
|
||||
}
|
||||
|
||||
if (typeof port === 'number') {
|
||||
options.port = port;
|
||||
if (typeof args[0] === 'number') {
|
||||
options.port = args[0];
|
||||
}
|
||||
|
||||
if (typeof host === 'string') {
|
||||
options.host = host;
|
||||
if (typeof args[1] === 'string') {
|
||||
options.host = args[1];
|
||||
}
|
||||
if (typeof args[args.length - 1] === 'function') {
|
||||
cb = args[args.length - 1];
|
||||
}
|
||||
|
||||
debug('createConnection', options);
|
||||
|
|
@ -170,7 +354,61 @@ function createConnection(port, host, options) {
|
|||
}
|
||||
}
|
||||
|
||||
const socket = tls.connect(options);
|
||||
let socket;
|
||||
const tunnelConfig = getTunnelConfigForProxiedHttps(this, options);
|
||||
debug(`https createConnection should use proxy for ${options.host}:${options.port}:`, tunnelConfig);
|
||||
|
||||
if (!tunnelConfig) {
|
||||
socket = tls.connect(options);
|
||||
} else {
|
||||
const connectOptions = {
|
||||
...this[kProxyConfig].proxyConnectionOptions,
|
||||
};
|
||||
debug('Create proxy socket', connectOptions);
|
||||
const onError = (err) => {
|
||||
cleanupAndPropagate(err, socket);
|
||||
};
|
||||
const proxyTunnelTimeout = tunnelConfig.requestOptions.timeout;
|
||||
const onTimeout = () => {
|
||||
const err = new ERR_PROXY_TUNNEL(`Connection to establish proxy tunnel timed out after ${proxyTunnelTimeout}ms`);
|
||||
err.proxyTunnelTimeout = proxyTunnelTimeout;
|
||||
cleanupAndPropagate(err, socket);
|
||||
};
|
||||
const cleanupAndPropagate = once((err, currentSocket) => {
|
||||
debug('cleanupAndPropagate', err);
|
||||
socket.removeListener('error', onError);
|
||||
socket.removeListener('timeout', onTimeout);
|
||||
// An error occurred during tunnel establishment, in that case just destroy the socket.
|
||||
// and propagate the error to the callback.
|
||||
|
||||
// When the error comes from unexpected status code, the stream is still in good shape,
|
||||
// in that case let req.onSocket handle the destruction instead.
|
||||
if (err && err.code === 'ERR_PROXY_TUNNEL' && !err.statusCode) {
|
||||
socket.destroy();
|
||||
}
|
||||
// This error should go to:
|
||||
// -> oncreate in Agent.prototype.createSocket
|
||||
// -> closure in Agent.prototype.addRequest or Agent.prototype.removeSocket
|
||||
if (cb) {
|
||||
cb(err, currentSocket);
|
||||
}
|
||||
});
|
||||
const onProxyConnection = () => {
|
||||
socket.removeListener('error', onError);
|
||||
establishTunnel(this, socket, options, tunnelConfig, cleanupAndPropagate);
|
||||
};
|
||||
if (this[kProxyConfig].protocol === 'http:') {
|
||||
socket = net.connect(connectOptions, onProxyConnection);
|
||||
} else {
|
||||
socket = tls.connect(connectOptions, onProxyConnection);
|
||||
}
|
||||
|
||||
socket.on('error', onError);
|
||||
if (proxyTunnelTimeout) {
|
||||
socket.setTimeout(proxyTunnelTimeout, onTimeout);
|
||||
}
|
||||
socket[kWaitForProxyTunnel] = true;
|
||||
}
|
||||
|
||||
if (options._agentKey) {
|
||||
// Cache new session for reuse
|
||||
|
|
@ -200,6 +438,9 @@ function createConnection(port, host, options) {
|
|||
* timeout?: number;
|
||||
* maxCachedSessions?: number;
|
||||
* servername?: string;
|
||||
* defaultPort?: number;
|
||||
* protocol?: string;
|
||||
* proxyEnv?: object;
|
||||
* }} [options]
|
||||
* @constructor
|
||||
*/
|
||||
|
|
@ -207,9 +448,11 @@ function Agent(options) {
|
|||
if (!(this instanceof Agent))
|
||||
return new Agent(options);
|
||||
|
||||
options = { __proto__: null, ...options };
|
||||
options.defaultPort ??= 443;
|
||||
options.protocol ??= 'https:';
|
||||
FunctionPrototypeCall(HttpAgent, this, options);
|
||||
this.defaultPort = 443;
|
||||
this.protocol = 'https:';
|
||||
|
||||
this.maxCachedSessions = this.options.maxCachedSessions;
|
||||
if (this.maxCachedSessions === undefined)
|
||||
this.maxCachedSessions = 100;
|
||||
|
|
@ -354,7 +597,10 @@ Agent.prototype._evictSession = function _evictSession(key) {
|
|||
delete this._sessionCache.map[key];
|
||||
};
|
||||
|
||||
const globalAgent = new Agent({ keepAlive: true, scheduling: 'lifo', timeout: 5000 });
|
||||
const globalAgent = new Agent({
|
||||
keepAlive: true, scheduling: 'lifo', timeout: 5000,
|
||||
proxyEnv: process.env.NODE_USE_ENV_PROXY ? filterEnvForProxies(process.env) : undefined,
|
||||
});
|
||||
|
||||
/**
|
||||
* Makes a request to a secure web server.
|
||||
|
|
|
|||
|
|
@ -1669,6 +1669,8 @@ E('ERR_PARSE_ARGS_UNKNOWN_OPTION', (option, allowPositionals) => {
|
|||
E('ERR_PERFORMANCE_INVALID_TIMESTAMP',
|
||||
'%d is not a valid timestamp', TypeError);
|
||||
E('ERR_PERFORMANCE_MEASURE_INVALID_OPTIONS', '%s', TypeError);
|
||||
E('ERR_PROXY_INVALID_CONFIG', '%s', Error);
|
||||
E('ERR_PROXY_TUNNEL', '%s', Error);
|
||||
E('ERR_QUIC_APPLICATION_ERROR', 'A QUIC application error occurred. %d [%s]', Error);
|
||||
E('ERR_QUIC_CONNECTION_FAILED', 'QUIC connection failed', Error);
|
||||
E('ERR_QUIC_ENDPOINT_CLOSED', 'QUIC endpoint closed: %s (%d)', Error);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
const {
|
||||
Date,
|
||||
NumberParseInt,
|
||||
Symbol,
|
||||
decodeURIComponent,
|
||||
} = primordials;
|
||||
|
||||
const { setUnrefTimeout } = require('internal/timers');
|
||||
|
|
@ -12,6 +14,10 @@ const {
|
|||
CHAR_LOWERCASE_E,
|
||||
} = require('internal/constants');
|
||||
|
||||
const { URL } = require('internal/url');
|
||||
const { Buffer } = require('buffer');
|
||||
const { isIPv4 } = require('internal/net');
|
||||
const { ERR_PROXY_INVALID_CONFIG } = require('internal/errors').codes;
|
||||
let utcCache;
|
||||
|
||||
function utcDate() {
|
||||
|
|
@ -51,12 +57,198 @@ function traceEnd(...args) {
|
|||
trace(CHAR_LOWERCASE_E, traceEventCategory, ...args);
|
||||
}
|
||||
|
||||
function ipToInt(ip) {
|
||||
const octets = ip.split('.');
|
||||
let result = 0;
|
||||
for (let i = 0; i < octets.length; i++) {
|
||||
result = (result << 8) + NumberParseInt(octets[i]);
|
||||
}
|
||||
// Force unsigned 32-bit result
|
||||
return result >>> 0;
|
||||
}
|
||||
|
||||
// There are two factors in play when proxying the request:
|
||||
// 1. What the request protocol is, that is, whether users are sending it via
|
||||
// http.request or https.request, or whether they are sending
|
||||
// the request to a https:// URL or a http:// URL. HTTPS requests should be
|
||||
// proxied by the proxy specified using the HTTPS_PROXY environment variable.
|
||||
// HTTP requests should be proxied by the proxy specified using the HTTP_PROXY
|
||||
// environment variable.
|
||||
// 2. What the proxy protocol is. This depends on the value of the environment variables,
|
||||
// for example.
|
||||
//
|
||||
// When proxying a HTTP request, the following needs to be done:
|
||||
// https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.2
|
||||
// 1. Rewrite the request path to absolute-form.
|
||||
// 2. Add proxy-connection and proxy-authorization headers appropriately.
|
||||
//
|
||||
// When proxying a HTTPS request, the following needs to be done:
|
||||
// https://datatracker.ietf.org/doc/html/rfc9110#CONNECT
|
||||
// 1. Send a CONNECT request to the proxy server.
|
||||
// 2. Wait for 200 connection established response to establish the tunnel.
|
||||
// 3. Perform TLS handshake with the endpoint through the tunnel.
|
||||
// 4. Tunnel the request using the established connection.
|
||||
//
|
||||
// When the proxy protocol is HTTP, the modified HTTP request can just be sent over
|
||||
// the TCP socket to the proxy server, and the HTTPS request tunnel can be established
|
||||
// over the TCP socket to the proxy server.
|
||||
// When the proxy protocol is HTTPS, the modified request needs to be sent after
|
||||
// TLS handshake with the proxy server. Same goes to the HTTPS request tunnel establishment.
|
||||
|
||||
/**
|
||||
* Represents the proxy configuration for an agent. The built-in http and https agent
|
||||
* implementation have one of this when they are configured to use a proxy.
|
||||
* @property {string} href - Full URL of the proxy server.
|
||||
* @property {string} host - Full host including port, e.g. 'localhost:8080'.
|
||||
* @property {string} hostname - Hostname without brackets for IPv6 addresses.
|
||||
* @property {number} port - Port number of the proxy server.
|
||||
* @property {string} protocol - Protocol of the proxy server, e.g. 'http:' or 'https:'.
|
||||
* @property {string|undefined} auth - proxy-authorization header value, if username or password is provided.
|
||||
* @property {Array<string>} bypassList - List of hosts to bypass the proxy.
|
||||
* @property {object} proxyConnectionOptions - Options for connecting to the proxy server.
|
||||
*/
|
||||
class ProxyConfig {
|
||||
constructor(proxyUrl, keepAlive, noProxyList) {
|
||||
const { host, hostname, port, protocol, username, password } = new URL(proxyUrl);
|
||||
this.href = proxyUrl; // Full URL of the proxy server.
|
||||
this.host = host; // Full host including port, e.g. 'localhost:8080'.
|
||||
this.hostname = hostname.replace(/^\[|\]$/g, ''); // Trim off the brackets from IPv6 addresses.
|
||||
this.port = port ? NumberParseInt(port, 10) : (protocol === 'https:' ? 443 : 80);
|
||||
this.protocol = protocol; // Protocol of the proxy server, e.g. 'http:' or 'https:'.
|
||||
|
||||
if (username || password) {
|
||||
// If username or password is provided, prepare the proxy-authorization header.
|
||||
const auth = `${decodeURIComponent(username)}:${decodeURIComponent(password)}`;
|
||||
this.auth = `Basic ${Buffer.from(auth).toString('base64')}`;
|
||||
}
|
||||
if (noProxyList) {
|
||||
this.bypassList = noProxyList.split(',').map((entry) => entry.trim().toLowerCase());
|
||||
} else {
|
||||
this.bypassList = []; // No bypass list provided.
|
||||
}
|
||||
this.proxyConnectionOptions = {
|
||||
host: this.hostname,
|
||||
port: this.port,
|
||||
};
|
||||
}
|
||||
|
||||
// See: https://about.gitlab.com/blog/we-need-to-talk-no-proxy
|
||||
// TODO(joyeecheung): share code with undici.
|
||||
shouldUseProxy(hostname, port) {
|
||||
const bypassList = this.bypassList;
|
||||
if (this.bypassList.length === 0) {
|
||||
return true; // No bypass list, always use the proxy.
|
||||
}
|
||||
|
||||
const host = hostname.toLowerCase();
|
||||
const hostWithPort = port ? `${host}:${port}` : host;
|
||||
|
||||
for (let i = 0; i < bypassList.length; i++) {
|
||||
const entry = bypassList[i];
|
||||
|
||||
if (entry === '*') return false; // * bypasses all hosts.
|
||||
if (entry === host || entry === hostWithPort) return false; // Matching host and host:port
|
||||
|
||||
// Follow curl's behavior: strip leading dot before matching suffixes.
|
||||
if (entry.startsWith('.')) {
|
||||
const suffix = entry.substring(1);
|
||||
if (host.endsWith(suffix)) return false;
|
||||
}
|
||||
|
||||
// Handle wildcards like *.example.com
|
||||
if (entry.startsWith('*.') && host.endsWith(entry.substring(1))) return false;
|
||||
|
||||
// Handle IP ranges (simple format like 192.168.1.0-192.168.1.255)
|
||||
// TODO(joyeecheung): support IPv6.
|
||||
if (entry.includes('-') && isIPv4(host)) {
|
||||
let { 0: startIP, 1: endIP } = entry.split('-');
|
||||
startIP = startIP.trim();
|
||||
endIP = endIP.trim();
|
||||
if (startIP && endIP && isIPv4(startIP) && isIPv4(endIP)) {
|
||||
const hostInt = ipToInt(host);
|
||||
const startInt = ipToInt(startIP);
|
||||
const endInt = ipToInt(endIP);
|
||||
if (hostInt >= startInt && hostInt <= endInt) return false;
|
||||
}
|
||||
}
|
||||
|
||||
// It might be useful to support CIDR notation, but it's not so widely supported
|
||||
// in other tools as a de-facto standard to follow, so we don't implement it for now.
|
||||
}
|
||||
|
||||
return true; // If no matches found, use the proxy.
|
||||
}
|
||||
}
|
||||
|
||||
function parseProxyConfigFromEnv(env, protocol, keepAlive) {
|
||||
// We only support proxying for HTTP and HTTPS requests.
|
||||
if (protocol !== 'http:' && protocol !== 'https:') {
|
||||
return null;
|
||||
}
|
||||
// Get the proxy url - following the most popular convention, lower case takes precedence.
|
||||
// See https://about.gitlab.com/blog/we-need-to-talk-no-proxy/#http_proxy-and-https_proxy
|
||||
const proxyUrl = (protocol === 'https:') ?
|
||||
(env.https_proxy || env.HTTPS_PROXY) : (env.http_proxy || env.HTTP_PROXY);
|
||||
// No proxy settings from the environment, ignore.
|
||||
if (!proxyUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (proxyUrl.includes('\r') || proxyUrl.includes('\n')) {
|
||||
throw new ERR_PROXY_INVALID_CONFIG(`Invalid proxy URL: ${proxyUrl}`);
|
||||
}
|
||||
|
||||
// Only http:// and https:// proxies are supported.
|
||||
// Ignore instead of throw, in case other protocols are supposed to be
|
||||
// handled by the user land.
|
||||
if (!proxyUrl.startsWith('http://') && !proxyUrl.startsWith('https://')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const noProxyList = env.no_proxy || env.NO_PROXY;
|
||||
return new ProxyConfig(proxyUrl, keepAlive, noProxyList);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ProxyConfig} proxyConfig
|
||||
* @param {object} reqOptions
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function checkShouldUseProxy(proxyConfig, reqOptions) {
|
||||
if (!proxyConfig) {
|
||||
return false;
|
||||
}
|
||||
if (reqOptions.socketPath) {
|
||||
// If socketPath is set, the endpoint is a Unix domain socket, which can't
|
||||
// be proxied.
|
||||
return false;
|
||||
}
|
||||
return proxyConfig.shouldUseProxy(reqOptions.host || 'localhost', reqOptions.port);
|
||||
}
|
||||
|
||||
function filterEnvForProxies(env) {
|
||||
return {
|
||||
__proto__: null,
|
||||
http_proxy: env.http_proxy,
|
||||
HTTP_PROXY: env.HTTP_PROXY,
|
||||
https_proxy: env.https_proxy,
|
||||
HTTPS_PROXY: env.HTTPS_PROXY,
|
||||
no_proxy: env.no_proxy,
|
||||
NO_PROXY: env.NO_PROXY,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
kOutHeaders: Symbol('kOutHeaders'),
|
||||
kNeedDrain: Symbol('kNeedDrain'),
|
||||
kProxyConfig: Symbol('kProxyConfig'),
|
||||
kWaitForProxyTunnel: Symbol('kWaitForProxyTunnel'),
|
||||
checkShouldUseProxy,
|
||||
parseProxyConfigFromEnv,
|
||||
utcDate,
|
||||
traceBegin,
|
||||
traceEnd,
|
||||
getNextTraceEventId,
|
||||
isTraceHTTPEnabled,
|
||||
filterEnvForProxies,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -172,8 +172,9 @@ function setupHttpProxy() {
|
|||
const { setGlobalDispatcher, EnvHttpProxyAgent } = require('internal/deps/undici/undici');
|
||||
const envHttpProxyAgent = new EnvHttpProxyAgent();
|
||||
setGlobalDispatcher(envHttpProxyAgent);
|
||||
// TODO(joyeecheung): This currently only affects fetch. Implement handling in the
|
||||
// http/https Agent constructor too.
|
||||
// For fetch, we need to set the global dispatcher from here.
|
||||
// For http/https agents, we'll configure the global agent when they are
|
||||
// actually created, in lib/_http_agent.js and lib/https.js.
|
||||
// TODO(joyeecheung): This is currently guarded with NODE_USE_ENV_PROXY. Investigate whether
|
||||
// it's possible to enable it by default without stepping on other existing libraries that
|
||||
// sets the global dispatcher or monkey patches the global agent.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
// This tests that when the proxy server connection is refused, the client can
|
||||
// handle it correctly.
|
||||
|
||||
import * as common from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
import http from 'node:http';
|
||||
import { once } from 'events';
|
||||
import { runProxiedRequest } from '../common/proxy-server.js';
|
||||
import dgram from 'node:dgram';
|
||||
|
||||
const server = http.createServer(common.mustNotCall());
|
||||
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
|
||||
server.listen(0);
|
||||
await once(server, 'listening');
|
||||
|
||||
const serverHost = `localhost:${server.address().port}`;
|
||||
const requestUrl = `http://${serverHost}/test`;
|
||||
|
||||
// Make it fail on connection refused by connecting to a UDP port with TCP.
|
||||
const udp = dgram.createSocket('udp4');
|
||||
udp.bind(0, '127.0.0.1');
|
||||
await once(udp, 'listening');
|
||||
|
||||
const port = udp.address().port;
|
||||
|
||||
const { code, signal, stderr, stdout } = await runProxiedRequest({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: requestUrl,
|
||||
HTTP_PROXY: `http://localhost:${port}`,
|
||||
});
|
||||
|
||||
// The proxy client should get a connection refused error.
|
||||
assert.match(stderr, /Error.*connect ECONNREFUSED/);
|
||||
assert.strictEqual(stdout.trim(), '');
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
|
||||
server.close();
|
||||
udp.close();
|
||||
54
test/client-proxy/test-http-proxy-request-https-proxy.mjs
Normal file
54
test/client-proxy/test-http-proxy-request-https-proxy.mjs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
// This tests that when the `NODE_USE_ENV_PROXY` environment variable is set to 1, Node.js
|
||||
// correctly uses the `HTTP_PROXY` or `http_proxy` environment variable to proxy HTTP requests
|
||||
// via request rewriting when the proxy server itself uses HTTPS.
|
||||
|
||||
import * as common from '../common/index.mjs';
|
||||
import fixtures from '../common/fixtures.js';
|
||||
import assert from 'node:assert';
|
||||
import http from 'node:http';
|
||||
import { once } from 'events';
|
||||
import { createProxyServer, runProxiedRequest } from '../common/proxy-server.js';
|
||||
|
||||
if (!common.hasCrypto) {
|
||||
common.skip('missing crypto');
|
||||
}
|
||||
|
||||
// Start a server to process the final request.
|
||||
const server = http.createServer(common.mustCall((req, res) => {
|
||||
res.end('Hello world');
|
||||
}));
|
||||
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
|
||||
server.listen(0);
|
||||
await once(server, 'listening');
|
||||
|
||||
// Start an HTTPS proxy server.
|
||||
const { proxy, logs } = createProxyServer({ https: true });
|
||||
proxy.listen(0);
|
||||
await once(proxy, 'listening');
|
||||
|
||||
const serverHost = `localhost:${server.address().port}`;
|
||||
const requestUrl = `http://${serverHost}/test`;
|
||||
const expectedLogs = [{
|
||||
method: 'GET',
|
||||
url: requestUrl,
|
||||
headers: {
|
||||
'connection': 'keep-alive',
|
||||
'proxy-connection': 'keep-alive',
|
||||
'host': serverHost,
|
||||
},
|
||||
}];
|
||||
|
||||
const { code, signal, stderr, stdout } = await runProxiedRequest({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: requestUrl,
|
||||
HTTP_PROXY: `https://localhost:${proxy.address().port}`,
|
||||
NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'fake-startcom-root-cert.pem'),
|
||||
});
|
||||
assert.deepStrictEqual(logs, expectedLogs);
|
||||
assert.strictEqual(stderr.trim(), '');
|
||||
assert.match(stdout, /Hello world/);
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
|
||||
proxy.close();
|
||||
server.close();
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
// This tests that invalid hosts or ports with carriage return or newline characters
|
||||
// in HTTP request options would lead to ERR_INVALID_CHAR.
|
||||
|
||||
import * as common from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
import http from 'node:http';
|
||||
import { once } from 'events';
|
||||
|
||||
const server = http.createServer(common.mustNotCall());
|
||||
server.listen(0);
|
||||
await once(server, 'listening');
|
||||
server.on('error', common.mustNotCall());
|
||||
const port = server.address().port.toString();
|
||||
|
||||
const testCases = [
|
||||
{ host: 'local\rhost', port: port, path: '/carriage-return-in-host' },
|
||||
{ host: 'local\nhost', port: port, path: '/newline-in-host' },
|
||||
{ host: 'local\r\nhost', port: port, path: '/crlf-in-host' },
|
||||
{ host: 'localhost', port: port.substring(0, 1) + '\r' + port.substring(1), path: '/carriage-return-in-port' },
|
||||
{ host: 'localhost', port: port.substring(0, 1) + '\n' + port.substring(1), path: '/newline-in-port' },
|
||||
{ host: 'localhost', port: port.substring(0, 1) + '\r\n' + port.substring(1), path: '/crlf-in-port' },
|
||||
];
|
||||
|
||||
const proxy = http.createServer(common.mustNotCall());
|
||||
proxy.listen(0);
|
||||
await once(proxy, 'listening');
|
||||
|
||||
const agent = new http.Agent({
|
||||
proxyEnv: {
|
||||
HTTP_PROXY: `http://localhost:${proxy.address().port}`,
|
||||
},
|
||||
});
|
||||
|
||||
for (const testCase of testCases) {
|
||||
const options = { ...testCase, agent };
|
||||
assert.throws(() => {
|
||||
http.request(options, common.mustNotCall());
|
||||
}, {
|
||||
code: 'ERR_INVALID_CHAR',
|
||||
});
|
||||
}
|
||||
|
||||
server.close();
|
||||
proxy.close();
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
// This tests that invalid hosts or ports with carriage return or newline characters
|
||||
// in HTTP request urls are stripped away before being sent to the server.
|
||||
|
||||
import * as common from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
import http from 'node:http';
|
||||
import { once } from 'events';
|
||||
import { inspect } from 'node:util';
|
||||
import fixtures from '../common/fixtures.js';
|
||||
import { Worker } from 'node:worker_threads';
|
||||
|
||||
const expectedProxyLogs = new Set();
|
||||
const proxyWorker = new Worker(fixtures.path('proxy-server-worker.js'));
|
||||
|
||||
proxyWorker.on('message', (message) => {
|
||||
console.log('Received message from worker:', message.type);
|
||||
if (message.type === 'proxy-listening') {
|
||||
startTest(message.port);
|
||||
} else if (message.type === 'proxy-stopped') {
|
||||
assert.deepStrictEqual(new Set(message.logs), expectedProxyLogs);
|
||||
// Close the server after the proxy is stopped.
|
||||
proxyWorker.terminate();
|
||||
}
|
||||
});
|
||||
|
||||
const requests = new Set();
|
||||
// Create a server that records the requests it gets.
|
||||
const server = http.createServer((req, res) => {
|
||||
requests.add(`http://localhost:${server.address().port}${req.url}`);
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
res.end(`Response for ${req.url}`);
|
||||
});
|
||||
proxyWorker.on('exit', common.mustCall(() => {
|
||||
server.close();
|
||||
}));
|
||||
|
||||
async function startTest(proxyPort) {
|
||||
// Start a minimal proxy server in a worker, we don't do it in this thread
|
||||
// because we'll mutate the global http agent here.
|
||||
http.globalAgent = new http.Agent({
|
||||
proxyEnv: {
|
||||
HTTP_PROXY: `http://localhost:${proxyPort}`,
|
||||
},
|
||||
});
|
||||
|
||||
server.listen(0);
|
||||
await once(server, 'listening');
|
||||
server.on('error', common.mustNotCall());
|
||||
const port = server.address().port.toString();
|
||||
|
||||
const testCases = [
|
||||
{ host: 'local\rhost', port: port, path: '/carriage-return-in-host' },
|
||||
{ host: 'local\nhost', port: port, path: '/newline-in-host' },
|
||||
{ host: 'local\r\nhost', port: port, path: '/crlf-in-host' },
|
||||
{ host: 'localhost', port: port.substring(0, 1) + '\r' + port.substring(1), path: '/carriage-return-in-port' },
|
||||
{ host: 'localhost', port: port.substring(0, 1) + '\n' + port.substring(1), path: '/newline-in-port' },
|
||||
{ host: 'localhost', port: port.substring(0, 1) + '\r\n' + port.substring(1), path: '/crlf-in-port' },
|
||||
];
|
||||
const severHost = `localhost:${server.address().port}`;
|
||||
|
||||
let counter = testCases.length;
|
||||
const expectedUrls = new Set();
|
||||
for (const testCase of testCases) {
|
||||
const url = `http://${testCase.host}:${testCase.port}${testCase.path}`;
|
||||
// The invalid characters should all be stripped away before being sent.
|
||||
const cleanUrl = url.replaceAll(/\r|\n/g, '');
|
||||
expectedUrls.add(cleanUrl);
|
||||
expectedProxyLogs.add({
|
||||
method: 'GET',
|
||||
url: cleanUrl,
|
||||
headers: {
|
||||
'host': severHost,
|
||||
'connection': 'close',
|
||||
'proxy-connection': 'close',
|
||||
},
|
||||
});
|
||||
http.request(url, (res) => {
|
||||
res.on('error', common.mustNotCall());
|
||||
res.setEncoding('utf8');
|
||||
res.on('data', () => {});
|
||||
res.on('end', common.mustCall(() => {
|
||||
console.log(`#${counter--} ended response for: ${inspect(url)}`);
|
||||
// Finished all test cases.
|
||||
if (counter === 0) {
|
||||
// Check that the requests received by the server have sanitized URLs.
|
||||
assert.deepStrictEqual(requests, expectedUrls);
|
||||
console.log('Sending stop-proxy message to worker');
|
||||
proxyWorker.postMessage({ type: 'stop-proxy' });
|
||||
}
|
||||
}));
|
||||
}).on('error', common.mustNotCall()).end();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
// This tests that proxy URLs with invalid credentials (containing \r or \n characters)
|
||||
// are rejected with an appropriate error.
|
||||
import '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
import http from 'node:http';
|
||||
|
||||
const testCases = [
|
||||
{
|
||||
name: 'carriage return in username',
|
||||
proxyUrl: 'http://user\r:pass@proxy.example.com:8080',
|
||||
expectedError: { code: 'ERR_PROXY_INVALID_CONFIG', message: /Invalid proxy URL/ },
|
||||
},
|
||||
{
|
||||
name: 'newline in username',
|
||||
proxyUrl: 'http://user\n:pass@proxy.example.com:8080',
|
||||
expectedError: { code: 'ERR_PROXY_INVALID_CONFIG', message: /Invalid proxy URL/ },
|
||||
},
|
||||
{
|
||||
name: 'carriage return in password',
|
||||
proxyUrl: 'http://user:pass\r@proxy.example.com:8080',
|
||||
expectedError: { code: 'ERR_PROXY_INVALID_CONFIG', message: /Invalid proxy URL/ },
|
||||
},
|
||||
{
|
||||
name: 'newline in password',
|
||||
proxyUrl: 'http://user:pass\n@proxy.example.com:8080',
|
||||
expectedError: { code: 'ERR_PROXY_INVALID_CONFIG', message: /Invalid proxy URL/ },
|
||||
},
|
||||
{
|
||||
name: 'CRLF injection attempt in username',
|
||||
proxyUrl: 'http://user\r\nHost: example.com:pass@proxy.example.com:8080',
|
||||
expectedError: { code: 'ERR_PROXY_INVALID_CONFIG', message: /Invalid proxy URL/ },
|
||||
},
|
||||
{
|
||||
name: 'CRLF injection attempt in password',
|
||||
proxyUrl: 'http://user:pass\r\nHost: example.com@proxy.example.com:8080',
|
||||
expectedError: { code: 'ERR_PROXY_INVALID_CONFIG', message: /Invalid proxy URL/ },
|
||||
},
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
// Test that creating an agent with invalid proxy credentials throws an error
|
||||
assert.throws(() => {
|
||||
new http.Agent({
|
||||
proxyEnv: {
|
||||
HTTP_PROXY: testCase.proxyUrl,
|
||||
},
|
||||
});
|
||||
}, testCase.expectedError);
|
||||
}
|
||||
30
test/client-proxy/test-http-proxy-request-invalid-url.mjs
Normal file
30
test/client-proxy/test-http-proxy-request-invalid-url.mjs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// This tests that invalid proxy URLs are handled correctly.
|
||||
import * as common from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
import { once } from 'events';
|
||||
import http from 'node:http';
|
||||
import { runProxiedRequest } from '../common/proxy-server.js';
|
||||
|
||||
// Start a target server that should not be reached.
|
||||
const server = http.createServer(common.mustNotCall());
|
||||
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
|
||||
server.listen(0);
|
||||
await once(server, 'listening');
|
||||
|
||||
const serverHost = `localhost:${server.address().port}`;
|
||||
const requestUrl = `http://${serverHost}/test`;
|
||||
|
||||
// Test invalid proxy URL
|
||||
const { code, signal, stderr, stdout } = await runProxiedRequest({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: requestUrl,
|
||||
HTTP_PROXY: 'not-a-valid-url',
|
||||
});
|
||||
|
||||
// Should get an error about invalid URL
|
||||
assert.match(stderr, /TypeError.*Invalid URL/);
|
||||
assert.strictEqual(stdout.trim(), '');
|
||||
assert.strictEqual(code, 1);
|
||||
assert.strictEqual(signal, null);
|
||||
|
||||
server.close();
|
||||
123
test/client-proxy/test-http-proxy-request-max-sockets.mjs
Normal file
123
test/client-proxy/test-http-proxy-request-max-sockets.mjs
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
// This tests that when using a proxy with an agent with maxSockets: 1,
|
||||
// subsequent requests are queued when the first request is still alive,
|
||||
// and processed after the first request completes, and both are sending
|
||||
// the request through the proxy.
|
||||
|
||||
import * as common from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
import http from 'node:http';
|
||||
import { once } from 'events';
|
||||
import { createProxyServer } from '../common/proxy-server.js';
|
||||
|
||||
let resolve;
|
||||
const p = new Promise((r) => { resolve = r; });
|
||||
|
||||
// Start a server that delays responses to test queuing behavior
|
||||
const server = http.createServer(common.mustCall((req, res) => {
|
||||
if (req.url === '/first') {
|
||||
// Simulate a long response for the first request
|
||||
p.then(() => {
|
||||
console.log('Responding to /first');
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
res.end('Response for /first');
|
||||
});
|
||||
} else if (req.url === '/second') {
|
||||
// Respond immediately for the second request
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
res.end('Response for /second');
|
||||
} else {
|
||||
assert.fail(`Unexpected request to ${req.url}`);
|
||||
}
|
||||
}, 2));
|
||||
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
|
||||
server.listen(0);
|
||||
await once(server, 'listening');
|
||||
|
||||
// Start a minimal proxy server
|
||||
const { proxy, logs } = createProxyServer();
|
||||
proxy.listen(0);
|
||||
await once(proxy, 'listening');
|
||||
|
||||
const serverHost = `localhost:${server.address().port}`;
|
||||
const proxyUrl = `http://localhost:${proxy.address().port}`;
|
||||
|
||||
// Create an agent with maxSockets: 1 and proxy support
|
||||
const agent = new http.Agent({
|
||||
maxSockets: 1,
|
||||
proxyEnv: {
|
||||
HTTP_PROXY: proxyUrl,
|
||||
},
|
||||
});
|
||||
|
||||
const requestTimes = [];
|
||||
|
||||
// Make first request that takes longer
|
||||
const firstReq = http.request({
|
||||
hostname: 'localhost',
|
||||
port: server.address().port,
|
||||
path: '/first',
|
||||
agent: agent,
|
||||
}, common.mustCall((res) => {
|
||||
console.log('req1 response received');
|
||||
let data = '';
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
res.on('end', common.mustCall(() => {
|
||||
console.log('req1 end');
|
||||
requestTimes[0] = { path: '/first', data, endTime: Date.now() };
|
||||
assert.strictEqual(data, 'Response for /first');
|
||||
}));
|
||||
}));
|
||||
|
||||
firstReq.on('socket', common.mustCall((socket) => {
|
||||
console.log('req1 socket acquired');
|
||||
// Start second request when first request gets its socket
|
||||
// so that it will be queued.
|
||||
const secondReq = http.request({
|
||||
hostname: 'localhost',
|
||||
port: server.address().port,
|
||||
path: '/second',
|
||||
agent: agent,
|
||||
}, common.mustCall((res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
res.on('end', common.mustCall(() => {
|
||||
requestTimes[1] = { path: '/second', data, endTime: Date.now() };
|
||||
assert.strictEqual(data, 'Response for /second');
|
||||
|
||||
// Verify both requests went through the proxy
|
||||
assert.deepStrictEqual(logs, [
|
||||
{
|
||||
method: 'GET',
|
||||
url: `http://${serverHost}/first`,
|
||||
headers: {
|
||||
'host': serverHost,
|
||||
'proxy-connection': 'keep-alive',
|
||||
'connection': 'keep-alive',
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
url: `http://${serverHost}/second`,
|
||||
headers: {
|
||||
'host': serverHost,
|
||||
'proxy-connection': 'keep-alive',
|
||||
'connection': 'keep-alive',
|
||||
},
|
||||
},
|
||||
]);
|
||||
proxy.close();
|
||||
server.close();
|
||||
}));
|
||||
}));
|
||||
|
||||
secondReq.on('error', common.mustNotCall());
|
||||
firstReq.end();
|
||||
secondReq.end();
|
||||
resolve(); // Tell the server to respond to the first request
|
||||
}));
|
||||
|
||||
firstReq.on('error', common.mustNotCall());
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
// This tests that NO_PROXY=* bypasses proxy for all requests.
|
||||
import * as common from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
import { once } from 'events';
|
||||
import http from 'node:http';
|
||||
import { runProxiedRequest } from '../common/proxy-server.js';
|
||||
|
||||
// Start a server to process the final request.
|
||||
const server = http.createServer(common.mustCall((req, res) => {
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
res.end('Hello World\n');
|
||||
}, 1));
|
||||
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
|
||||
server.listen(0);
|
||||
await once(server, 'listening');
|
||||
|
||||
// Start a proxy server that should NOT be used.
|
||||
const proxy = http.createServer();
|
||||
proxy.on('request', common.mustNotCall());
|
||||
proxy.listen(0);
|
||||
await once(proxy, 'listening');
|
||||
|
||||
const serverHost = `localhost:${server.address().port}`;
|
||||
const requestUrl = `http://${serverHost}/test`;
|
||||
|
||||
// Test NO_PROXY with asterisk (bypass all)
|
||||
const { code, signal, stderr, stdout } = await runProxiedRequest({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: requestUrl,
|
||||
HTTP_PROXY: `http://localhost:${proxy.address().port}`,
|
||||
NO_PROXY: '*',
|
||||
});
|
||||
|
||||
// The request should succeed and bypass proxy
|
||||
assert.match(stdout, /Status Code: 200/);
|
||||
assert.match(stdout, /Hello World/);
|
||||
assert.strictEqual(stderr.trim(), '');
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
|
||||
proxy.close();
|
||||
server.close();
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
// This tests that NO_PROXY environment variable supports domain matching.
|
||||
|
||||
import * as common from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
import { once } from 'events';
|
||||
import http from 'node:http';
|
||||
import { runProxiedRequest } from '../common/proxy-server.js';
|
||||
|
||||
// Start a server to process the final request.
|
||||
const server = http.createServer(common.mustCall((req, res) => {
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
res.end('Hello World\n');
|
||||
}, 3));
|
||||
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
|
||||
server.listen(0, '127.0.0.1');
|
||||
await once(server, 'listening');
|
||||
|
||||
// Start a proxy server that should NOT be used.
|
||||
const proxy = http.createServer();
|
||||
proxy.on('request', common.mustNotCall());
|
||||
proxy.listen(0);
|
||||
await once(proxy, 'listening');
|
||||
|
||||
{
|
||||
// Test NO_PROXY with exact domain match.
|
||||
const { code, signal, stderr, stdout } = await runProxiedRequest({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: `http://test.example.com:${server.address().port}/test`,
|
||||
HTTP_PROXY: `http://localhost:${proxy.address().port}`,
|
||||
RESOLVE_TO_LOCALHOST: 'test.example.com',
|
||||
NO_PROXY: 'test.example.com',
|
||||
});
|
||||
|
||||
// The request should succeed and bypass proxy.
|
||||
assert.match(stdout, /Status Code: 200/);
|
||||
assert.match(stdout, /Hello World/);
|
||||
assert.match(stdout, /Resolving lookup for test\.example\.com/);
|
||||
assert.strictEqual(stderr.trim(), '');
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
}
|
||||
|
||||
{
|
||||
// Test NO_PROXY with wildcard match.
|
||||
const { code, signal, stderr, stdout } = await runProxiedRequest({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: `http://test.example.com:${server.address().port}/test`,
|
||||
HTTP_PROXY: `http://localhost:${proxy.address().port}`,
|
||||
RESOLVE_TO_LOCALHOST: 'test.example.com',
|
||||
NO_PROXY: '*.example.com',
|
||||
});
|
||||
|
||||
// The request should succeed and bypass proxy.
|
||||
assert.match(stdout, /Status Code: 200/);
|
||||
assert.match(stdout, /Hello World/);
|
||||
assert.match(stdout, /Resolving lookup for test\.example\.com/);
|
||||
assert.strictEqual(stderr.trim(), '');
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
}
|
||||
|
||||
// Test NO_PROXY with domain suffix match.
|
||||
{
|
||||
const { code, signal, stderr, stdout } = await runProxiedRequest({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: `http://test.example.com:${server.address().port}/test`,
|
||||
HTTP_PROXY: `http://localhost:${proxy.address().port}`,
|
||||
RESOLVE_TO_LOCALHOST: 'test.example.com',
|
||||
NO_PROXY: '.example.com',
|
||||
});
|
||||
|
||||
// The request should succeed and bypass proxy
|
||||
assert.match(stdout, /Status Code: 200/);
|
||||
assert.match(stdout, /Hello World/);
|
||||
assert.match(stdout, /Resolving lookup for test\.example\.com/);
|
||||
assert.strictEqual(stderr.trim(), '');
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
}
|
||||
proxy.close();
|
||||
server.close();
|
||||
59
test/client-proxy/test-http-proxy-request-no-proxy-ip.mjs
Normal file
59
test/client-proxy/test-http-proxy-request-no-proxy-ip.mjs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
// This tests that NO_PROXY environment variable supports IP matches.
|
||||
|
||||
import * as common from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
import { once } from 'events';
|
||||
import http from 'node:http';
|
||||
import { runProxiedRequest } from '../common/proxy-server.js';
|
||||
|
||||
// Start a server to process the final request.
|
||||
const server = http.createServer(common.mustCall((req, res) => {
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
res.end('Hello World\n');
|
||||
}, 2));
|
||||
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
|
||||
server.listen(0, '127.0.0.1');
|
||||
await once(server, 'listening');
|
||||
|
||||
// Start a proxy server that should NOT be used.
|
||||
const proxy = http.createServer();
|
||||
proxy.on('request', common.mustNotCall());
|
||||
proxy.listen(0);
|
||||
await once(proxy, 'listening');
|
||||
|
||||
// Test NO_PROXY with exact IP.
|
||||
{
|
||||
const { code, signal, stderr, stdout } = await runProxiedRequest({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: `http://127.0.0.1:${server.address().port}/test`,
|
||||
HTTP_PROXY: `http://localhost:${proxy.address().port}`,
|
||||
NO_PROXY: '127.0.0.1',
|
||||
});
|
||||
|
||||
// The request should succeed and bypass proxy
|
||||
assert.match(stdout, /Status Code: 200/);
|
||||
assert.match(stdout, /Hello World/);
|
||||
assert.strictEqual(stderr.trim(), '');
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
}
|
||||
|
||||
// Test NO_PROXY with IP range (127.0.0.1-127.0.0.255 includes 127.0.0.1)
|
||||
{
|
||||
const { code, signal, stderr, stdout } = await runProxiedRequest({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: `http://127.0.0.1:${server.address().port}/test`,
|
||||
HTTP_PROXY: `http://localhost:${proxy.address().port}`,
|
||||
NO_PROXY: '127.0.0.1-127.0.0.255',
|
||||
});
|
||||
|
||||
// The request should succeed and bypass proxy
|
||||
assert.match(stdout, /Status Code: 200/);
|
||||
assert.match(stdout, /Hello World/);
|
||||
assert.strictEqual(stderr.trim(), '');
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
}
|
||||
|
||||
proxy.close();
|
||||
server.close();
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
// This tests that NO_PROXY environment variable supports port-specific matches.
|
||||
|
||||
import * as common from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
import { once } from 'events';
|
||||
import http from 'node:http';
|
||||
import { createProxyServer, runProxiedRequest } from '../common/proxy-server.js';
|
||||
|
||||
// Start a server to process the final request.
|
||||
const server = http.createServer(common.mustCall((req, res) => {
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
res.end('Hello World\n');
|
||||
}, 1));
|
||||
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
|
||||
server.listen(0);
|
||||
await once(server, 'listening');
|
||||
|
||||
// Start a proxy server that should NOT be used.
|
||||
const { proxy, logs } = createProxyServer();
|
||||
proxy.listen(0);
|
||||
await once(proxy, 'listening');
|
||||
|
||||
// Test NO_PROXY with port-specific match correctly bypassing the proxy.
|
||||
{
|
||||
const { code, signal, stderr, stdout } = await runProxiedRequest({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: `http://localhost:${server.address().port}/test`,
|
||||
HTTP_PROXY: `http://localhost:${proxy.address().port}`,
|
||||
NO_PROXY: `localhost:${server.address().port}`,
|
||||
});
|
||||
|
||||
// The request should succeed and bypass proxy.
|
||||
assert.match(stdout, /Status Code: 200/);
|
||||
assert.match(stdout, /Hello World/);
|
||||
assert.strictEqual(stderr.trim(), '');
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
// Proxy should not have been used, so logs should be empty.
|
||||
assert.deepStrictEqual(logs, []);
|
||||
}
|
||||
|
||||
// Test NO_PROXY with port-specific mismatch does not bypass the proxy.
|
||||
{
|
||||
const server2 = http.createServer(common.mustCall((req, res) => {
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
res.end('Hello World\n');
|
||||
}, 1));
|
||||
server2.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
|
||||
server2.listen(0);
|
||||
await once(server2, 'listening');
|
||||
|
||||
const serverHost = `localhost:${server2.address().port}`;
|
||||
const { code, signal, stderr, stdout } = await runProxiedRequest({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: `http://${serverHost}/test`,
|
||||
HTTP_PROXY: `http://localhost:${proxy.address().port}`,
|
||||
NO_PROXY: `localhost:${server.address().port}`,
|
||||
});
|
||||
// The request should succeed and bypass proxy.
|
||||
assert.match(stdout, /Status Code: 200/);
|
||||
assert.match(stdout, /Hello World/);
|
||||
assert.strictEqual(stderr.trim(), '');
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
// Proxy should not have been used, so logs should be empty.
|
||||
assert.deepStrictEqual(logs, [{
|
||||
method: 'GET',
|
||||
url: `http://${serverHost}/test`,
|
||||
headers: {
|
||||
'host': serverHost,
|
||||
'proxy-connection': 'keep-alive',
|
||||
'connection': 'keep-alive',
|
||||
},
|
||||
}]);
|
||||
|
||||
server2.close();
|
||||
}
|
||||
|
||||
proxy.close();
|
||||
server.close();
|
||||
60
test/client-proxy/test-http-proxy-request-no-proxy.mjs
Normal file
60
test/client-proxy/test-http-proxy-request-no-proxy.mjs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
// This tests that NO_PROXY environment variable is respected for HTTP requests.
|
||||
|
||||
import * as common from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
import http from 'node:http';
|
||||
import { once } from 'events';
|
||||
import { runProxiedRequest } from '../common/proxy-server.js';
|
||||
|
||||
// Start a server to process the final request.
|
||||
const server = http.createServer(common.mustCall((req, res) => {
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
res.end('Hello World\n');
|
||||
}, 2));
|
||||
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
|
||||
server.listen(0);
|
||||
await once(server, 'listening');
|
||||
|
||||
// Start a proxy server that should NOT be used.
|
||||
const proxy = http.createServer();
|
||||
proxy.on('request', common.mustNotCall());
|
||||
proxy.on('connect', common.mustNotCall());
|
||||
proxy.listen(0);
|
||||
await once(proxy, 'listening');
|
||||
|
||||
// Test NO_PROXY with exact hostname match.
|
||||
{
|
||||
const { code, signal, stderr, stdout } = await runProxiedRequest({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: `http://localhost:${server.address().port}/test`,
|
||||
HTTP_PROXY: `http://localhost:${proxy.address().port}`,
|
||||
NO_PROXY: 'localhost',
|
||||
});
|
||||
|
||||
// The request should succeed and bypass proxy.
|
||||
assert.match(stdout, /Status Code: 200/);
|
||||
assert.match(stdout, /Hello World/);
|
||||
assert.strictEqual(stderr.trim(), '');
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
}
|
||||
|
||||
// Test comma-separated NO_PROXY.
|
||||
{
|
||||
const { code, signal, stderr, stdout } = await runProxiedRequest({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: `http://127.0.0.1:${server.address().port}/test`,
|
||||
HTTP_PROXY: `http://localhost:${proxy.address().port}`,
|
||||
NO_PROXY: 'localhost,127.0.0.1',
|
||||
});
|
||||
|
||||
// The request should succeed and bypass proxy.
|
||||
assert.match(stdout, /Status Code: 200/);
|
||||
assert.match(stdout, /Hello World/);
|
||||
assert.strictEqual(stderr.trim(), '');
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
}
|
||||
|
||||
proxy.close();
|
||||
server.close();
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
// This tests that when the proxy server returns a 500 response, the client can
|
||||
// handle it correctly.
|
||||
|
||||
import * as common from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
import { once } from 'events';
|
||||
import http from 'node:http';
|
||||
import { createProxyServer, runProxiedRequest } from '../common/proxy-server.js';
|
||||
|
||||
// Start a server to process the final request.
|
||||
const server = http.createServer(common.mustCall((req, res) => {
|
||||
req.destroy();
|
||||
}, 1));
|
||||
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
|
||||
server.listen(0);
|
||||
await once(server, 'listening');
|
||||
|
||||
// Start a minimal proxy server.
|
||||
const { proxy, logs } = createProxyServer();
|
||||
proxy.listen(0);
|
||||
await once(proxy, 'listening');
|
||||
|
||||
const serverHost = `localhost:${server.address().port}`;
|
||||
const requestUrl = `http://${serverHost}/test`;
|
||||
|
||||
const { code, signal, stderr, stdout } = await runProxiedRequest({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: requestUrl,
|
||||
HTTP_PROXY: `http://localhost:${proxy.address().port}`,
|
||||
});
|
||||
|
||||
// The proxy client should receive a well-formed 500 response.
|
||||
assert.match(stdout, /Proxy error ECONNRESET: socket hang up/);
|
||||
assert.match(stdout, /Status Code: 500/);
|
||||
assert.strictEqual(stderr.trim(), '');
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
|
||||
// The proxy should receive a GET request.
|
||||
assert.deepStrictEqual(logs[0], {
|
||||
method: 'GET',
|
||||
url: requestUrl,
|
||||
headers: {
|
||||
'connection': 'keep-alive',
|
||||
'proxy-connection': 'keep-alive',
|
||||
'host': serverHost,
|
||||
},
|
||||
});
|
||||
|
||||
// The proxy should receive a ECONNRESET from the target server, and then send
|
||||
// 500 to the client.
|
||||
assert.strictEqual(logs[1].source, 'proxy request');
|
||||
assert.strictEqual(logs[1].error.code, 'ECONNRESET');
|
||||
proxy.close();
|
||||
server.close();
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
// This tests that when the proxy server that just hangs up, the client can
|
||||
// handle it correctly.
|
||||
|
||||
import * as common from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
import { once } from 'events';
|
||||
import http from 'node:http';
|
||||
import { runProxiedRequest } from '../common/proxy-server.js';
|
||||
|
||||
const server = http.createServer(common.mustNotCall());
|
||||
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
|
||||
server.listen(0);
|
||||
await once(server, 'listening');
|
||||
|
||||
// Start a proxy server that just hangs up.
|
||||
const proxy = http.createServer();
|
||||
proxy.on('request', common.mustCall((req, res) => {
|
||||
req.destroy();
|
||||
}, 1));
|
||||
proxy.listen(0);
|
||||
await once(proxy, 'listening');
|
||||
|
||||
const serverHost = `localhost:${server.address().port}`;
|
||||
const requestUrl = `http://${serverHost}/test`;
|
||||
|
||||
const { code, signal, stderr, stdout } = await runProxiedRequest({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: requestUrl,
|
||||
HTTP_PROXY: `http://localhost:${proxy.address().port}`,
|
||||
});
|
||||
|
||||
// The proxy client should get hung up by the proxy server.
|
||||
assert.match(stderr, /Error: socket hang up/);
|
||||
assert.strictEqual(stdout.trim(), '');
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
|
||||
proxy.close();
|
||||
server.close();
|
||||
102
test/client-proxy/test-http-proxy-request-socket-keep-alive.mjs
Normal file
102
test/client-proxy/test-http-proxy-request-socket-keep-alive.mjs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
// This tests that when using a proxy with an agent with maxSockets: 1 and
|
||||
// keepAlive: true, after the first request finishes, a subsequent request
|
||||
// reuses the same socket connection.
|
||||
|
||||
import * as common from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
import http from 'node:http';
|
||||
import { once } from 'events';
|
||||
import { createProxyServer } from '../common/proxy-server.js';
|
||||
|
||||
// Start a server to handle requests
|
||||
const server = http.createServer(common.mustCall((req, res) => {
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
res.end(`Response for ${req.url}`);
|
||||
}, 2));
|
||||
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
|
||||
server.listen(0);
|
||||
await once(server, 'listening');
|
||||
|
||||
// Start a minimal proxy server
|
||||
const { proxy, logs } = createProxyServer();
|
||||
proxy.listen(0);
|
||||
await once(proxy, 'listening');
|
||||
|
||||
const serverHost = `localhost:${server.address().port}`;
|
||||
const proxyUrl = `http://localhost:${proxy.address().port}`;
|
||||
|
||||
// Create an agent with maxSockets: 1, keepAlive: true, and proxy support
|
||||
const agent = new http.Agent({
|
||||
maxSockets: 1,
|
||||
keepAlive: true,
|
||||
proxyEnv: {
|
||||
HTTP_PROXY: proxyUrl,
|
||||
},
|
||||
});
|
||||
|
||||
// Make first request
|
||||
const firstReq = http.request({
|
||||
hostname: 'localhost',
|
||||
port: server.address().port,
|
||||
path: '/first',
|
||||
agent: agent,
|
||||
}, common.mustCall((res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk) => data += chunk);
|
||||
res.on('end', common.mustCall(() => {
|
||||
assert.strictEqual(data, `Response for /first`);
|
||||
}));
|
||||
}));
|
||||
firstReq.on('error', common.mustNotCall());
|
||||
firstReq.end();
|
||||
|
||||
agent.once('free', common.mustCall((socket) => {
|
||||
// At this point, the first request has completed and the socket is returned
|
||||
// to the pool.
|
||||
process.nextTick(() => {
|
||||
const options = {
|
||||
hostname: 'localhost',
|
||||
port: server.address().port,
|
||||
path: '/second',
|
||||
agent: agent,
|
||||
};
|
||||
// Check that the socket is still in the pool.
|
||||
assert.strictEqual(agent.freeSockets[agent.getName(options)].length, 1);
|
||||
// Send second request when first request closes (socket returned to pool)
|
||||
const secondReq = http.request(options, common.mustCall((res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk) => data += chunk);
|
||||
res.on('end', common.mustCall(() => {
|
||||
assert.strictEqual(data, `Response for /second`);
|
||||
|
||||
// Verify both requests went through the proxy
|
||||
assert.deepStrictEqual(logs, [
|
||||
{
|
||||
method: 'GET',
|
||||
url: `http://${serverHost}/first`,
|
||||
headers: {
|
||||
'host': serverHost,
|
||||
'proxy-connection': 'keep-alive',
|
||||
'connection': 'keep-alive',
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
url: `http://${serverHost}/second`,
|
||||
headers: {
|
||||
'host': serverHost,
|
||||
'proxy-connection': 'keep-alive',
|
||||
'connection': 'keep-alive',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
proxy.close();
|
||||
server.close();
|
||||
}));
|
||||
}));
|
||||
|
||||
secondReq.on('error', common.mustNotCall());
|
||||
secondReq.end();
|
||||
});
|
||||
}));
|
||||
93
test/client-proxy/test-http-proxy-request.mjs
Normal file
93
test/client-proxy/test-http-proxy-request.mjs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
// This tests that when the `NODE_USE_ENV_PROXY` environment variable is set to 1, Node.js
|
||||
// correctly uses the `HTTP_PROXY` or `http_proxy` environment variable to proxy HTTP requests
|
||||
// via request rewriting.
|
||||
|
||||
import * as common from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
import http from 'node:http';
|
||||
import { once } from 'events';
|
||||
import { createProxyServer, runProxiedRequest } from '../common/proxy-server.js';
|
||||
|
||||
// Start a server to process the final request.
|
||||
const server = http.createServer(common.mustCall((req, res) => {
|
||||
res.end('Hello world');
|
||||
}, common.isWindows ? 2 : 3));
|
||||
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
|
||||
server.listen(0);
|
||||
await once(server, 'listening');
|
||||
|
||||
// Start a minimal proxy server.
|
||||
const { proxy, logs } = createProxyServer();
|
||||
proxy.listen(0);
|
||||
await once(proxy, 'listening');
|
||||
|
||||
const serverHost = `localhost:${server.address().port}`;
|
||||
const requestUrl = `http://${serverHost}/test`;
|
||||
const expectedLogs = [{
|
||||
method: 'GET',
|
||||
url: requestUrl,
|
||||
headers: {
|
||||
'connection': 'keep-alive',
|
||||
'proxy-connection': 'keep-alive',
|
||||
'host': serverHost,
|
||||
},
|
||||
}];
|
||||
|
||||
// Check upper-cased HTTP_PROXY environment variable.
|
||||
{
|
||||
const { code, signal, stderr, stdout } = await runProxiedRequest({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: requestUrl,
|
||||
HTTP_PROXY: `http://localhost:${proxy.address().port}`,
|
||||
});
|
||||
assert.deepStrictEqual(logs, expectedLogs);
|
||||
assert.strictEqual(stderr.trim(), '');
|
||||
assert.match(stdout, /Hello world/);
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
}
|
||||
|
||||
// Check lower-cased http_proxy environment variable.
|
||||
{
|
||||
logs.splice(0, logs.length);
|
||||
const { code, signal, stderr, stdout } = await runProxiedRequest({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: requestUrl,
|
||||
http_proxy: `http://localhost:${proxy.address().port}`,
|
||||
});
|
||||
assert.deepStrictEqual(logs, expectedLogs);
|
||||
assert.strictEqual(stderr.trim(), '');
|
||||
assert.match(stdout, /Hello world/);
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
}
|
||||
|
||||
// Check that the lower-cased http_proxy environment variable takes precedence over the
|
||||
// upper-cased HTTP_PROXY.
|
||||
// On Windows, environment variables are case-insensitive, so this test is not applicable.
|
||||
if (!common.isWindows) {
|
||||
const proxy2 = http.createServer(common.mustNotCall());
|
||||
proxy2.on('connect', common.mustNotCall());
|
||||
proxy2.listen(0);
|
||||
await once(proxy2, 'listening');
|
||||
|
||||
// Check lower-cased http_proxy environment variable takes precedence.
|
||||
logs.splice(0, logs.length);
|
||||
const { code, signal, stderr, stdout } = await runProxiedRequest({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: requestUrl,
|
||||
http_proxy: `http://localhost:${proxy.address().port}`,
|
||||
HTTP_PROXY: `http://localhost:${proxy2.address().port}`,
|
||||
}, {
|
||||
stdout: 'Hello world',
|
||||
});
|
||||
assert.deepStrictEqual(logs, expectedLogs);
|
||||
assert.strictEqual(stderr.trim(), '');
|
||||
assert.match(stdout, /Hello world/);
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
proxy2.close();
|
||||
}
|
||||
|
||||
proxy.close();
|
||||
server.close();
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
// This tests that when the target server fails during a POST request,
|
||||
// the proxy client can handle it correctly.
|
||||
|
||||
import * as common from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
import { once } from 'events';
|
||||
import http from 'node:http';
|
||||
import { createProxyServer, runProxiedPOST } from '../common/proxy-server.js';
|
||||
|
||||
// Start a server that immediately destroys connections during resource creation.
|
||||
const server = http.createServer(common.mustCall((req, res) => {
|
||||
// Simulate server failure during resource creation
|
||||
req.destroy();
|
||||
}));
|
||||
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
|
||||
server.listen(0);
|
||||
await once(server, 'listening');
|
||||
|
||||
// Start a minimal proxy server.
|
||||
const { proxy, logs } = createProxyServer();
|
||||
proxy.listen(0);
|
||||
await once(proxy, 'listening');
|
||||
|
||||
const serverHost = `localhost:${server.address().port}`;
|
||||
const requestUrl = `http://${serverHost}/resources`;
|
||||
const resourceData = JSON.stringify({ name: 'failing-resource', data: 'will-not-be-created' });
|
||||
|
||||
const { code, signal, stderr, stdout } = await runProxiedPOST({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: requestUrl,
|
||||
RESOURCE_DATA: resourceData,
|
||||
HTTP_PROXY: `http://localhost:${proxy.address().port}`,
|
||||
});
|
||||
|
||||
// The proxy client should receive a well-formed 500 response.
|
||||
assert.match(stdout, /Proxy error ECONNRESET: socket hang up/);
|
||||
assert.match(stdout, /Status Code: 500/);
|
||||
assert.strictEqual(stderr.trim(), '');
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
|
||||
// The proxy should receive the POST request with correct headers.
|
||||
assert.deepStrictEqual(logs[0], {
|
||||
method: 'POST',
|
||||
url: requestUrl,
|
||||
headers: {
|
||||
'connection': 'keep-alive',
|
||||
'proxy-connection': 'keep-alive',
|
||||
'host': serverHost,
|
||||
'content-type': 'application/json',
|
||||
'content-length': Buffer.byteLength(resourceData).toString(),
|
||||
},
|
||||
});
|
||||
|
||||
// The proxy should receive a ECONNRESET from the target server.
|
||||
assert.strictEqual(logs[1].source, 'proxy request');
|
||||
assert.strictEqual(logs[1].error.code, 'ECONNRESET');
|
||||
|
||||
proxy.close();
|
||||
server.close();
|
||||
62
test/client-proxy/test-http-request-proxy-post.mjs
Normal file
62
test/client-proxy/test-http-request-proxy-post.mjs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import * as common from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
import { once } from 'events';
|
||||
import http from 'node:http';
|
||||
import { createProxyServer, runProxiedPOST } from '../common/proxy-server.js';
|
||||
|
||||
// Start a server that creates resources
|
||||
const resources = [];
|
||||
const server = http.createServer(common.mustCall((req, res) => {
|
||||
let body = '';
|
||||
req.on('data', (chunk) => {
|
||||
body += chunk;
|
||||
});
|
||||
req.on('end', () => {
|
||||
const resource = JSON.parse(body);
|
||||
resource.id = resources.length + 1;
|
||||
resources.push(resource);
|
||||
|
||||
res.writeHead(201, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(resource));
|
||||
});
|
||||
}));
|
||||
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
|
||||
server.listen(0);
|
||||
await once(server, 'listening');
|
||||
|
||||
// Start a minimal proxy server.
|
||||
const { proxy, logs } = createProxyServer();
|
||||
proxy.listen(0);
|
||||
await once(proxy, 'listening');
|
||||
|
||||
const serverHost = `localhost:${server.address().port}`;
|
||||
const requestUrl = `http://${serverHost}/resources`;
|
||||
const resourceData = JSON.stringify({ name: 'test-resource', value: 'some-value' });
|
||||
|
||||
const { code, signal, stderr, stdout } = await runProxiedPOST({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: requestUrl,
|
||||
RESOURCE_DATA: resourceData,
|
||||
HTTP_PROXY: `http://localhost:${proxy.address().port}`,
|
||||
});
|
||||
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
assert.strictEqual(stderr.trim(), '');
|
||||
assert.match(stdout, /Status Code: 201/);
|
||||
|
||||
// Verify the resource was created
|
||||
const jsonMatch = stdout.match(/{"[^}]*}$/);
|
||||
assert(jsonMatch, 'Should have JSON response');
|
||||
const response = JSON.parse(jsonMatch[0]);
|
||||
assert.strictEqual(response.name, 'test-resource');
|
||||
assert.strictEqual(response.id, 1);
|
||||
|
||||
// Verify proxy logged the POST request
|
||||
assert.strictEqual(logs.length, 1);
|
||||
assert.strictEqual(logs[0].method, 'POST');
|
||||
assert.strictEqual(logs[0].url, requestUrl);
|
||||
assert.strictEqual(logs[0].headers['content-type'], 'application/json');
|
||||
|
||||
proxy.close();
|
||||
server.close();
|
||||
|
|
@ -10,7 +10,7 @@ if (!common.hasCrypto)
|
|||
|
||||
// https must be dynamically imported so that builds without crypto support
|
||||
// can skip it.
|
||||
const https = (await import('node:https')).default;
|
||||
const { default: https } = await import('node:https');
|
||||
|
||||
// Start a server to process the final request.
|
||||
const server = https.createServer({
|
||||
|
|
|
|||
60
test/client-proxy/test-https-proxy-request-auth-failure.mjs
Normal file
60
test/client-proxy/test-https-proxy-request-auth-failure.mjs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
// This tests that when the proxy server rejects authentication for CONNECT,
|
||||
// the client can handle it correctly.
|
||||
|
||||
import * as common from '../common/index.mjs';
|
||||
import fixtures from '../common/fixtures.js';
|
||||
import assert from 'node:assert';
|
||||
import { once } from 'events';
|
||||
import http from 'node:http';
|
||||
import { runProxiedRequest } from '../common/proxy-server.js';
|
||||
|
||||
if (!common.hasCrypto)
|
||||
common.skip('missing crypto');
|
||||
|
||||
// https must be dynamically imported so that builds without crypto support
|
||||
// can skip it.
|
||||
const { default: https } = await import('node:https');
|
||||
|
||||
const server = https.createServer({
|
||||
cert: fixtures.readKey('agent8-cert.pem'),
|
||||
key: fixtures.readKey('agent8-key.pem'),
|
||||
}, common.mustNotCall());
|
||||
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
|
||||
server.listen(0);
|
||||
await once(server, 'listening');
|
||||
|
||||
// Start a proxy server that rejects authentication.
|
||||
const proxy = http.createServer();
|
||||
proxy.on('connect', common.mustCall((req, res) => {
|
||||
const authHeader = req.headers['proxy-authorization'];
|
||||
assert(authHeader);
|
||||
assert.match(authHeader, /^Basic /);
|
||||
const credentials = Buffer.from(authHeader.slice(6), 'base64').toString();
|
||||
assert.strictEqual(credentials, 'baduser:badpass');
|
||||
|
||||
res.write('HTTP/1.1 407 Proxy Authentication Required\r\n');
|
||||
res.write('Proxy-Authenticate: Basic realm="proxy"\r\n');
|
||||
res.write('\r\n');
|
||||
res.end();
|
||||
}, 1));
|
||||
proxy.listen(0);
|
||||
await once(proxy, 'listening');
|
||||
|
||||
const serverHost = `localhost:${server.address().port}`;
|
||||
const requestUrl = `https://${serverHost}/test`;
|
||||
|
||||
const { code, signal, stderr } = await runProxiedRequest({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: requestUrl,
|
||||
HTTPS_PROXY: `http://baduser:badpass@localhost:${proxy.address().port}`,
|
||||
NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'fake-startcom-root-cert.pem'),
|
||||
});
|
||||
|
||||
// The proxy client should get an error from proxy authentication failure.
|
||||
// Since the process exits cleanly but with an error, check for any error output
|
||||
assert.match(stderr, /407 Proxy Authentication Required/);
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
|
||||
proxy.close();
|
||||
server.close();
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
// This tests that when the proxy server connection is refused, the HTTPS client can
|
||||
// handle it correctly.
|
||||
import * as common from '../common/index.mjs';
|
||||
import fixtures from '../common/fixtures.js';
|
||||
import assert from 'node:assert';
|
||||
import { once } from 'events';
|
||||
import { runProxiedRequest } from '../common/proxy-server.js';
|
||||
import dgram from 'node:dgram';
|
||||
|
||||
if (!common.hasCrypto)
|
||||
common.skip('missing crypto');
|
||||
|
||||
// https must be dynamically imported so that builds without crypto support
|
||||
// can skip it.
|
||||
const { default: https } = await import('node:https');
|
||||
|
||||
const server = https.createServer({
|
||||
cert: fixtures.readKey('agent8-cert.pem'),
|
||||
key: fixtures.readKey('agent8-key.pem'),
|
||||
}, common.mustNotCall());
|
||||
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
|
||||
server.listen(0);
|
||||
await once(server, 'listening');
|
||||
|
||||
const serverHost = `localhost:${server.address().port}`;
|
||||
const requestUrl = `https://${serverHost}/test`;
|
||||
|
||||
// Make it fail on connection refused by connecting to a UDP port with TCP.
|
||||
const udp = dgram.createSocket('udp4');
|
||||
udp.bind(0, '127.0.0.1');
|
||||
await once(udp, 'listening');
|
||||
const port = udp.address().port;
|
||||
|
||||
const { code, signal, stderr, stdout } = await runProxiedRequest({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: requestUrl,
|
||||
HTTPS_PROXY: `http://localhost:${port}`,
|
||||
NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'fake-startcom-root-cert.pem'),
|
||||
});
|
||||
|
||||
// The proxy client should get a connection refused error.
|
||||
assert.match(stderr, /Error.*connect ECONNREFUSED/);
|
||||
assert.strictEqual(stdout.trim(), '');
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
|
||||
server.close();
|
||||
udp.close();
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
// This tests that when the proxy server returns an empty response to CONNECT,
|
||||
// the client can handle it correctly.
|
||||
import * as common from '../common/index.mjs';
|
||||
|
||||
import fixtures from '../common/fixtures.js';
|
||||
import assert from 'node:assert';
|
||||
import { once } from 'events';
|
||||
import http from 'node:http';
|
||||
import { runProxiedRequest } from '../common/proxy-server.js';
|
||||
|
||||
if (!common.hasCrypto)
|
||||
common.skip('missing crypto');
|
||||
|
||||
// https must be dynamically imported so that builds without crypto support
|
||||
// can skip it.
|
||||
const { default: https } = await import('node:https');
|
||||
|
||||
const server = https.createServer({
|
||||
cert: fixtures.readKey('agent8-cert.pem'),
|
||||
key: fixtures.readKey('agent8-key.pem'),
|
||||
}, common.mustNotCall());
|
||||
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
|
||||
server.listen(0);
|
||||
await once(server, 'listening');
|
||||
|
||||
// Start a proxy server that sends empty response and closes.
|
||||
const proxy = http.createServer();
|
||||
proxy.on('connect', common.mustCall((req, res) => {
|
||||
res.end();
|
||||
}, 1));
|
||||
proxy.listen(0);
|
||||
await once(proxy, 'listening');
|
||||
|
||||
const serverHost = `localhost:${server.address().port}`;
|
||||
const requestUrl = `https://${serverHost}/test`;
|
||||
|
||||
const { code, signal, stderr, stdout } = await runProxiedRequest({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: requestUrl,
|
||||
HTTPS_PROXY: `http://localhost:${proxy.address().port}`,
|
||||
NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'fake-startcom-root-cert.pem'),
|
||||
});
|
||||
|
||||
// The proxy client should get an error from failure in establishing the tunnel.
|
||||
assert.match(stderr, /ERR_PROXY_TUNNEL.*Connection to establish proxy tunnel ended unexpectedly/);
|
||||
assert.strictEqual(stdout.trim(), '');
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
|
||||
proxy.close();
|
||||
server.close();
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
// This tests that when the TLS handshake with the endpoint fails,
|
||||
// the proxy client will get a connection error.
|
||||
import * as common from '../common/index.mjs';
|
||||
|
||||
import fixtures from '../common/fixtures.js';
|
||||
import assert from 'node:assert';
|
||||
import { once } from 'events';
|
||||
import { runProxiedRequest, createProxyServer } from '../common/proxy-server.js';
|
||||
|
||||
if (!common.hasCrypto)
|
||||
common.skip('missing crypto');
|
||||
|
||||
// https must be dynamically imported so that builds without crypto support
|
||||
// can skip it.
|
||||
const { default: https } = await import('node:https');
|
||||
|
||||
const server = https.createServer({
|
||||
cert: fixtures.readKey('agent8-cert.pem'),
|
||||
key: fixtures.readKey('agent8-key.pem'),
|
||||
}, common.mustNotCall());
|
||||
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
|
||||
server.listen(0);
|
||||
await once(server, 'listening');
|
||||
|
||||
// Start a proxy server that sends incomplete headers.
|
||||
const { proxy, logs } = createProxyServer();
|
||||
proxy.listen(0);
|
||||
await once(proxy, 'listening');
|
||||
|
||||
const serverHost = `localhost:${server.address().port}`;
|
||||
const requestUrl = `https://${serverHost}/test`;
|
||||
|
||||
const { code, signal, stderr, stdout } = await runProxiedRequest({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: requestUrl,
|
||||
REQUEST_TIMEOUT: 1000,
|
||||
HTTPS_PROXY: `http://localhost:${proxy.address().port}`,
|
||||
});
|
||||
|
||||
// The proxy client should get a UNABLE_TO_VERIFY_LEAF_SIGNATURE during TLS handshake.
|
||||
assert.match(stderr, /UNABLE_TO_VERIFY_LEAF_SIGNATURE/);
|
||||
assert.strictEqual(stdout, '');
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
|
||||
// Verify that it goes through the proxy.
|
||||
assert.deepStrictEqual(logs, [{
|
||||
method: 'CONNECT',
|
||||
url: serverHost,
|
||||
headers: {
|
||||
'proxy-connection': 'keep-alive',
|
||||
'host': serverHost,
|
||||
},
|
||||
}]);
|
||||
|
||||
proxy.close();
|
||||
server.close();
|
||||
59
test/client-proxy/test-https-proxy-request-https-proxy.mjs
Normal file
59
test/client-proxy/test-https-proxy-request-https-proxy.mjs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
// This tests that when the `NODE_USE_ENV_PROXY` environment variable is set to 1, Node.js
|
||||
// correctly uses the `HTTPS_PROXY` or `https_proxy` environment variable to proxy HTTPS requests
|
||||
// via a tunnel when the proxy server itself uses HTTPS.
|
||||
|
||||
import * as common from '../common/index.mjs';
|
||||
import fixtures from '../common/fixtures.js';
|
||||
import assert from 'node:assert';
|
||||
import { once } from 'events';
|
||||
import { createProxyServer, runProxiedRequest } from '../common/proxy-server.js';
|
||||
|
||||
if (!common.hasCrypto) {
|
||||
common.skip('missing crypto');
|
||||
}
|
||||
|
||||
// https must be dynamically imported so that builds without crypto support
|
||||
// can skip it.
|
||||
const { default: https } = await import('node:https');
|
||||
|
||||
// Start a server to process the final request.
|
||||
const server = https.createServer({
|
||||
cert: fixtures.readKey('agent8-cert.pem'),
|
||||
key: fixtures.readKey('agent8-key.pem'),
|
||||
}, common.mustCall((req, res) => {
|
||||
res.end('Hello world');
|
||||
}));
|
||||
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
|
||||
server.listen(0);
|
||||
await once(server, 'listening');
|
||||
|
||||
// Start an HTTPS proxy server.
|
||||
const { proxy, logs } = createProxyServer({ https: true });
|
||||
proxy.listen(0);
|
||||
await once(proxy, 'listening');
|
||||
|
||||
const serverHost = `localhost:${server.address().port}`;
|
||||
const requestUrl = `https://${serverHost}/test`;
|
||||
const expectedLogs = [{
|
||||
method: 'CONNECT',
|
||||
url: serverHost,
|
||||
headers: {
|
||||
'proxy-connection': 'keep-alive',
|
||||
'host': serverHost,
|
||||
},
|
||||
}];
|
||||
|
||||
const { code, signal, stderr, stdout } = await runProxiedRequest({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: requestUrl,
|
||||
HTTPS_PROXY: `https://localhost:${proxy.address().port}`,
|
||||
NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'fake-startcom-root-cert.pem'),
|
||||
});
|
||||
assert.deepStrictEqual(logs, expectedLogs);
|
||||
assert.strictEqual(stderr.trim(), '');
|
||||
assert.match(stdout, /Hello world/);
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
|
||||
proxy.close();
|
||||
server.close();
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
// This tests that when the proxy server returns incomplete headers for CONNECT,
|
||||
// the client will just timeout.
|
||||
import * as common from '../common/index.mjs';
|
||||
|
||||
import fixtures from '../common/fixtures.js';
|
||||
import assert from 'node:assert';
|
||||
import { once } from 'events';
|
||||
import http from 'node:http';
|
||||
import { runProxiedRequest } from '../common/proxy-server.js';
|
||||
if (!common.hasCrypto)
|
||||
common.skip('missing crypto');
|
||||
|
||||
// https must be dynamically imported so that builds without crypto support
|
||||
// can skip it.
|
||||
const { default: https } = await import('node:https');
|
||||
|
||||
const server = https.createServer({
|
||||
cert: fixtures.readKey('agent8-cert.pem'),
|
||||
key: fixtures.readKey('agent8-key.pem'),
|
||||
}, common.mustNotCall());
|
||||
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
|
||||
server.listen(0);
|
||||
await once(server, 'listening');
|
||||
|
||||
// Start a proxy server that sends incomplete headers.
|
||||
const proxy = http.createServer();
|
||||
proxy.on('connect', common.mustCall((req, res) => {
|
||||
res.write('HTTP/1.1 200 Connection Established\r\n');
|
||||
// Missing the final \r\n to complete headers - this should cause a hang/timeout
|
||||
}, 1));
|
||||
proxy.listen(0);
|
||||
await once(proxy, 'listening');
|
||||
|
||||
const serverHost = `localhost:${server.address().port}`;
|
||||
const requestUrl = `https://${serverHost}/test`;
|
||||
|
||||
const { code, signal, stderr, stdout } = await runProxiedRequest({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: requestUrl,
|
||||
REQUEST_TIMEOUT: 1000,
|
||||
HTTPS_PROXY: `http://localhost:${proxy.address().port}`,
|
||||
NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'fake-startcom-root-cert.pem'),
|
||||
});
|
||||
|
||||
// The proxy client should get a connection timeout.
|
||||
assert.match(stderr, /Request timed out/);
|
||||
assert.match(stderr, /timed out after 1000ms/);
|
||||
assert.strictEqual(stdout.trim(), '');
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
|
||||
proxy.close();
|
||||
server.close();
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
// This tests that invalid hosts or ports with carriage return or newline characters
|
||||
// in HTTPsS request options would lead to ERR_INVALID_CHAR.
|
||||
|
||||
import * as common from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
import { once } from 'events';
|
||||
import fixtures from '../common/fixtures.js';
|
||||
|
||||
if (!common.hasCrypto)
|
||||
common.skip('missing crypto');
|
||||
|
||||
// https must be dynamically imported so that builds without crypto support
|
||||
// can skip it.
|
||||
const { default: https } = await import('node:https');
|
||||
|
||||
const server = https.createServer({
|
||||
cert: fixtures.readKey('agent8-cert.pem'),
|
||||
key: fixtures.readKey('agent8-key.pem'),
|
||||
}, common.mustNotCall());
|
||||
server.listen(0);
|
||||
await once(server, 'listening');
|
||||
server.on('error', common.mustNotCall());
|
||||
const port = server.address().port.toString();
|
||||
|
||||
const testCases = [
|
||||
{ host: 'local\rhost', port: port, path: '/carriage-return-in-host' },
|
||||
{ host: 'local\nhost', port: port, path: '/newline-in-host' },
|
||||
{ host: 'local\r\nhost', port: port, path: '/crlf-in-host' },
|
||||
{ host: 'localhost', port: port.substring(0, 1) + '\r' + port.substring(1), path: '/carriage-return-in-port' },
|
||||
{ host: 'localhost', port: port.substring(0, 1) + '\n' + port.substring(1), path: '/newline-in-port' },
|
||||
{ host: 'localhost', port: port.substring(0, 1) + '\r\n' + port.substring(1), path: '/crlf-in-port' },
|
||||
];
|
||||
|
||||
const proxy = https.createServer(common.mustNotCall());
|
||||
proxy.listen(0);
|
||||
await once(proxy, 'listening');
|
||||
|
||||
const agent = new https.Agent({
|
||||
ca: fixtures.readKey('fake-startcom-root-cert.pem'),
|
||||
proxyEnv: {
|
||||
HTTPS_PROXY: `https://localhost:${proxy.address().port}`,
|
||||
},
|
||||
});
|
||||
|
||||
for (const testCase of testCases) {
|
||||
const options = { ...testCase, agent };
|
||||
assert.throws(() => {
|
||||
https.request(options, common.mustNotCall());
|
||||
}, {
|
||||
code: 'ERR_INVALID_CHAR',
|
||||
});
|
||||
}
|
||||
|
||||
server.close();
|
||||
proxy.close();
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
// This tests that invalid hosts or ports with carriage return or newline characters
|
||||
// in HTTPS request urls are stripped away before being sent to the server.
|
||||
|
||||
import * as common from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
import fixtures from '../common/fixtures.js';
|
||||
import { once } from 'events';
|
||||
import { inspect } from 'node:util';
|
||||
import { createProxyServer } from '../common/proxy-server.js';
|
||||
|
||||
if (!common.hasCrypto) {
|
||||
common.skip('missing crypto');
|
||||
}
|
||||
|
||||
// https must be dynamically imported so that builds without crypto support
|
||||
// can skip it.
|
||||
const { default: https } = await import('node:https');
|
||||
const requests = new Set();
|
||||
|
||||
const server = https.createServer({
|
||||
cert: fixtures.readKey('agent8-cert.pem'),
|
||||
key: fixtures.readKey('agent8-key.pem'),
|
||||
}, (req, res) => {
|
||||
requests.add(`https://localhost:${server.address().port}${req.url}`);
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
res.end(`Response for ${req.url}`);
|
||||
});
|
||||
|
||||
server.listen(0);
|
||||
await once(server, 'listening');
|
||||
server.on('error', common.mustNotCall());
|
||||
const port = server.address().port.toString();
|
||||
|
||||
const testCases = [
|
||||
{ host: 'local\rhost', port: port, path: '/carriage-return-in-host' },
|
||||
{ host: 'local\nhost', port: port, path: '/newline-in-host' },
|
||||
{ host: 'local\r\nhost', port: port, path: '/crlf-in-host' },
|
||||
{ host: 'localhost', port: port.substring(0, 1) + '\r' + port.substring(1), path: '/carriage-return-in-port' },
|
||||
{ host: 'localhost', port: port.substring(0, 1) + '\n' + port.substring(1), path: '/newline-in-port' },
|
||||
{ host: 'localhost', port: port.substring(0, 1) + '\r\n' + port.substring(1), path: '/crlf-in-port' },
|
||||
];
|
||||
|
||||
// Start a minimal proxy server
|
||||
const { proxy, logs } = createProxyServer();
|
||||
proxy.listen(0);
|
||||
await once(proxy, 'listening');
|
||||
|
||||
https.globalAgent = new https.Agent({
|
||||
ca: fixtures.readKey('fake-startcom-root-cert.pem'),
|
||||
proxyEnv: {
|
||||
HTTPS_PROXY: `http://localhost:${proxy.address().port}`,
|
||||
},
|
||||
});
|
||||
|
||||
const severHost = `localhost:${server.address().port}`;
|
||||
|
||||
let counter = testCases.length;
|
||||
const expectedUrls = new Set();
|
||||
const expectedProxyLogs = new Set();
|
||||
for (const testCase of testCases) {
|
||||
const url = `https://${testCase.host}:${testCase.port}${testCase.path}`;
|
||||
// The invalid characters should all be stripped away before being sent.
|
||||
expectedUrls.add(url.replaceAll(/\r|\n/g, ''));
|
||||
expectedProxyLogs.add({
|
||||
method: 'CONNECT',
|
||||
url: severHost,
|
||||
headers: { host: severHost },
|
||||
});
|
||||
https.request(url, (res) => {
|
||||
res.on('error', common.mustNotCall());
|
||||
res.setEncoding('utf8');
|
||||
res.on('data', () => {});
|
||||
res.on('end', common.mustCall(() => {
|
||||
console.log(`#${counter--} eneded response for: ${inspect(url)}`);
|
||||
// Finished all test cases.
|
||||
if (counter === 0) {
|
||||
proxy.close();
|
||||
server.close();
|
||||
assert.deepStrictEqual(requests, expectedUrls);
|
||||
assert.deepStrictEqual(new Set(logs), expectedProxyLogs);
|
||||
}
|
||||
}));
|
||||
}).on('error', common.mustNotCall()).end();
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
// This tests that HTTPS proxy URLs with invalid credentials (containing \r or \n characters)
|
||||
// are rejected with an appropriate error.
|
||||
import * as common from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
|
||||
if (!common.hasCrypto) {
|
||||
common.skip('missing crypto');
|
||||
}
|
||||
|
||||
// https must be dynamically imported so that builds without crypto support
|
||||
// can skip it.
|
||||
const { default: https } = await import('node:https');
|
||||
|
||||
const testCases = [
|
||||
{
|
||||
name: 'carriage return in username (HTTPS)',
|
||||
proxyUrl: 'https://user\r:pass@proxy.example.com:8080',
|
||||
expectedError: { code: 'ERR_PROXY_INVALID_CONFIG', message: /Invalid proxy URL/ },
|
||||
},
|
||||
{
|
||||
name: 'newline in username (HTTPS)',
|
||||
proxyUrl: 'https://user\n:pass@proxy.example.com:8080',
|
||||
expectedError: { code: 'ERR_PROXY_INVALID_CONFIG', message: /Invalid proxy URL/ },
|
||||
},
|
||||
{
|
||||
name: 'carriage return in password (HTTPS)',
|
||||
proxyUrl: 'https://user:pass\r@proxy.example.com:8080',
|
||||
expectedError: { code: 'ERR_PROXY_INVALID_CONFIG', message: /Invalid proxy URL/ },
|
||||
},
|
||||
{
|
||||
name: 'newline in password (HTTPS)',
|
||||
proxyUrl: 'https://user:pass\n@proxy.example.com:8080',
|
||||
expectedError: { code: 'ERR_PROXY_INVALID_CONFIG', message: /Invalid proxy URL/ },
|
||||
},
|
||||
{
|
||||
name: 'CRLF injection attempt in username (HTTPS)',
|
||||
proxyUrl: 'https://user\r\nHost: example.com:pass@proxy.example.com:8080',
|
||||
expectedError: { code: 'ERR_PROXY_INVALID_CONFIG', message: /Invalid proxy URL/ },
|
||||
},
|
||||
{
|
||||
name: 'CRLF injection attempt in password (HTTPS)',
|
||||
proxyUrl: 'https://user:pass\r\nHost: example.com@proxy.example.com:8080',
|
||||
expectedError: { code: 'ERR_PROXY_INVALID_CONFIG', message: /Invalid proxy URL/ },
|
||||
},
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
// Test that creating an agent with invalid proxy credentials throws an error
|
||||
assert.throws(() => {
|
||||
new https.Agent({
|
||||
proxyEnv: {
|
||||
HTTPS_PROXY: testCase.proxyUrl,
|
||||
},
|
||||
});
|
||||
}, testCase.expectedError);
|
||||
}
|
||||
40
test/client-proxy/test-https-proxy-request-invalid-url.mjs
Normal file
40
test/client-proxy/test-https-proxy-request-invalid-url.mjs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
// This tests that invalid proxy URLs are handled correctly for HTTPS requests.
|
||||
import * as common from '../common/index.mjs';
|
||||
import fixtures from '../common/fixtures.js';
|
||||
import assert from 'node:assert';
|
||||
import { once } from 'events';
|
||||
import { runProxiedRequest } from '../common/proxy-server.js';
|
||||
|
||||
if (!common.hasCrypto)
|
||||
common.skip('missing crypto');
|
||||
|
||||
// https must be dynamically imported so that builds without crypto support
|
||||
// can skip it.
|
||||
const { default: https } = await import('node:https');
|
||||
|
||||
const server = https.createServer({
|
||||
cert: fixtures.readKey('agent8-cert.pem'),
|
||||
key: fixtures.readKey('agent8-key.pem'),
|
||||
}, common.mustNotCall());
|
||||
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
|
||||
server.listen(0);
|
||||
await once(server, 'listening');
|
||||
|
||||
const serverHost = `localhost:${server.address().port}`;
|
||||
const requestUrl = `https://${serverHost}/test`;
|
||||
|
||||
// Test invalid proxy URL
|
||||
const { code, signal, stderr, stdout } = await runProxiedRequest({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: requestUrl,
|
||||
HTTPS_PROXY: 'not-a-valid-url',
|
||||
NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'fake-startcom-root-cert.pem'),
|
||||
});
|
||||
|
||||
// Should get an error about invalid URL
|
||||
assert.match(stderr, /TypeError.*Invalid URL/);
|
||||
assert.strictEqual(stdout.trim(), '');
|
||||
assert.strictEqual(code, 1);
|
||||
assert.strictEqual(signal, null);
|
||||
|
||||
server.close();
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
// This tests that when the proxy server returns a malformed response,
|
||||
// the client can handle it correctly.
|
||||
|
||||
import * as common from '../common/index.mjs';
|
||||
import fixtures from '../common/fixtures.js';
|
||||
import assert from 'node:assert';
|
||||
import { once } from 'events';
|
||||
import http from 'node:http';
|
||||
import { runProxiedRequest } from '../common/proxy-server.js';
|
||||
|
||||
if (!common.hasCrypto)
|
||||
common.skip('missing crypto');
|
||||
|
||||
// https must be dynamically imported so that builds without crypto support
|
||||
// can skip it.
|
||||
const { default: https } = await import('node:https');
|
||||
|
||||
const server = https.createServer({
|
||||
cert: fixtures.readKey('agent8-cert.pem'),
|
||||
key: fixtures.readKey('agent8-key.pem'),
|
||||
}, common.mustNotCall());
|
||||
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
|
||||
server.listen(0);
|
||||
await once(server, 'listening');
|
||||
|
||||
// Start a proxy server that sends malformed response.
|
||||
const proxy = http.createServer();
|
||||
proxy.on('connect', common.mustCall((req, res) => {
|
||||
res.write('NOT-HTTP MALFORMED RESPONSE\r\n\r\n');
|
||||
res.end();
|
||||
}, 1));
|
||||
proxy.listen(0);
|
||||
await once(proxy, 'listening');
|
||||
|
||||
const serverHost = `localhost:${server.address().port}`;
|
||||
const requestUrl = `https://${serverHost}/test`;
|
||||
|
||||
const { code, signal, stderr, stdout } = await runProxiedRequest({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: requestUrl,
|
||||
HTTPS_PROXY: `http://localhost:${proxy.address().port}`,
|
||||
NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'fake-startcom-root-cert.pem'),
|
||||
});
|
||||
|
||||
// The proxy client should get an error from failure in establishing the tunnel.
|
||||
assert.match(stderr, /ERR_PROXY_TUNNEL.*Failed to establish tunnel to .* NOT-HTTP MALFORMED RESPONSE/);
|
||||
assert.strictEqual(stdout.trim(), '');
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
|
||||
proxy.close();
|
||||
server.close();
|
||||
120
test/client-proxy/test-https-proxy-request-max-sockets.mjs
Normal file
120
test/client-proxy/test-https-proxy-request-max-sockets.mjs
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
// This tests that when using a proxy with an agent with maxSockets: 1,
|
||||
// subsequent requests are queued when the first request is still alive,
|
||||
// and processed after the first request completes, and both are sending
|
||||
// the request through the proxy.
|
||||
|
||||
import * as common from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
import { once } from 'events';
|
||||
import fixtures from '../common/fixtures.js';
|
||||
import { createProxyServer } from '../common/proxy-server.js';
|
||||
|
||||
if (!common.hasCrypto)
|
||||
common.skip('missing crypto');
|
||||
|
||||
// https must be dynamically imported so that builds without crypto support
|
||||
// can skip it.
|
||||
const { default: https } = await import('node:https');
|
||||
|
||||
let resolve;
|
||||
const p = new Promise((r) => { resolve = r; });
|
||||
|
||||
// Start a server that delays responses to test queuing behavior
|
||||
const server = https.createServer({
|
||||
cert: fixtures.readKey('agent8-cert.pem'),
|
||||
key: fixtures.readKey('agent8-key.pem'),
|
||||
}, common.mustCall((req, res) => {
|
||||
console.log('headers received for', req.url, req.headers);
|
||||
if (req.url === '/first') {
|
||||
// Simulate a long response for the first request
|
||||
p.then(() => {
|
||||
console.log('Responding to /first');
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
res.end('Response for /first');
|
||||
});
|
||||
} else if (req.url === '/second') {
|
||||
// Respond immediately for the second request
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
res.end('Response for /second');
|
||||
} else {
|
||||
assert.fail(`Unexpected request to ${req.url}`);
|
||||
}
|
||||
}, 2));
|
||||
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
|
||||
server.listen(0);
|
||||
await once(server, 'listening');
|
||||
|
||||
// Start a minimal proxy server
|
||||
const { proxy, logs } = createProxyServer();
|
||||
proxy.listen(0);
|
||||
await once(proxy, 'listening');
|
||||
|
||||
const serverHost = `localhost:${server.address().port}`;
|
||||
const proxyUrl = `http://localhost:${proxy.address().port}`;
|
||||
|
||||
// Create an agent with maxSockets: 1 and proxy support
|
||||
const agent = new https.Agent({
|
||||
maxSockets: 1,
|
||||
proxyEnv: {
|
||||
HTTPS_PROXY: proxyUrl,
|
||||
},
|
||||
ca: fixtures.readKey('fake-startcom-root-cert.pem'),
|
||||
});
|
||||
|
||||
const requestTimes = [];
|
||||
|
||||
// Make first request that takes longer
|
||||
const firstReq = https.request({
|
||||
hostname: 'localhost',
|
||||
port: server.address().port,
|
||||
path: '/first',
|
||||
agent: agent,
|
||||
}, common.mustCall((res) => {
|
||||
console.log('req1 response received');
|
||||
let data = '';
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
res.on('end', common.mustCall(() => {
|
||||
console.log('req1 end');
|
||||
requestTimes[0] = { path: '/first', data, endTime: Date.now() };
|
||||
assert.strictEqual(data, 'Response for /first');
|
||||
}));
|
||||
}));
|
||||
|
||||
firstReq.on('socket', common.mustCall((socket) => {
|
||||
console.log('req1 socket acquired');
|
||||
// Start second request when first request gets its socket
|
||||
// so that it will be queued.
|
||||
const secondReq = https.request({
|
||||
hostname: 'localhost',
|
||||
port: server.address().port,
|
||||
path: '/second',
|
||||
agent: agent,
|
||||
}, common.mustCall((res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
res.on('end', common.mustCall(() => {
|
||||
requestTimes[1] = { path: '/second', data, endTime: Date.now() };
|
||||
assert.strictEqual(data, 'Response for /second');
|
||||
|
||||
// The two shares the same proxy connection.
|
||||
assert.deepStrictEqual(logs, [{
|
||||
method: 'CONNECT',
|
||||
url: serverHost,
|
||||
headers: { 'proxy-connection': 'keep-alive', 'host': serverHost },
|
||||
}]);
|
||||
proxy.close();
|
||||
server.close();
|
||||
}));
|
||||
}));
|
||||
|
||||
secondReq.on('error', common.mustNotCall());
|
||||
firstReq.end();
|
||||
secondReq.end();
|
||||
resolve(); // Tell the server to respond to the first request
|
||||
}));
|
||||
|
||||
firstReq.on('error', common.mustNotCall());
|
||||
54
test/client-proxy/test-https-proxy-request-no-proxy.mjs
Normal file
54
test/client-proxy/test-https-proxy-request-no-proxy.mjs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
// This tests that NO_PROXY environment variable is respected for HTTPS requests.
|
||||
import * as common from '../common/index.mjs';
|
||||
import fixtures from '../common/fixtures.js';
|
||||
import assert from 'node:assert';
|
||||
import { once } from 'events';
|
||||
import http from 'node:http';
|
||||
import { runProxiedRequest } from '../common/proxy-server.js';
|
||||
|
||||
if (!common.hasCrypto)
|
||||
common.skip('missing crypto');
|
||||
|
||||
// https must be dynamically imported so that builds without crypto support
|
||||
// can skip it.
|
||||
const { default: https } = await import('node:https');
|
||||
|
||||
// Start a server to process the final request.
|
||||
const server = https.createServer({
|
||||
cert: fixtures.readKey('agent8-cert.pem'),
|
||||
key: fixtures.readKey('agent8-key.pem'),
|
||||
}, common.mustCall((req, res) => {
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
res.end('Hello World\n');
|
||||
}, 1));
|
||||
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
|
||||
server.listen(0);
|
||||
await once(server, 'listening');
|
||||
|
||||
// Start a proxy server that should NOT be used.
|
||||
const proxy = http.createServer();
|
||||
proxy.on('connect', common.mustNotCall());
|
||||
proxy.listen(0);
|
||||
await once(proxy, 'listening');
|
||||
|
||||
const serverHost = `localhost:${server.address().port}`;
|
||||
const requestUrl = `https://${serverHost}/test`;
|
||||
|
||||
// Test NO_PROXY with exact hostname match.
|
||||
const { code, signal, stderr, stdout } = await runProxiedRequest({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: requestUrl,
|
||||
HTTPS_PROXY: `http://localhost:${proxy.address().port}`,
|
||||
NO_PROXY: 'localhost',
|
||||
NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'fake-startcom-root-cert.pem'),
|
||||
});
|
||||
|
||||
// The request should succeed and bypass proxy.
|
||||
assert.match(stdout, /Status Code: 200/);
|
||||
assert.match(stdout, /Hello World/);
|
||||
assert.strictEqual(stderr.trim(), '');
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
|
||||
proxy.close();
|
||||
server.close();
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
// This tests that when the proxy server returns a 404 status code for CONNECT,
|
||||
// the client can handle it correctly.
|
||||
|
||||
import * as common from '../common/index.mjs';
|
||||
if (!common.hasCrypto)
|
||||
common.skip('missing crypto');
|
||||
|
||||
import fixtures from '../common/fixtures.js';
|
||||
import assert from 'node:assert';
|
||||
import { once } from 'events';
|
||||
import http from 'node:http';
|
||||
import { runProxiedRequest } from '../common/proxy-server.js';
|
||||
|
||||
// https must be dynamically imported so that builds without crypto support
|
||||
// can skip it.
|
||||
const { default: https } = await import('node:https');
|
||||
|
||||
const server = https.createServer({
|
||||
cert: fixtures.readKey('agent8-cert.pem'),
|
||||
key: fixtures.readKey('agent8-key.pem'),
|
||||
}, common.mustNotCall());
|
||||
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
|
||||
server.listen(0);
|
||||
await once(server, 'listening');
|
||||
|
||||
// Start a proxy server that returns 404 for CONNECT requests.
|
||||
const proxy = http.createServer();
|
||||
proxy.on('connect', common.mustCall((req, res) => {
|
||||
res.write('HTTP/1.1 404 Not Found\r\n\r\n');
|
||||
res.end();
|
||||
}, 1));
|
||||
proxy.listen(0);
|
||||
await once(proxy, 'listening');
|
||||
|
||||
const serverHost = `localhost:${server.address().port}`;
|
||||
const requestUrl = `https://${serverHost}/test`;
|
||||
|
||||
const { code, signal, stderr, stdout } = await runProxiedRequest({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: requestUrl,
|
||||
HTTPS_PROXY: `http://localhost:${proxy.address().port}`,
|
||||
NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'fake-startcom-root-cert.pem'),
|
||||
});
|
||||
|
||||
// The proxy client should get an error from failure in establishing the tunnel.
|
||||
assert.match(stderr, /ERR_PROXY_TUNNEL.*Failed to establish tunnel to .* HTTP\/1\.1 404 Not Found/);
|
||||
assert.strictEqual(stdout.trim(), '');
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
|
||||
proxy.close();
|
||||
server.close();
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
// This tests that when the proxy server returns a 500 response, the https client can
|
||||
// handle it correctly.
|
||||
|
||||
import * as common from '../common/index.mjs';
|
||||
import fixtures from '../common/fixtures.js';
|
||||
import assert from 'node:assert';
|
||||
import { once } from 'events';
|
||||
import http from 'node:http';
|
||||
import { runProxiedRequest } from '../common/proxy-server.js';
|
||||
|
||||
if (!common.hasCrypto)
|
||||
common.skip('missing crypto');
|
||||
|
||||
// https must be dynamically imported so that builds without crypto support
|
||||
// can skip it.
|
||||
const { default: https } = await import('node:https');
|
||||
|
||||
// Start a server to process the final request.
|
||||
const server = https.createServer({
|
||||
cert: fixtures.readKey('agent8-cert.pem'),
|
||||
key: fixtures.readKey('agent8-key.pem'),
|
||||
}, common.mustNotCall());
|
||||
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
|
||||
server.listen(0);
|
||||
await once(server, 'listening');
|
||||
|
||||
// Start a proxy server that sends 500 response back.
|
||||
const proxy = http.createServer();
|
||||
proxy.on('connect', common.mustCall((req, res) => {
|
||||
res.write('HTTP/1.1 500 Connection Error\r\n\r\n');
|
||||
res.end('Proxy error: test error');
|
||||
}, 1));
|
||||
proxy.listen(0);
|
||||
await once(proxy, 'listening');
|
||||
|
||||
const serverHost = `localhost:${server.address().port}`;
|
||||
const requestUrl = `https://${serverHost}/test`;
|
||||
|
||||
const { code, signal, stderr, stdout } = await runProxiedRequest({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: requestUrl,
|
||||
HTTPS_PROXY: `http://localhost:${proxy.address().port}`,
|
||||
NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'fake-startcom-root-cert.pem'),
|
||||
});
|
||||
|
||||
// The proxy client should get an error from failure in establishing the tunnel.
|
||||
assert.match(stderr, /ERR_PROXY_TUNNEL.*Failed to establish tunnel to .* HTTP\/1\.1 500 Connection Error/);
|
||||
assert.strictEqual(stdout.trim(), '');
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
|
||||
proxy.close();
|
||||
server.close();
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
// This tests that when the proxy server returns a 502 status code for CONNECT,
|
||||
// the client can handle it correctly.
|
||||
import * as common from '../common/index.mjs';
|
||||
import fixtures from '../common/fixtures.js';
|
||||
import assert from 'node:assert';
|
||||
import { once } from 'events';
|
||||
import http from 'node:http';
|
||||
import { runProxiedRequest } from '../common/proxy-server.js';
|
||||
|
||||
if (!common.hasCrypto)
|
||||
common.skip('missing crypto');
|
||||
|
||||
// https must be dynamically imported so that builds without crypto support
|
||||
// can skip it.
|
||||
const { default: https } = await import('node:https');
|
||||
|
||||
const server = https.createServer({
|
||||
cert: fixtures.readKey('agent8-cert.pem'),
|
||||
key: fixtures.readKey('agent8-key.pem'),
|
||||
}, common.mustNotCall());
|
||||
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
|
||||
server.listen(0);
|
||||
await once(server, 'listening');
|
||||
|
||||
// Start a proxy server that returns 502 for CONNECT requests.
|
||||
const proxy = http.createServer();
|
||||
proxy.on('connect', common.mustCall((req, res) => {
|
||||
res.write('HTTP/1.1 502 Bad Gateway\r\n\r\n');
|
||||
res.end();
|
||||
}, 1));
|
||||
proxy.listen(0);
|
||||
await once(proxy, 'listening');
|
||||
|
||||
const serverHost = `localhost:${server.address().port}`;
|
||||
const requestUrl = `https://${serverHost}/test`;
|
||||
|
||||
const { code, signal, stderr, stdout } = await runProxiedRequest({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: requestUrl,
|
||||
HTTPS_PROXY: `http://localhost:${proxy.address().port}`,
|
||||
NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'fake-startcom-root-cert.pem'),
|
||||
});
|
||||
|
||||
// The proxy client should get an error from failure in establishing the tunnel.
|
||||
assert.match(stderr, /ERR_PROXY_TUNNEL.*Failed to establish tunnel to .* HTTP\/1\.1 502 Bad Gateway/);
|
||||
assert.strictEqual(stdout.trim(), '');
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
|
||||
proxy.close();
|
||||
server.close();
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
// This tests that when the proxy server hangs up the tunnel connection,
|
||||
// the client can receive a correct error.
|
||||
|
||||
import * as common from '../common/index.mjs';
|
||||
if (!common.hasCrypto)
|
||||
common.skip('missing crypto');
|
||||
|
||||
import fixtures from '../common/fixtures.js';
|
||||
import assert from 'node:assert';
|
||||
import { once } from 'events';
|
||||
import http from 'node:http';
|
||||
import { runProxiedRequest } from '../common/proxy-server.js';
|
||||
|
||||
// https must be dynamically imported so that builds without crypto support
|
||||
// can skip it.
|
||||
const { default: https } = await import('node:https');
|
||||
|
||||
const server = https.createServer({
|
||||
cert: fixtures.readKey('agent8-cert.pem'),
|
||||
key: fixtures.readKey('agent8-key.pem'),
|
||||
}, common.mustNotCall());
|
||||
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
|
||||
server.listen(0);
|
||||
await once(server, 'listening');
|
||||
|
||||
// Start a proxy server that just hangs up the tunnel.
|
||||
const proxy = http.createServer();
|
||||
proxy.on('connect', common.mustCall((req, res) => {
|
||||
req.destroy();
|
||||
}, 1));
|
||||
proxy.listen(0);
|
||||
await once(proxy, 'listening');
|
||||
|
||||
const serverHost = `localhost:${server.address().port}`;
|
||||
const requestUrl = `https://${serverHost}/test`;
|
||||
|
||||
const { code, signal, stderr, stdout } = await runProxiedRequest({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: requestUrl,
|
||||
HTTPS_PROXY: `http://localhost:${proxy.address().port}`,
|
||||
NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'fake-startcom-root-cert.pem'),
|
||||
});
|
||||
|
||||
// The proxy client should get hung up by the proxy server.
|
||||
assert.match(stderr, /ERR_PROXY_TUNNEL.*Connection to establish proxy tunnel ended unexpectedly/);
|
||||
assert.strictEqual(stdout.trim(), '');
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
|
||||
proxy.close();
|
||||
server.close();
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
// This tests that when the target server hangs up,
|
||||
// the client can receive a correct error.
|
||||
|
||||
import * as common from '../common/index.mjs';
|
||||
if (!common.hasCrypto)
|
||||
common.skip('missing crypto');
|
||||
|
||||
import fixtures from '../common/fixtures.js';
|
||||
import assert from 'node:assert';
|
||||
import { once } from 'events';
|
||||
import { createProxyServer, runProxiedRequest } from '../common/proxy-server.js';
|
||||
|
||||
// https must be dynamically imported so that builds without crypto support
|
||||
// can skip it.
|
||||
const { default: https } = await import('node:https');
|
||||
|
||||
// Start a server to process the final request.
|
||||
const server = https.createServer({
|
||||
cert: fixtures.readKey('agent8-cert.pem'),
|
||||
key: fixtures.readKey('agent8-key.pem'),
|
||||
}, common.mustCall((req, res) => { req.destroy(); }, 1));
|
||||
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
|
||||
server.listen(0);
|
||||
await once(server, 'listening');
|
||||
|
||||
// Start a minimal proxy server.
|
||||
const { proxy, logs } = createProxyServer();
|
||||
proxy.listen(0);
|
||||
await once(proxy, 'listening');
|
||||
|
||||
const serverHost = `localhost:${server.address().port}`;
|
||||
const requestUrl = `https://${serverHost}/test`;
|
||||
|
||||
const { code, signal, stderr, stdout } = await runProxiedRequest({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: requestUrl,
|
||||
HTTPS_PROXY: `http://localhost:${proxy.address().port}`,
|
||||
NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'fake-startcom-root-cert.pem'),
|
||||
});
|
||||
|
||||
// The proxy client should get hung up because the tunnel gets hung up.
|
||||
// Note: this is different from the http proxy test because when a tunnel
|
||||
// is used for HTTPS requests, the proxy server doesn't get to interject
|
||||
// and send a 500 response back.
|
||||
assert.match(stderr, /Error: socket hang up/);
|
||||
assert.strictEqual(stdout.trim(), '');
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
|
||||
// The proxy should receive a CONNECT request.
|
||||
assert.deepStrictEqual(logs, [{
|
||||
method: 'CONNECT',
|
||||
url: serverHost,
|
||||
headers: {
|
||||
'proxy-connection': 'keep-alive',
|
||||
'host': serverHost,
|
||||
},
|
||||
}]);
|
||||
|
||||
proxy.close();
|
||||
server.close();
|
||||
101
test/client-proxy/test-https-proxy-request-socket-keep-alive.mjs
Normal file
101
test/client-proxy/test-https-proxy-request-socket-keep-alive.mjs
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
// This tests that when using a proxy with an agent with maxSockets: 1 and
|
||||
// keepAlive: true, after the first request finishes, a subsequent request
|
||||
// reuses the same socket connection.
|
||||
|
||||
import * as common from '../common/index.mjs';
|
||||
import assert from 'node:assert';
|
||||
import { once } from 'events';
|
||||
import { createProxyServer } from '../common/proxy-server.js';
|
||||
import fixtures from '../common/fixtures.js';
|
||||
|
||||
if (!common.hasCrypto)
|
||||
common.skip('missing crypto');
|
||||
|
||||
// https must be dynamically imported so that builds without crypto support
|
||||
// can skip it.
|
||||
const { default: https } = await import('node:https');
|
||||
|
||||
// Start a server to handle requests
|
||||
const server = https.createServer({
|
||||
cert: fixtures.readKey('agent8-cert.pem'),
|
||||
key: fixtures.readKey('agent8-key.pem'),
|
||||
}, common.mustCall((req, res) => {
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
res.end(`Response for ${req.url}`);
|
||||
}, 2));
|
||||
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
|
||||
server.listen(0);
|
||||
await once(server, 'listening');
|
||||
|
||||
// Start a minimal proxy server
|
||||
const { proxy, logs } = createProxyServer();
|
||||
proxy.listen(0);
|
||||
await once(proxy, 'listening');
|
||||
|
||||
const serverHost = `localhost:${server.address().port}`;
|
||||
const proxyUrl = `http://localhost:${proxy.address().port}`;
|
||||
|
||||
// Create an agent with maxSockets: 1, keepAlive: true, and proxy support
|
||||
const agent = new https.Agent({
|
||||
maxSockets: 1,
|
||||
keepAlive: true,
|
||||
proxyEnv: {
|
||||
HTTPS_PROXY: proxyUrl,
|
||||
},
|
||||
ca: fixtures.readKey('fake-startcom-root-cert.pem'),
|
||||
});
|
||||
|
||||
// Make first request
|
||||
const firstReq = https.request({
|
||||
hostname: 'localhost',
|
||||
port: server.address().port,
|
||||
path: '/first',
|
||||
agent: agent,
|
||||
}, common.mustCall((res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk) => data += chunk);
|
||||
res.on('end', common.mustCall(() => {
|
||||
assert.strictEqual(data, `Response for /first`);
|
||||
}));
|
||||
}));
|
||||
firstReq.on('error', common.mustNotCall());
|
||||
firstReq.end();
|
||||
|
||||
agent.once('free', common.mustCall((socket) => {
|
||||
// At this point, the first request has completed and the socket is returned
|
||||
// to the pool.
|
||||
process.nextTick(() => {
|
||||
const options = {
|
||||
hostname: 'localhost',
|
||||
port: server.address().port,
|
||||
path: '/second',
|
||||
agent: agent,
|
||||
};
|
||||
// Check that the socket is still in the pool.
|
||||
// TODO(joyeecheung): we don't currently have a way to set the root certificate
|
||||
// dynamically and have to use the ca: option which affects name computation.
|
||||
// So we cannot check the pool here until we have that feature.
|
||||
// See https://github.com/nodejs/node/pull/58822
|
||||
// Send second request when first request closes (socket returned to pool)
|
||||
const secondReq = https.request(options, common.mustCall((res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk) => data += chunk);
|
||||
res.on('end', common.mustCall(() => {
|
||||
assert.strictEqual(data, `Response for /second`);
|
||||
|
||||
// The two shares the same proxy connection.
|
||||
assert.deepStrictEqual(logs, [{
|
||||
method: 'CONNECT',
|
||||
url: serverHost,
|
||||
headers: { 'proxy-connection': 'keep-alive', 'host': serverHost },
|
||||
}]);
|
||||
|
||||
proxy.close();
|
||||
server.close();
|
||||
}));
|
||||
}));
|
||||
|
||||
secondReq.on('error', common.mustNotCall());
|
||||
secondReq.end();
|
||||
});
|
||||
}));
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
// This tests that when the proxy server doesn't respond to CONNECT in time,
|
||||
// the client respects the agent timeout setting.
|
||||
|
||||
import * as common from '../common/index.mjs';
|
||||
import fixtures from '../common/fixtures.js';
|
||||
import assert from 'node:assert';
|
||||
import { once } from 'events';
|
||||
import http from 'node:http';
|
||||
import { runProxiedRequest } from '../common/proxy-server.js';
|
||||
|
||||
if (!common.hasCrypto)
|
||||
common.skip('missing crypto');
|
||||
|
||||
// https must be dynamically imported so that builds without crypto support
|
||||
// can skip it.
|
||||
const { default: https } = await import('node:https');
|
||||
|
||||
const server = https.createServer({
|
||||
cert: fixtures.readKey('agent8-cert.pem'),
|
||||
key: fixtures.readKey('agent8-key.pem'),
|
||||
}, common.mustNotCall());
|
||||
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
|
||||
server.listen(0);
|
||||
await once(server, 'listening');
|
||||
|
||||
// Start a proxy server that accepts CONNECT but never responds.
|
||||
const proxy = http.createServer();
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
proxy.on('connect', common.mustCall(() => {
|
||||
// Don't respond - just hang to simulate timeout
|
||||
}, 1));
|
||||
proxy.listen(0);
|
||||
await once(proxy, 'listening');
|
||||
|
||||
const serverHost = `localhost:${server.address().port}`;
|
||||
const requestUrl = `https://${serverHost}/test`;
|
||||
|
||||
const { code, signal, stderr, stdout } = await runProxiedRequest({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: requestUrl,
|
||||
AGENT_TIMEOUT: 1000,
|
||||
HTTPS_PROXY: `http://localhost:${proxy.address().port}`,
|
||||
NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'fake-startcom-root-cert.pem'),
|
||||
});
|
||||
|
||||
// The proxy client should get a connection timeout.
|
||||
assert.match(stderr, /Request timed out/);
|
||||
assert.match(stderr, /timed out after 1000ms/);
|
||||
assert.strictEqual(stdout.trim(), '');
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
|
||||
proxy.close();
|
||||
server.close();
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
// This tests that when the proxy server doesn't respond to CONNECT in time,
|
||||
// the client can handle it correctly.
|
||||
|
||||
import * as common from '../common/index.mjs';
|
||||
import fixtures from '../common/fixtures.js';
|
||||
import assert from 'node:assert';
|
||||
import http from 'node:http';
|
||||
import { once } from 'events';
|
||||
import { runProxiedRequest } from '../common/proxy-server.js';
|
||||
|
||||
if (!common.hasCrypto)
|
||||
common.skip('missing crypto');
|
||||
|
||||
// https must be dynamically imported so that builds without crypto support
|
||||
// can skip it.
|
||||
const { default: https } = await import('node:https');
|
||||
|
||||
const server = https.createServer({
|
||||
cert: fixtures.readKey('agent8-cert.pem'),
|
||||
key: fixtures.readKey('agent8-key.pem'),
|
||||
}, common.mustNotCall());
|
||||
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
|
||||
server.listen(0);
|
||||
await once(server, 'listening');
|
||||
|
||||
// Start a proxy server that accepts CONNECT but never responds.
|
||||
const proxy = http.createServer();
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
proxy.on('connect', common.mustCall((req, res) => {
|
||||
// Don't respond - just hang to simulate timeout
|
||||
}, 1));
|
||||
proxy.listen(0);
|
||||
await once(proxy, 'listening');
|
||||
|
||||
const serverHost = `localhost:${server.address().port}`;
|
||||
const requestUrl = `https://${serverHost}/test`;
|
||||
|
||||
const { code, signal, stderr, stdout } = await runProxiedRequest({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: requestUrl,
|
||||
REQUEST_TIMEOUT: 1000,
|
||||
HTTPS_PROXY: `http://localhost:${proxy.address().port}`,
|
||||
NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'fake-startcom-root-cert.pem'),
|
||||
});
|
||||
|
||||
// The proxy client should get a connection timeout.
|
||||
assert.match(stderr, /Request timed out/);
|
||||
assert.match(stderr, /timed out after 1000ms/);
|
||||
assert.strictEqual(stdout.trim(), '');
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
|
||||
proxy.close();
|
||||
server.close();
|
||||
107
test/client-proxy/test-https-proxy-request.mjs
Normal file
107
test/client-proxy/test-https-proxy-request.mjs
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
// This tests that when the `NODE_USE_ENV_PROXY` environment variable is set to 1, Node.js
|
||||
// correctly uses the `HTTPS_PROXY` or `https_proxy` environment variable to proxy HTTPS requests
|
||||
// via a tunnel.
|
||||
|
||||
import * as common from '../common/index.mjs';
|
||||
import fixtures from '../common/fixtures.js';
|
||||
import assert from 'node:assert';
|
||||
import http from 'node:http';
|
||||
import { once } from 'events';
|
||||
import { createProxyServer, runProxiedRequest } from '../common/proxy-server.js';
|
||||
|
||||
if (!common.hasCrypto) {
|
||||
common.skip('missing crypto');
|
||||
}
|
||||
|
||||
// https must be dynamically imported so that builds without crypto support
|
||||
// can skip it.
|
||||
const { default: https } = await import('node:https');
|
||||
|
||||
// Start a server to process the final request.
|
||||
const server = https.createServer({
|
||||
cert: fixtures.readKey('agent8-cert.pem'),
|
||||
key: fixtures.readKey('agent8-key.pem'),
|
||||
}, common.mustCall((req, res) => {
|
||||
res.end('Hello world');
|
||||
}, common.isWindows ? 2 : 3));
|
||||
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
|
||||
server.listen(0);
|
||||
await once(server, 'listening');
|
||||
|
||||
// Start a minimal proxy server.
|
||||
const { proxy, logs } = createProxyServer();
|
||||
proxy.listen(0);
|
||||
await once(proxy, 'listening');
|
||||
|
||||
const serverHost = `localhost:${server.address().port}`;
|
||||
const requestUrl = `https://${serverHost}/test`;
|
||||
const expectedLogs = [{
|
||||
method: 'CONNECT',
|
||||
url: serverHost,
|
||||
headers: {
|
||||
'proxy-connection': 'keep-alive',
|
||||
'host': serverHost,
|
||||
},
|
||||
}];
|
||||
|
||||
// Check upper-cased HTTPS_PROXY environment variable.
|
||||
{
|
||||
const { code, signal, stderr, stdout } = await runProxiedRequest({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: requestUrl,
|
||||
HTTPS_PROXY: `http://localhost:${proxy.address().port}`,
|
||||
NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'fake-startcom-root-cert.pem'),
|
||||
});
|
||||
assert.deepStrictEqual(logs, expectedLogs);
|
||||
assert.strictEqual(stderr.trim(), '');
|
||||
assert.match(stdout, /Hello world/);
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
}
|
||||
|
||||
// Check lower-cased https_proxy environment variable.
|
||||
{
|
||||
logs.splice(0, logs.length);
|
||||
const { code, signal, stderr, stdout } = await runProxiedRequest({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: requestUrl,
|
||||
https_proxy: `http://localhost:${proxy.address().port}`,
|
||||
NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'fake-startcom-root-cert.pem'),
|
||||
});
|
||||
assert.deepStrictEqual(logs, expectedLogs);
|
||||
assert.strictEqual(stderr.trim(), '');
|
||||
assert.match(stdout, /Hello world/);
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
}
|
||||
|
||||
// Check that the lower-cased https_proxy environment variable takes precedence over the
|
||||
// upper-cased HTTPS_PROXY.
|
||||
// On Windows, environment variables are case-insensitive, so this test is not applicable.
|
||||
if (!common.isWindows) {
|
||||
const proxy2 = http.createServer(common.mustNotCall());
|
||||
proxy2.on('connect', common.mustNotCall());
|
||||
proxy2.listen(0);
|
||||
await once(proxy2, 'listening');
|
||||
|
||||
// Check lower-cased http_proxy environment variable takes precedence.
|
||||
logs.splice(0, logs.length);
|
||||
const { code, signal, stderr, stdout } = await runProxiedRequest({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: requestUrl,
|
||||
https_proxy: `http://localhost:${proxy.address().port}`,
|
||||
HTTPS_PROXY: `http://localhost:${proxy2.address().port}`,
|
||||
NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'fake-startcom-root-cert.pem'),
|
||||
}, {
|
||||
stdout: 'Hello world',
|
||||
});
|
||||
assert.deepStrictEqual(logs, expectedLogs);
|
||||
assert.strictEqual(stderr.trim(), '');
|
||||
assert.match(stdout, /Hello world/);
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
proxy2.close();
|
||||
}
|
||||
|
||||
proxy.close();
|
||||
server.close();
|
||||
74
test/client-proxy/test-https-request-proxy-post.mjs
Normal file
74
test/client-proxy/test-https-request-proxy-post.mjs
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import * as common from '../common/index.mjs';
|
||||
import fixtures from '../common/fixtures.js';
|
||||
import assert from 'node:assert';
|
||||
import { once } from 'events';
|
||||
import { createProxyServer, runProxiedPOST } from '../common/proxy-server.js';
|
||||
|
||||
if (!common.hasCrypto)
|
||||
common.skip('missing crypto');
|
||||
|
||||
// https must be dynamically imported so that builds without crypto support
|
||||
// can skip it.
|
||||
const { default: https } = await import('node:https');
|
||||
|
||||
// Start a HTTPS server that creates resources
|
||||
const resources = [];
|
||||
const server = https.createServer({
|
||||
cert: fixtures.readKey('agent8-cert.pem'),
|
||||
key: fixtures.readKey('agent8-key.pem'),
|
||||
}, common.mustCall((req, res) => {
|
||||
let body = '';
|
||||
req.on('data', (chunk) => {
|
||||
body += chunk;
|
||||
});
|
||||
req.on('end', () => {
|
||||
const resource = JSON.parse(body);
|
||||
resource.id = resources.length + 1;
|
||||
resource.secure = true;
|
||||
resources.push(resource);
|
||||
|
||||
res.writeHead(201, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(resource));
|
||||
});
|
||||
}));
|
||||
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
|
||||
server.listen(0);
|
||||
await once(server, 'listening');
|
||||
|
||||
// Start a minimal proxy server.
|
||||
const { proxy, logs } = createProxyServer();
|
||||
proxy.listen(0);
|
||||
await once(proxy, 'listening');
|
||||
|
||||
const serverHost = `localhost:${server.address().port}`;
|
||||
const requestUrl = `https://${serverHost}/secure-resources`;
|
||||
const resourceData = JSON.stringify({ name: 'secure-resource', confidential: true });
|
||||
|
||||
const { code, signal, stderr, stdout } = await runProxiedPOST({
|
||||
NODE_USE_ENV_PROXY: 1,
|
||||
REQUEST_URL: requestUrl,
|
||||
RESOURCE_DATA: resourceData,
|
||||
HTTPS_PROXY: `http://localhost:${proxy.address().port}`,
|
||||
NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'fake-startcom-root-cert.pem'),
|
||||
});
|
||||
|
||||
assert.strictEqual(code, 0);
|
||||
assert.strictEqual(signal, null);
|
||||
assert.strictEqual(stderr.trim(), '');
|
||||
assert.match(stdout, /Status Code: 201/);
|
||||
|
||||
// Verify the resource was created securely
|
||||
const jsonMatch = stdout.match(/{[^}]*}$/);
|
||||
assert(jsonMatch, 'Should have JSON response');
|
||||
const response = JSON.parse(jsonMatch[0]);
|
||||
assert.strictEqual(response.name, 'secure-resource');
|
||||
assert.strictEqual(response.secure, true);
|
||||
assert.strictEqual(response.id, 1);
|
||||
|
||||
// Verify proxy logged the CONNECT request (for HTTPS tunneling)
|
||||
assert.strictEqual(logs.length, 1);
|
||||
assert.strictEqual(logs[0].method, 'CONNECT');
|
||||
assert.strictEqual(logs[0].url, serverHost);
|
||||
|
||||
proxy.close();
|
||||
server.close();
|
||||
|
|
@ -41,7 +41,10 @@ exports.createProxyServer = function(options = {}) {
|
|||
port: targetPort,
|
||||
path: url.pathname + url.search, // Convert back to relative URL.
|
||||
method: req.method,
|
||||
headers: req.headers,
|
||||
headers: {
|
||||
...req.headers,
|
||||
'connection': req.headers['proxy-connection'] || 'close',
|
||||
},
|
||||
};
|
||||
|
||||
const proxyReq = http.request(options, (proxyRes) => {
|
||||
|
|
@ -162,3 +165,27 @@ exports.checkProxiedFetch = async function(envExtension, expectation) {
|
|||
...expectation,
|
||||
});
|
||||
};
|
||||
|
||||
exports.runProxiedRequest = async function(envExtension) {
|
||||
const fixtures = require('./fixtures');
|
||||
return spawnPromisified(
|
||||
process.execPath,
|
||||
[fixtures.path('request-and-log.js')], {
|
||||
env: {
|
||||
...process.env,
|
||||
...envExtension,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
exports.runProxiedPOST = async function(envExtension) {
|
||||
const fixtures = require('./fixtures');
|
||||
return spawnPromisified(
|
||||
process.execPath,
|
||||
[fixtures.path('post-resource-and-log.js')], {
|
||||
env: {
|
||||
...process.env,
|
||||
...envExtension,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
|||
50
test/fixtures/post-resource-and-log.js
vendored
Normal file
50
test/fixtures/post-resource-and-log.js
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
const url = process.env.REQUEST_URL;
|
||||
const resourceData = process.env.RESOURCE_DATA || '{"name":"test","value":"data"}';
|
||||
|
||||
let lib;
|
||||
if (url.startsWith('https')) {
|
||||
lib = require('https');
|
||||
} else {
|
||||
lib = require('http');
|
||||
}
|
||||
|
||||
let timeout;
|
||||
if (process.env.REQUEST_TIMEOUT) {
|
||||
timeout = parseInt(process.env.REQUEST_TIMEOUT, 10);
|
||||
}
|
||||
|
||||
let agent;
|
||||
if (process.env.AGENT_TIMEOUT) {
|
||||
agent = new lib.Agent({
|
||||
proxyEnv: process.env,
|
||||
timeout: parseInt(process.env.AGENT_TIMEOUT, 10)
|
||||
});
|
||||
}
|
||||
|
||||
const options = {
|
||||
method: 'POST',
|
||||
timeout,
|
||||
agent,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(resourceData)
|
||||
}
|
||||
};
|
||||
|
||||
const req = lib.request(url, options, (res) => {
|
||||
console.log(`Status Code: ${res.statusCode}`);
|
||||
console.log('Headers:', res.headers);
|
||||
res.pipe(process.stdout);
|
||||
});
|
||||
|
||||
req.on('error', (e) => {
|
||||
console.error('Request Error', e);
|
||||
});
|
||||
|
||||
req.on('timeout', () => {
|
||||
console.error('Request timed out');
|
||||
req.destroy();
|
||||
});
|
||||
|
||||
req.write(resourceData);
|
||||
req.end();
|
||||
24
test/fixtures/proxy-server-worker.js
vendored
Normal file
24
test/fixtures/proxy-server-worker.js
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
'use strict';
|
||||
|
||||
const { parentPort } = require('worker_threads');
|
||||
const { createProxyServer } = require('../common/proxy-server');
|
||||
|
||||
const { proxy, logs } = createProxyServer();
|
||||
proxy.listen(0);
|
||||
proxy.on('listening', () => {
|
||||
parentPort.postMessage({
|
||||
type: 'proxy-listening',
|
||||
port: proxy.address().port,
|
||||
});
|
||||
});
|
||||
|
||||
parentPort.on('message', (msg) => {
|
||||
console.log('Received message from main thread:', msg.type);
|
||||
if (msg.type === 'stop-proxy') {
|
||||
parentPort.postMessage({
|
||||
type: 'proxy-stopped',
|
||||
logs,
|
||||
});
|
||||
proxy.close();
|
||||
}
|
||||
});
|
||||
55
test/fixtures/request-and-log.js
vendored
Normal file
55
test/fixtures/request-and-log.js
vendored
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
const url = process.env.REQUEST_URL;
|
||||
|
||||
let lib;
|
||||
if (url.startsWith('https')) {
|
||||
lib = require('https');
|
||||
} else {
|
||||
lib = require('http');
|
||||
}
|
||||
|
||||
const request = lib.get;
|
||||
|
||||
let timeout;
|
||||
if (process.env.REQUEST_TIMEOUT) {
|
||||
timeout = parseInt(process.env.REQUEST_TIMEOUT, 10);
|
||||
}
|
||||
let agent;
|
||||
if (process.env.AGENT_TIMEOUT) {
|
||||
agent = new lib.Agent({
|
||||
proxyEnv: process.env,
|
||||
timeout: parseInt(process.env.AGENT_TIMEOUT, 10)
|
||||
});
|
||||
}
|
||||
|
||||
let lookup;
|
||||
if (process.env.RESOLVE_TO_LOCALHOST) {
|
||||
lookup = (hostname, options, callback) => {
|
||||
if (hostname === process.env.RESOLVE_TO_LOCALHOST) {
|
||||
console.log(`Resolving lookup for ${hostname} to 127.0.0.1`);
|
||||
return callback(null, [{ address: '127.0.0.1', family: 4 }]);
|
||||
}
|
||||
return require('dns').lookup(hostname, options, callback);
|
||||
};
|
||||
}
|
||||
|
||||
const req = request(url, {
|
||||
timeout,
|
||||
agent,
|
||||
lookup,
|
||||
}, (res) => {
|
||||
// Log the status code
|
||||
console.log(`Status Code: ${res.statusCode}`);
|
||||
console.log('Headers:', res.headers);
|
||||
res.pipe(process.stdout);
|
||||
});
|
||||
|
||||
req.on('error', (e) => {
|
||||
console.error('Request Error', e);
|
||||
});
|
||||
|
||||
req.on('timeout', () => {
|
||||
console.error('Request timed out');
|
||||
req.destroy();
|
||||
});
|
||||
|
||||
req.end();
|
||||
Loading…
Reference in New Issue
Block a user