Skip to content

Commit

Permalink
Add "proxy" and "proxy-agent"
Browse files Browse the repository at this point in the history
  • Loading branch information
TooTallNate committed Apr 28, 2023
1 parent 81139bd commit 86c885e
Show file tree
Hide file tree
Showing 33 changed files with 1,915 additions and 515 deletions.
31 changes: 31 additions & 0 deletions packages/agent-base/src/helpers.ts
@@ -0,0 +1,31 @@
import * as http from "http";
import * as https from "https";
import type { Readable } from "stream";

export async function toBuffer(stream: Readable): Promise<Buffer> {
let length = 0;
const chunks: Buffer[] = [];
for await (const chunk of stream) {
length += chunk.length;
chunks.push(chunk);
}
return Buffer.concat(chunks, length);
}

export async function json(stream: Readable): Promise<Record<string, string>> {
const buf = await toBuffer(stream);
return JSON.parse(buf.toString('utf8'));
}

export function req(
url: string | URL,
opts: https.RequestOptions
): Promise<http.IncomingMessage> {
return new Promise((resolve, reject) => {
const href = typeof url === 'string' ? url : url.href;
(href.startsWith("https:") ? https : http)
.request(url, opts, resolve)
.once("error", reject)
.end();
});
}
2 changes: 2 additions & 0 deletions packages/agent-base/src/index.ts
Expand Up @@ -3,6 +3,8 @@ import * as tls from 'tls';
import * as http from 'http';
import { Duplex } from 'stream';

export * from './helpers';

