From 106993219c43b282655a94a298d738da5dbcfb99 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Thu, 18 May 2023 15:28:40 -0400 Subject: [PATCH] [http-proxy-agent] Support dynamic `headers` option (#175) --- .changeset/unlucky-cows-listen.md | 5 +++ packages/http-proxy-agent/README.md | 19 +++++++++ packages/http-proxy-agent/src/index.ts | 54 ++++++++++++++++++++------ packages/http-proxy-agent/test/test.ts | 32 +++++++++++++++ 4 files changed, 99 insertions(+), 11 deletions(-) create mode 100644 .changeset/unlucky-cows-listen.md diff --git a/.changeset/unlucky-cows-listen.md b/.changeset/unlucky-cows-listen.md new file mode 100644 index 00000000..bcaa546d --- /dev/null +++ b/.changeset/unlucky-cows-listen.md @@ -0,0 +1,5 @@ +--- +'http-proxy-agent': minor +--- + +Added "headers" option diff --git a/packages/http-proxy-agent/README.md b/packages/http-proxy-agent/README.md index ff8523fa..5d47db10 100644 --- a/packages/http-proxy-agent/README.md +++ b/packages/http-proxy-agent/README.md @@ -24,6 +24,25 @@ http.get('http://nodejs.org/api/', { agent }, (res) => { }); ``` +API +--- + +### new HttpProxyAgent(proxy: string | URL, options?: HttpProxyAgentOptions) + +The `HttpProxyAgent` class implements an `http.Agent` subclass that connects +to the specified "HTTP(s) proxy server" in order to proxy HTTP requests. + +The `proxy` argument is the URL for the proxy server. + +The `options` argument accepts the usual `http.Agent` constructor options, and +some additional properties: + + * `headers` - Object containing additional headers to send to the proxy server + in each request. This may also be a function that returns a headers object. + + **NOTE:** If your proxy does not strip these headers from the request, they + will also be sent to the destination server. + License ------- diff --git a/packages/http-proxy-agent/src/index.ts b/packages/http-proxy-agent/src/index.ts index 3b697fa2..3287ab71 100644 --- a/packages/http-proxy-agent/src/index.ts +++ b/packages/http-proxy-agent/src/index.ts @@ -3,6 +3,7 @@ import * as tls from 'tls'; import * as http from 'http'; import createDebug from 'debug'; import { once } from 'events'; +import type { OutgoingHttpHeaders } from 'http'; import { Agent, AgentConnectOpts } from 'agent-base'; const debug = createDebug('http-proxy-agent'); @@ -21,7 +22,10 @@ type ConnectOpts = { : never; }[keyof ConnectOptsMap]; -export type HttpProxyAgentOptions = ConnectOpts & http.AgentOptions; +export type HttpProxyAgentOptions = ConnectOpts & + http.AgentOptions & { + headers?: OutgoingHttpHeaders | (() => OutgoingHttpHeaders); + }; interface HttpProxyAgentClientRequest extends http.ClientRequest { outputData?: { @@ -43,6 +47,7 @@ export class HttpProxyAgent extends Agent { static protocols = ['http', 'https'] as const; readonly proxy: URL; + proxyHeaders: OutgoingHttpHeaders | (() => OutgoingHttpHeaders); connectOpts: net.TcpNetConnectOpts & tls.ConnectionOptions; get secureProxy() { @@ -52,6 +57,7 @@ export class HttpProxyAgent extends Agent { constructor(proxy: Uri | URL, opts?: HttpProxyAgentOptions) { super(opts); this.proxy = typeof proxy === 'string' ? new URL(proxy) : proxy; + this.proxyHeaders = opts?.headers ?? {}; debug('Creating new HttpProxyAgent instance: %o', this.proxy.href); // Trim off the brackets from IPv6 addresses @@ -65,7 +71,7 @@ export class HttpProxyAgent extends Agent { ? 443 : 80; this.connectOpts = { - ...opts, + ...(opts ? omit(opts, 'headers') : null), host, port, }; @@ -91,21 +97,29 @@ export class HttpProxyAgent extends Agent { // Inject the `Proxy-Authorization` header if necessary. req._header = null; + const headers: OutgoingHttpHeaders = + typeof this.proxyHeaders === 'function' + ? this.proxyHeaders() + : { ...this.proxyHeaders }; if (proxy.username || proxy.password) { const auth = `${decodeURIComponent( proxy.username )}:${decodeURIComponent(proxy.password)}`; - req.setHeader( - 'Proxy-Authorization', - `Basic ${Buffer.from(auth).toString('base64')}` - ); + headers['Proxy-Authorization'] = `Basic ${Buffer.from( + auth + ).toString('base64')}`; } - if (!req.hasHeader('proxy-connection')) { - req.setHeader( - 'Proxy-Connection', - this.keepAlive ? 'Keep-Alive' : 'close' - ); + if (!headers['Proxy-Connection']) { + headers['Proxy-Connection'] = this.keepAlive + ? 'Keep-Alive' + : 'close'; + } + for (const name of Object.keys(headers)) { + const value = headers[name]; + if (value) { + req.setHeader(name, value); + } } // Create a socket connection to the proxy server. @@ -146,3 +160,21 @@ export class HttpProxyAgent extends Agent { return socket; } } + +function omit( + obj: T, + ...keys: K +): { + [K2 in Exclude]: T[K2]; +} { + const ret = {} as { + [K in keyof typeof obj]: (typeof obj)[K]; + }; + let key: keyof typeof obj; + for (key in obj) { + if (!keys.includes(key)) { + ret[key] = obj[key]; + } + } + return ret; +} diff --git a/packages/http-proxy-agent/test/test.ts b/packages/http-proxy-agent/test/test.ts index 3eb5eb68..d509dc01 100644 --- a/packages/http-proxy-agent/test/test.ts +++ b/packages/http-proxy-agent/test/test.ts @@ -176,6 +176,38 @@ describe('HttpProxyAgent', () => { assert(err); expect(err.code).toEqual('ECONNREFUSED'); }); + + it('should allow custom proxy "headers" object', async () => { + httpServer.once('request', (req, res) => { + res.end(JSON.stringify(req.headers)); + }); + const agent = new HttpProxyAgent(proxyUrl, { + headers: { Foo: 'bar' }, + }); + + const res = await req(httpServerUrl, { agent }); + const body = await json(res); + expect(body.foo).toEqual('bar'); + }); + + it('should allow custom proxy "headers" function', async () => { + let count = 1; + httpServer.on('request', (req, res) => { + res.end(JSON.stringify(req.headers)); + }); + const agent = new HttpProxyAgent(proxyUrl, { + headers: () => ({ number: count++ }), + }); + + const res = await req(httpServerUrl, { agent }); + const body = await json(res); + expect(body.number).toEqual('1'); + + const res2 = await req(httpServerUrl, { agent }); + const body2 = await json(res2); + expect(body2.number).toEqual('2'); + }); + it('should not send a port number for the default port', async () => { httpServer.once('request', (req, res) => { res.end(JSON.stringify(req.headers));