Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 114 additions & 95 deletions packages/https-proxy-agent/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import * as net from 'net';
import * as tls from 'tls';
import * as http from 'http';
import assert from 'assert';
import createDebug from 'debug';
import { Agent, AgentConnectOpts } from 'agent-base';
import { URL } from 'url';
import { parseProxyResponse } from './parse-proxy-response';
import type { OutgoingHttpHeaders } from 'http';

import * as assert from 'assert';
const debug = createDebug('https-proxy-agent');

const setServernameFromNonIpHost = <
Expand Down Expand Up @@ -100,108 +99,128 @@ export class HttpsProxyAgent<Uri extends string> extends Agent {
req: http.ClientRequest,
opts: AgentConnectOpts
): Promise<net.Socket> {
const { proxy } = this;
return new Promise((resolve, reject) => {
let connectionTimeout: NodeJS.Timeout | undefined;
if (this.connectOpts?.timeout) {
connectionTimeout = setTimeout(() => {
socket?.destroy();
reject(new Error('Proxy connection timeout'));
}, this.connectOpts.timeout);
}

if (!opts.host) {
throw new TypeError('No "host" provided');
}
const { proxy } = this;
// Create a socket connection to the proxy server.
let socket: net.Socket;
if (proxy.protocol === 'https:') {
debug('Creating `tls.Socket`: %o', this.connectOpts);
socket = tls.connect(
setServernameFromNonIpHost(this.connectOpts)
);
} else {
debug('Creating `net.Socket`: %o', this.connectOpts);
socket = net.connect(this.connectOpts);
}

// Create a socket connection to the proxy server.
let socket: net.Socket;
if (proxy.protocol === 'https:') {
debug('Creating `tls.Socket`: %o', this.connectOpts);
socket = tls.connect(setServernameFromNonIpHost(this.connectOpts));
} else {
debug('Creating `net.Socket`: %o', this.connectOpts);
socket = net.connect(this.connectOpts);
}
const headers: OutgoingHttpHeaders =
typeof this.proxyHeaders === 'function'
? this.proxyHeaders()
: { ...this.proxyHeaders };

const headers: OutgoingHttpHeaders =
typeof this.proxyHeaders === 'function'
? this.proxyHeaders()
: { ...this.proxyHeaders };
const 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(
auth
).toString('base64')}`;
}
if (!opts.host) {
reject(new TypeError('No "host" provided'));
}

headers.Host = `${host}:${opts.port}`;
const host = net.isIPv6(opts.host as string)
? `[${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(
auth
).toString('base64')}`;
}

if (!headers['Proxy-Connection']) {
headers['Proxy-Connection'] = this.keepAlive
? 'Keep-Alive'
: 'close';
}
for (const name of Object.keys(headers)) {
payload += `${name}: ${headers[name]}\r\n`;
}
headers.Host = `${host}:${opts.port}`;

const proxyResponsePromise = parseProxyResponse(socket);

socket.write(`${payload}\r\n`);

const { connect, buffered } = await proxyResponsePromise;
req.emit('proxyConnect', connect);
this.emit('proxyConnect', connect, req);

if (connect.statusCode === 200) {
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');
return tls.connect({
...omit(
setServernameFromNonIpHost(opts),
'host',
'path',
'port'
),
socket,
});
if (!headers['Proxy-Connection']) {
headers['Proxy-Connection'] = this.keepAlive
? 'Keep-Alive'
: 'close';
}
for (const name of Object.keys(headers)) {
payload += `${name}: ${headers[name]}\r\n`;
}

return socket;
}

// Some other status code that's not 200... need to re-play the HTTP
// header "data" events onto the socket once the HTTP machinery is
// attached so that the node core `http` can parse and handle the
// error status code.

// Close the original socket, and a new "fake" socket is returned
// instead, so that the proxy doesn't get the HTTP request
// written to it (which may contain `Authorization` headers or other
// sensitive data).
//
// See: https://hackerone.com/reports/541502
socket.destroy();

const fakeSocket = new net.Socket({ writable: false });
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);

// Replay the "buffered" Buffer onto the fake `socket`, since at
// this point the HTTP module machinery has been hooked up for
// the user.
s.push(buffered);
s.push(null);
socket.write(`${payload}\r\n`);

parseProxyResponse(socket)
.then(({ connect, buffered }) => {
req.emit('proxyConnect', connect);
this.emit('proxyConnect', connect, req);

clearTimeout(connectionTimeout);

if (connect.statusCode === 200) {

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');
return resolve(
tls.connect({
...omit(
setServernameFromNonIpHost(opts),
'host',
'path',
'port'
),
socket,
})
);
}

return resolve(socket);
}

// Some other status code that's not 200... need to re-play the HTTP
// header "data" events onto the socket once the HTTP machinery is
// attached so that the node core `http` can parse and handle the
// error status code.

// Close the original socket, and a new "fake" socket is returned
// instead, so that the proxy doesn't get the HTTP request
// written to it (which may contain `Authorization` headers or other
// sensitive data).
//
// See: https://hackerone.com/reports/541502
socket.destroy();

const fakeSocket = new net.Socket({ writable: false });
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.ok(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
// the user.
s.push(buffered);
s.push(null);
});

return resolve(fakeSocket);
})
.catch(reject);
});

return fakeSocket;
}
}

Expand Down