function isSecureEndpoint(): boolean {
const { stack } = new Error();
if (typeof stack !== 'string') return false;
Expand Down
12 changes: 8 additions & 4 deletions packages/get-uri/src/index.ts
Expand Up @@ -23,11 +23,15 @@ export const protocols = {
https,
};

type Protocols = typeof protocols;
export type Protocols = typeof protocols;

type ProtocolOpts<T> = {
[P in keyof Protocols]: Protocol<T> extends P
? Parameters<Protocols[P]>[1]
export type ProtocolsOptions = {
[P in keyof Protocols]: NonNullable<Parameters<Protocols[P]>[1]>
}

export type ProtocolOpts<T> = {
[P in keyof ProtocolsOptions]: Protocol<T> extends P
? ProtocolsOptions[P]
: never;
}[keyof Protocols];

Expand Down
1 change: 1 addition & 0 deletions packages/http-proxy-agent/.eslintignore
@@ -0,0 +1 @@
dist
2 changes: 1 addition & 1 deletion packages/http-proxy-agent/package.json
Expand Up @@ -37,7 +37,7 @@
"@types/debug": "^4.1.7",
"@types/node": "^14.18.43",
"mocha": "^6.2.3",
"proxy": "^1.0.2",
"proxy": "workspace:*",
"tsconfig": "workspace:*",
"typescript": "^5.0.4"
},
Expand Down
18 changes: 12 additions & 6 deletions packages/http-proxy-agent/src/index.ts
Expand Up @@ -38,6 +38,8 @@ function isHTTPS(protocol?: string | null): boolean {
* to the specified "HTTP proxy server" in order to proxy HTTP requests.
*/
export class HttpProxyAgent<Uri extends string> extends Agent {
static protocols = ['http', 'https'] as const;

readonly proxy: URL;
connectOpts: net.TcpNetConnectOpts & tls.ConnectionOptions;

Expand All @@ -48,9 +50,10 @@ export class HttpProxyAgent<Uri extends string> extends Agent {
constructor(proxy: Uri | URL, opts?: HttpProxyAgentOptions<Uri>) {
super();
this.proxy = typeof proxy === 'string' ? new URL(proxy) : proxy;
debug('Creating new HttpProxyAgent instance: %o', this.proxy);
debug('Creating new HttpProxyAgent instance: %o', this.proxy.href);

const host = this.proxy.hostname || this.proxy.host;
// Trim off the brackets from IPv6 addresses
const host = (this.proxy.hostname || this.proxy.host).replace(/^\[|\]$/g, '');
const port = this.proxy.port
? parseInt(this.proxy.port, 10)
: this.secureProxy
Expand All @@ -70,9 +73,12 @@ export class HttpProxyAgent<Uri extends string> extends Agent {
const { proxy } = this;

const protocol = opts.secureEndpoint ? 'https:' : 'http:';
const hostname = opts.host || req.getHeader('host') || 'localhost';
const base = `${protocol}//${hostname}:${opts.port}`;
const hostname = req.getHeader('host') || 'localhost';
const base = `${protocol}//${hostname}`;
const url = new URL(req.path, base);
if (opts.port !== 80) {
url.port = String(opts.port);
}

// Change the `http.ClientRequest` instance's "path" field
// to the absolute path of the URL that will be requested.
Expand All @@ -93,10 +99,10 @@ export class HttpProxyAgent<Uri extends string> extends Agent {
// Create a socket connection to the proxy server.
let socket: net.Socket;
if (this.secureProxy) {
debug('Creating `tls.Socket`: %o', proxy);
debug('Creating `tls.Socket`: %o', this.connectOpts);
socket = tls.connect(this.connectOpts);
} else {
debug('Creating `net.Socket`: %o', proxy);
debug('Creating `net.Socket`: %o', this.connectOpts);
socket = net.connect(this.connectOpts);
}

Expand Down
41 changes: 9 additions & 32 deletions packages/http-proxy-agent/test/test.js
Expand Up @@ -3,7 +3,7 @@ const url = require('url');
const http = require('http');
const https = require('https');
const assert = require('assert');
const Proxy = require('proxy');
const { createProxy } = require('proxy');
const { Agent } = require('agent-base');
const { HttpProxyAgent } = require('../');

Expand All @@ -21,7 +21,7 @@ describe('HttpProxyAgent', () => {

before((done) => {
// setup HTTP proxy server
proxy = Proxy();
proxy = createProxy();
proxy.listen(() => {
proxyPort = proxy.address().port;
done();
Expand All @@ -47,7 +47,7 @@ describe('HttpProxyAgent', () => {
key: fs.readFileSync(`${__dirname}/ssl-cert-snakeoil.key`),
cert: fs.readFileSync(`${__dirname}/ssl-cert-snakeoil.pem`),
};
sslProxy = Proxy(https.createServer(options));
sslProxy = createProxy(https.createServer(options));
sslProxy.listen(() => {
sslProxyPort = sslProxy.address().port;
done();
Expand Down Expand Up @@ -100,29 +100,12 @@ describe('HttpProxyAgent', () => {
assert.equal(80, agent.defaultPort);
});
describe('secureProxy', () => {
it('should default to `false`', () => {
let agent = new HttpProxyAgent({ port: proxyPort });
assert.equal(false, agent.secureProxy);
});
it('should be `false` when "http:" protocol is used', () => {
let agent = new HttpProxyAgent({
port: proxyPort,
protocol: 'http:',
});
let agent = new HttpProxyAgent(`http://127.0.0.1:${proxyPort}`);
assert.equal(false, agent.secureProxy);
});
it('should be `true` when "https:" protocol is used', () => {
let agent = new HttpProxyAgent({
port: proxyPort,
protocol: 'https:',
});
assert.equal(true, agent.secureProxy);
});
it('should be `true` when "https" protocol is used', () => {
let agent = new HttpProxyAgent({
port: proxyPort,
protocol: 'https',
});
let agent = new HttpProxyAgent(`https://127.0.0.1:${proxyPort}`);
assert.equal(true, agent.secureProxy);
});
});
Expand Down Expand Up @@ -216,11 +199,8 @@ describe('HttpProxyAgent', () => {
});
});
it('should receive the 407 authorization code on the `http.ClientResponse`', (done) => {
// set a proxy authentication function for this test
proxy.authenticate = function (req, fn) {
// reject all requests
fn(null, false);
};
// reject all requests
proxy.authenticate = () => false

let proxyUri = `http://127.0.0.1:${proxyPort}`;
let agent = new HttpProxyAgent(proxyUri);
Expand All @@ -240,12 +220,9 @@ describe('HttpProxyAgent', () => {
});
it('should send the "Proxy-Authorization" request header', (done) => {
// set a proxy authentication function for this test
proxy.authenticate = function (req, fn) {
proxy.authenticate = (req) => {
// username:password is "foo:bar"
fn(
null,
req.headers['proxy-authorization'] === 'Basic Zm9vOmJhcg=='
);
return req.headers['proxy-authorization'] === 'Basic Zm9vOmJhcg=='
};

// set HTTP "request" event handler for this test
Expand Down
2 changes: 1 addition & 1 deletion packages/https-proxy-agent/package.json
Expand Up @@ -38,7 +38,7 @@
"@types/node": "^14.18.43",
"async-listen": "^2.1.0",
"jest": "^29.5.0",
"proxy": "1",
"proxy": "workspace:*",
"ts-jest": "^29.1.0",
"tsconfig": "workspace:*",
"typescript": "^5.0.4"
Expand Down
42 changes: 24 additions & 18 deletions packages/https-proxy-agent/src/index.ts
Expand Up @@ -40,6 +40,8 @@ export type HttpsProxyAgentOptions<T> = ConnectOpts<T> & {
* the connection to the proxy server has been established.
*/
export class HttpsProxyAgent<Uri extends string> extends Agent {
static protocols = ["http", "https"] as const;

readonly proxy: URL;
proxyHeaders: OutgoingHttpHeaders;
connectOpts: net.TcpNetConnectOpts & tls.ConnectionOptions;
Expand All @@ -50,18 +52,19 @@ export class HttpsProxyAgent<Uri extends string> extends Agent {

constructor(proxy: Uri | URL, opts?: HttpsProxyAgentOptions<Uri>) {
super();
this.proxy = typeof proxy === 'string' ? new URL(proxy) : proxy;
this.proxy = typeof proxy === "string" ? new URL(proxy) : proxy;
this.proxyHeaders = opts?.headers ?? {};
debug('Creating new HttpsProxyAgent instance: %o', this.proxy.href);
debug("Creating new HttpsProxyAgent instance: %o", this.proxy.href);

const host = this.proxy.hostname || this.proxy.host;
// Trim off the brackets from IPv6 addresses
const host = (this.proxy.hostname || this.proxy.host).replace(/^\[|\]$/g, '');
const port = this.proxy.port
? parseInt(this.proxy.port, 10)
: this.secureProxy
? 443
: 80;
this.connectOpts = {
...(opts ? omit(opts, 'headers') : null),
...(opts ? omit(opts, "headers") : null),
host,
port,
};
Expand All @@ -77,40 +80,43 @@ export class HttpsProxyAgent<Uri extends string> extends Agent {
): Promise<net.Socket> {
const { proxy, secureProxy } = this;

if (!opts.host) {
throw new TypeError('No "host" provided');
}

// Create a socket connection to the proxy server.
let socket: net.Socket;
if (secureProxy) {
debug('Creating `tls.Socket`: %o', this.connectOpts);
debug("Creating `tls.Socket`: %o", this.connectOpts);
socket = tls.connect(this.connectOpts);
} else {
debug('Creating `net.Socket`: %o', this.connectOpts);
debug("Creating `net.Socket`: %o", this.connectOpts);
socket = net.connect(this.connectOpts);
}

const headers: OutgoingHttpHeaders = { ...this.proxyHeaders };
const hostname = `${opts.host}:${opts.port}`;
let payload = `CONNECT ${hostname} HTTP/1.1\r\n`;
let host = net.isIPv6(opts.host) ? `[${opts.host}]` : opts.host;
let payload = `CONNECT ${host}:${opts.port} HTTP/1.1\r\n`;

// Inject the `Proxy-Authorization` header if necessary.
if (proxy.username || proxy.password) {
const auth = `${decodeURIComponent(
proxy.username
)}:${decodeURIComponent(proxy.password)}`;
headers['Proxy-Authorization'] = `Basic ${Buffer.from(
headers["Proxy-Authorization"] = `Basic ${Buffer.from(
auth
).toString('base64')}`;
).toString("base64")}`;
}

// The `Host` header should only include the port
// number when it is not the default port.
let { host } = opts;
const { port, secureEndpoint } = opts;
if (!isDefaultPort(port, secureEndpoint)) {
host += `:${port}`;
}
headers.Host = host;

headers.Connection = 'close';
headers.Connection = "close";
for (const name of Object.keys(headers)) {
payload += `${name}: ${headers[name]}\r\n`;
}
Expand All @@ -122,15 +128,15 @@ export class HttpsProxyAgent<Uri extends string> extends Agent {
const { statusCode, buffered } = await proxyResponsePromise;

if (statusCode === 200) {
req.once('socket', resume);
req.once("socket", resume);

if (opts.secureEndpoint) {
// The proxy is connecting to a TLS server, so upgrade
// this socket connection to a TLS connection.
debug('Upgrading socket connection to TLS');
debug("Upgrading socket connection to TLS");
const servername = opts.servername || opts.host;
const s = tls.connect({
...omit(opts, 'host', 'path', 'port'),
...omit(opts, "host", "path", "port"),
socket,
servername,
});
Expand Down Expand Up @@ -161,9 +167,9 @@ export class HttpsProxyAgent<Uri extends string> extends Agent {
fakeSocket.readable = true;

// Need to wait for the "socket" event to re-play the "data" events.
req.once('socket', (s: net.Socket) => {
debug('Replaying proxy buffer for failed request');
assert(s.listenerCount('data') > 0);
req.once("socket", (s: net.Socket) => {
debug("Replaying proxy buffer for failed request");
assert(s.listenerCount("data") > 0);

// Replay the "buffered" Buffer onto the fake `socket`, since at
// this point the HTTP module machinery has been hooked up for
Expand Down

0 comments on commit 86c885e

Please sign in to comment.