From 3848d23fd9fe83b19772ff91ef13e994e52f073e Mon Sep 17 00:00:00 2001 From: Maxime Bargiel Date: Fri, 7 Oct 2022 06:01:01 -0400 Subject: [PATCH] Add support for proxyRequestOptions (#72) --- README.md | 27 ++++++++++++ index.d.ts | 15 +++++-- index.js | 12 ++++-- test/http-http.test.js | 91 ++++++++++++++++++++++++++++++++++++++++ test/http-https.test.js | 91 ++++++++++++++++++++++++++++++++++++++++ test/https-http.test.js | 91 ++++++++++++++++++++++++++++++++++++++++ test/https-https.test.js | 91 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 411 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 511ebf9..fa3cc50 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,33 @@ http.get('http://localhost:9200', { agent }) .end() ``` +You can also pass custom options intended only for the proxy CONNECT request with the `proxyConnectOptions` option, +such as headers or `tls.connect()` options: + +```js +const fs = require('fs') +const http = require('http') +const { HttpProxyAgent } = require('hpagent') + +const agent = new HttpProxyAgent({ + keepAlive: true, + keepAliveMsecs: 1000, + maxSockets: 256, + maxFreeSockets: 256, + proxy: 'https://localhost:8080', + proxyConnectOptions: { + headers: { + 'Proxy-Authorization': 'Basic YWxhZGRpbjpvcGVuc2VzYW1l', + }, + ca: [ fs.readFileSync('custom-proxy-cert.pem') ] + } +}) + +http.get('http://localhost:9200', { agent }) + .on('response', console.log) + .end() +``` + ## Integrations Following you can find the list of userland http libraries that are tested with this agent. diff --git a/index.d.ts b/index.d.ts index a61e969..6d5c9af 100644 --- a/index.d.ts +++ b/index.d.ts @@ -7,7 +7,8 @@ declare class HttpProxyAgent extends http.Agent { } interface HttpProxyAgentOptions extends http.AgentOptions { - proxy: string | URL + proxy: string | URL, + proxyRequestOptions?: ProxyAgentRequestOptions } declare class HttpsProxyAgent extends https.Agent { @@ -15,12 +16,20 @@ declare class HttpsProxyAgent extends https.Agent { } interface HttpsProxyAgentOptions extends https.AgentOptions { - proxy: string | URL + proxy: string | URL, + proxyRequestOptions?: ProxyAgentRequestOptions +} + +interface ProxyAgentRequestOptions { + ca?: string[], + headers?: Object, + rejectUnauthorized?: boolean } export { HttpProxyAgent, HttpProxyAgentOptions, HttpsProxyAgent, - HttpsProxyAgentOptions + HttpsProxyAgentOptions, + ProxyAgentRequestOptions, } diff --git a/index.js b/index.js index 3ee4afe..3484628 100644 --- a/index.js +++ b/index.js @@ -6,21 +6,23 @@ const { URL } = require('url') class HttpProxyAgent extends http.Agent { constructor (options) { - const { proxy, ...opts } = options + const { proxy, proxyRequestOptions, ...opts } = options super(opts) this.proxy = typeof proxy === 'string' ? new URL(proxy) : proxy + this.proxyRequestOptions = proxyRequestOptions || {} } createConnection (options, callback) { const requestOptions = { + ...this.proxyRequestOptions, method: 'CONNECT', host: this.proxy.hostname, port: this.proxy.port, path: `${options.host}:${options.port}`, setHost: false, - headers: { connection: this.keepAlive ? 'keep-alive' : 'close', host: `${options.host}:${options.port}` }, + headers: { ...this.proxyRequestOptions.headers, connection: this.keepAlive ? 'keep-alive' : 'close', host: `${options.host}:${options.port}` }, agent: false, timeout: options.timeout || 0 } @@ -60,21 +62,23 @@ class HttpProxyAgent extends http.Agent { class HttpsProxyAgent extends https.Agent { constructor (options) { - const { proxy, ...opts } = options + const { proxy, proxyRequestOptions, ...opts } = options super(opts) this.proxy = typeof proxy === 'string' ? new URL(proxy) : proxy + this.proxyRequestOptions = proxyRequestOptions || {} } createConnection (options, callback) { const requestOptions = { + ...this.proxyRequestOptions, method: 'CONNECT', host: this.proxy.hostname, port: this.proxy.port, path: `${options.host}:${options.port}`, setHost: false, - headers: { connection: this.keepAlive ? 'keep-alive' : 'close', host: `${options.host}:${options.port}` }, + headers: { ...this.proxyRequestOptions.headers, connection: this.keepAlive ? 'keep-alive' : 'close', host: `${options.host}:${options.port}` }, agent: false, timeout: options.timeout || 0 } diff --git a/test/http-http.test.js b/test/http-http.test.js index 666c8ec..9f2b332 100644 --- a/test/http-http.test.js +++ b/test/http-http.test.js @@ -416,3 +416,94 @@ test('Username and password should not be encoded', async t => { server.close() proxy.close() }) + +test('Proxy request options should be passed to the CONNECT request only', async t => { + const server = await createServer() + const proxy = await createProxy() + let serverCustomHeaderReceived + let proxyCustomHeaderReceived + server.on('request', (req, res) => { + serverCustomHeaderReceived = req.headers['x-custom-header'] + return res.end('ok') + }) + proxy.on('connect', (req) => { + proxyCustomHeaderReceived = req.headers['x-custom-header'] + }) + + const response = await request({ + method: 'GET', + hostname: server.address().address, + port: server.address().port, + path: '/', + agent: new HttpProxyAgent({ + proxyRequestOptions: { + headers: { + 'x-custom-header': 'value' + } + }, + keepAlive: true, + keepAliveMsecs: 1000, + maxSockets: 256, + maxFreeSockets: 256, + scheduling: 'lifo', + proxy: `http://${proxy.address().address}:${proxy.address().port}` + }) + }) + + let body = '' + response.setEncoding('utf8') + for await (const chunk of response) { + body += chunk + } + + t.is(body, 'ok') + t.is(response.statusCode, 200) + t.falsy(serverCustomHeaderReceived) + t.is(proxyCustomHeaderReceived, 'value') + + server.close() + proxy.close() +}) + +test('Proxy request options should not override internal default options for CONNECT request', async t => { + const server = await createServer() + const proxy = await createProxy() + let proxyConnectionHeaderReceived + server.on('request', (req, res) => res.end('ok')) + proxy.on('connect', (req) => { + proxyConnectionHeaderReceived = req.headers.connection + }) + + const response = await request({ + method: 'GET', + hostname: server.address().address, + port: server.address().port, + path: '/', + agent: new HttpProxyAgent({ + proxyRequestOptions: { + headers: { + connection: 'close' + } + }, + keepAlive: true, + keepAliveMsecs: 1000, + maxSockets: 256, + maxFreeSockets: 256, + scheduling: 'lifo', + proxy: `http://${proxy.address().address}:${proxy.address().port}` + }) + }) + + let body = '' + response.setEncoding('utf8') + for await (const chunk of response) { + body += chunk + } + + t.is(body, 'ok') + t.is(response.statusCode, 200) + t.is(proxyConnectionHeaderReceived, 'keep-alive') + + server.close() + proxy.close() +}) diff --git a/test/http-https.test.js b/test/http-https.test.js index 90f2e90..c5a943a 100644 --- a/test/http-https.test.js +++ b/test/http-https.test.js @@ -341,3 +341,94 @@ test('Timeout', async t => { server.close() proxy.close() }) + +test('Proxy request options should be passed to the CONNECT request only', async t => { + const server = await createSecureServer() + const proxy = await createProxy() + let serverCustomHeaderReceived + let proxyCustomHeaderReceived + server.on('request', (req, res) => { + serverCustomHeaderReceived = req.headers['x-custom-header'] + return res.end('ok') + }) + proxy.on('connect', (req) => { + proxyCustomHeaderReceived = req.headers['x-custom-header'] + }) + + const response = await request({ + method: 'GET', + hostname: SERVER_HOSTNAME, + port: server.address().port, + path: '/', + agent: new HttpsProxyAgent({ + proxyRequestOptions: { + headers: { + 'x-custom-header': 'value' + } + }, + keepAlive: true, + keepAliveMsecs: 1000, + maxSockets: 256, + maxFreeSockets: 256, + scheduling: 'lifo', + proxy: `http://${proxy.address().address}:${proxy.address().port}` + }) + }) + + let body = '' + response.setEncoding('utf8') + for await (const chunk of response) { + body += chunk + } + + t.is(body, 'ok') + t.is(response.statusCode, 200) + t.falsy(serverCustomHeaderReceived) + t.is(proxyCustomHeaderReceived, 'value') + + server.close() + proxy.close() +}) + +test('Proxy request options should not override internal default options for CONNECT request', async t => { + const server = await createSecureServer() + const proxy = await createProxy() + let proxyConnectionHeaderReceived + server.on('request', (req, res) => res.end('ok')) + proxy.on('connect', (req) => { + proxyConnectionHeaderReceived = req.headers.connection + }) + + const response = await request({ + method: 'GET', + hostname: SERVER_HOSTNAME, + port: server.address().port, + path: '/', + agent: new HttpsProxyAgent({ + proxyRequestOptions: { + headers: { + connection: 'close' + } + }, + keepAlive: true, + keepAliveMsecs: 1000, + maxSockets: 256, + maxFreeSockets: 256, + scheduling: 'lifo', + proxy: `http://${proxy.address().address}:${proxy.address().port}` + }) + }) + + let body = '' + response.setEncoding('utf8') + for await (const chunk of response) { + body += chunk + } + + t.is(body, 'ok') + t.is(response.statusCode, 200) + t.is(proxyConnectionHeaderReceived, 'keep-alive') + + server.close() + proxy.close() +}) diff --git a/test/https-http.test.js b/test/https-http.test.js index 570fcbc..1a29dee 100644 --- a/test/https-http.test.js +++ b/test/https-http.test.js @@ -341,3 +341,94 @@ test('Timeout', async t => { server.close() proxy.close() }) + +test('Proxy request options should be passed to the CONNECT request only', async t => { + const server = await createServer() + const proxy = await createSecureProxy() + let serverCustomHeaderReceived + let proxyCustomHeaderReceived + server.on('request', (req, res) => { + serverCustomHeaderReceived = req.headers['x-custom-header'] + return res.end('ok') + }) + proxy.on('connect', (req) => { + proxyCustomHeaderReceived = req.headers['x-custom-header'] + }) + + const response = await request({ + method: 'GET', + hostname: server.address().address, + port: server.address().port, + path: '/', + agent: new HttpProxyAgent({ + proxyRequestOptions: { + headers: { + 'x-custom-header': 'value' + } + }, + keepAlive: true, + keepAliveMsecs: 1000, + maxSockets: 256, + maxFreeSockets: 256, + scheduling: 'lifo', + proxy: `https://${PROXY_HOSTNAME}:${proxy.address().port}` + }) + }) + + let body = '' + response.setEncoding('utf8') + for await (const chunk of response) { + body += chunk + } + + t.is(body, 'ok') + t.is(response.statusCode, 200) + t.falsy(serverCustomHeaderReceived) + t.is(proxyCustomHeaderReceived, 'value') + + server.close() + proxy.close() +}) + +test('Proxy request options should not override internal default options for CONNECT request', async t => { + const server = await createServer() + const proxy = await createSecureProxy() + let proxyConnectionHeaderReceived + server.on('request', (req, res) => res.end('ok')) + proxy.on('connect', (req) => { + proxyConnectionHeaderReceived = req.headers.connection + }) + + const response = await request({ + method: 'GET', + hostname: server.address().address, + port: server.address().port, + path: '/', + agent: new HttpProxyAgent({ + proxyRequestOptions: { + headers: { + connection: 'close' + } + }, + keepAlive: true, + keepAliveMsecs: 1000, + maxSockets: 256, + maxFreeSockets: 256, + scheduling: 'lifo', + proxy: `https://${PROXY_HOSTNAME}:${proxy.address().port}` + }) + }) + + let body = '' + response.setEncoding('utf8') + for await (const chunk of response) { + body += chunk + } + + t.is(body, 'ok') + t.is(response.statusCode, 200) + t.is(proxyConnectionHeaderReceived, 'keep-alive') + + server.close() + proxy.close() +}) diff --git a/test/https-https.test.js b/test/https-https.test.js index fff5fbc..2e4356c 100644 --- a/test/https-https.test.js +++ b/test/https-https.test.js @@ -378,3 +378,94 @@ test('Username and password should not be encoded', async t => { server.close() proxy.close() }) + +test('Proxy request options should be passed to the CONNECT request only', async t => { + const server = await createSecureServer() + const proxy = await createSecureProxy() + let serverCustomHeaderReceived + let proxyCustomHeaderReceived + server.on('request', (req, res) => { + serverCustomHeaderReceived = req.headers['x-custom-header'] + return res.end('ok') + }) + proxy.on('connect', (req) => { + proxyCustomHeaderReceived = req.headers['x-custom-header'] + }) + + const response = await request({ + method: 'GET', + hostname: SERVER_HOSTNAME, + port: server.address().port, + path: '/', + agent: new HttpsProxyAgent({ + proxyRequestOptions: { + headers: { + 'x-custom-header': 'value' + } + }, + keepAlive: true, + keepAliveMsecs: 1000, + maxSockets: 256, + maxFreeSockets: 256, + scheduling: 'lifo', + proxy: `https://${PROXY_HOSTNAME}:${proxy.address().port}` + }) + }) + + let body = '' + response.setEncoding('utf8') + for await (const chunk of response) { + body += chunk + } + + t.is(body, 'ok') + t.is(response.statusCode, 200) + t.falsy(serverCustomHeaderReceived) + t.is(proxyCustomHeaderReceived, 'value') + + server.close() + proxy.close() +}) + +test('Proxy request options should not override internal default options for CONNECT request', async t => { + const server = await createSecureServer() + const proxy = await createSecureProxy() + let proxyConnectionHeaderReceived + server.on('request', (req, res) => res.end('ok')) + proxy.on('connect', (req) => { + proxyConnectionHeaderReceived = req.headers.connection + }) + + const response = await request({ + method: 'GET', + hostname: SERVER_HOSTNAME, + port: server.address().port, + path: '/', + agent: new HttpsProxyAgent({ + proxyRequestOptions: { + headers: { + connection: 'close' + } + }, + keepAlive: true, + keepAliveMsecs: 1000, + maxSockets: 256, + maxFreeSockets: 256, + scheduling: 'lifo', + proxy: `https://${PROXY_HOSTNAME}:${proxy.address().port}` + }) + }) + + let body = '' + response.setEncoding('utf8') + for await (const chunk of response) { + body += chunk + } + + t.is(body, 'ok') + t.is(response.statusCode, 200) + t.is(proxyConnectionHeaderReceived, 'keep-alive') + + server.close() + proxy.close() +})