Skip to content

Commit bad3172

Browse files
authored
Add HTTPS upstream proxy support with option to ignore proxy cert errors (#577)
This PR adds support for a new boolean option `ignoreProxyCertificate`. If set to `true`, certificate errors will be ignored, which can be useful for HTTPS proxies with self-signed certificates. Usage: ``` const server = new Server({ prepareRequestFunction: () => ({ upstreamProxyUrl: 'https://user:pass@myproxy:8080', ignoreUpstreamProxyCertificate: true // Useful for self-signed cert }) }).listen(); ``` Anonymize: ``` anonymizeProxy({ url: 'https://user:pass@myproxy:8080', ignoreProxyCertificate: true, }).then((anonymizedURL) => { console.log(`Anonymized URL: ${anonymizedURL}`); }); ``` Create tunnel: ``` createTunnel( 'https://user:pass@myproxy:8080', targetHost, { ignoreProxyCertificate: true } ).then((url) => { console.log('Tunnel endpoint:', url); }); ```
1 parent 16bca47 commit bad3172

File tree

6 files changed

+58
-19
lines changed

6 files changed

+58
-19
lines changed

README.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,18 @@ const server = new ProxyChain.Server({
6868
// requiring Basic authentication. Here you can verify user credentials.
6969
requestAuthentication: username !== 'bob' || password !== 'TopSecret',
7070

71-
// Sets up an upstream HTTP/SOCKS proxy to which all the requests are forwarded.
71+
// Sets up an upstream HTTP/HTTPS/SOCKS proxy to which all the requests are forwarded.
7272
// If null, the proxy works in direct mode, i.e. the connection is forwarded directly
7373
// to the target server. This field is ignored if "requestAuthentication" is true.
7474
// The username and password must be URI-encoded.
7575
upstreamProxyUrl: `http://username:password@proxy.example.com:3128`,
7676
// Or use SOCKS4/5 proxy, e.g.
7777
// upstreamProxyUrl: `socks://username:password@proxy.example.com:1080`,
7878

79+
// Applies to HTTPS upstream proxy. If set to true, requests made to the proxy will
80+
// ignore certificate errors. Useful when upstream proxy uses self-signed certificate. By default "false".
81+
ignoreUpstreamProxyCertificate: true
82+
7983
// If "requestAuthentication" is true, you can use the following property
8084
// to define a custom error message to return to the client instead of the default "Proxy credentials required"
8185
failMsg: 'Bad username or password, please try again.',
@@ -368,10 +372,13 @@ The package also provides several utility functions.
368372

369373
### `anonymizeProxy({ url, port }, callback)`
370374

371-
Parses and validates a HTTP proxy URL. If the proxy requires authentication,
375+
Parses and validates a HTTP/HTTPS proxy URL. If the proxy requires authentication,
372376
then the function starts an open local proxy server that forwards to the proxy.
373377
The port (on which the local proxy server will start) can be set via the `port` property of the first argument, if not provided, it will be chosen randomly.
374378

379+
For HTTPS proxy with self-signed certificate, set `ignoreProxyCertificate` property of the first argument to `true` to ignore certificate errors in
380+
proxy requests.
381+
375382
The function takes an optional callback that receives the anonymous proxy URL.
376383
If no callback is supplied, the function returns a promise that resolves to a String with
377384
anonymous proxy URL or the original URL if it was already anonymous.
@@ -420,13 +427,14 @@ If callback is not provided, the function returns a promise instead.
420427

421428
### `createTunnel(proxyUrl, targetHost, options, callback)`
422429

423-
Creates a TCP tunnel to `targetHost` that goes through a HTTP proxy server
430+
Creates a TCP tunnel to `targetHost` that goes through a HTTP/HTTPS proxy server
424431
specified by the `proxyUrl` parameter.
425432

426433
The optional `options` parameter is an object with the following properties:
427434
- `port: Number` - Enables specifying the local port to listen at. By default `0`,
428435
which means a random port will be selected.
429436
- `hostname: String` - Local hostname to listen at. By default `localhost`.
437+
- `ignoreProxyCertificate` - For HTTPS proxy, ignore certificate errors in proxy requests. Useful for proxy with self-signed certificate. By default `false`.
430438
- `verbose: Boolean` - If `true`, the functions logs a lot. By default `false`.
431439

432440
The result of the function is a local endpoint in a form of `hostname:port`.

src/anonymize_proxy.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ const anonymizedProxyUrlToServer: Record<string, Server> = {};
1212
export interface AnonymizeProxyOptions {
1313
url: string;
1414
port: number;
15+
ignoreProxyCertificate?: boolean;
1516
}
1617

1718
/**
18-
* Parses and validates a HTTP proxy URL. If the proxy requires authentication, then the function
19+
* Parses and validates a HTTP proxy URL. If the proxy requires authentication,
20+
* or if it is an HTTPS proxy and `ignoreProxyCertificate` is `true`, then the function
1921
* starts an open local proxy server that forwards to the upstream proxy.
2022
*/
2123
export const anonymizeProxy = async (
@@ -24,6 +26,7 @@ export const anonymizeProxy = async (
2426
): Promise<string> => {
2527
let proxyUrl: string;
2628
let port = 0;
29+
let ignoreProxyCertificate = false;
2730

2831
if (typeof options === 'string') {
2932
proxyUrl = options;
@@ -36,15 +39,19 @@ export const anonymizeProxy = async (
3639
'Invalid "port" option: only values equals or between 0-65535 are valid',
3740
);
3841
}
42+
43+
if (options.ignoreProxyCertificate !== undefined) {
44+
ignoreProxyCertificate = options.ignoreProxyCertificate;
45+
}
3946
}
4047

4148
const parsedProxyUrl = new URL(proxyUrl);
42-
if (!['http:', ...SOCKS_PROTOCOLS].includes(parsedProxyUrl.protocol)) {
43-
throw new Error(`Invalid "proxyUrl" provided: URL must have one of the following protocols: "http", ${SOCKS_PROTOCOLS.map((p) => `"${p.replace(':', '')}"`).join(', ')} (was "${parsedProxyUrl}")`);
49+
if (!['http:', 'https:', ...SOCKS_PROTOCOLS].includes(parsedProxyUrl.protocol)) {
50+
throw new Error(`Invalid "proxyUrl" provided: URL must have one of the following protocols: "http", "https", ${SOCKS_PROTOCOLS.map((p) => `"${p.replace(':', '')}"`).join(', ')} (was "${parsedProxyUrl}")`);
4451
}
4552

46-
// If upstream proxy requires no password, return it directly
47-
if (!parsedProxyUrl.username && !parsedProxyUrl.password) {
53+
// If upstream proxy requires no password or if there is no need to ignore HTTPS proxy cert errors, return it directly
54+
if (!parsedProxyUrl.username && !parsedProxyUrl.password && (!ignoreProxyCertificate || parsedProxyUrl.protocol !== 'https:')) {
4855
return nodeify(Promise.resolve(proxyUrl), callback);
4956
}
5057

@@ -60,6 +67,7 @@ export const anonymizeProxy = async (
6067
return {
6168
requestAuthentication: false,
6269
upstreamProxyUrl: proxyUrl,
70+
ignoreUpstreamProxyCertificate: ignoreProxyCertificate,
6371
};
6472
},
6573
}) as Server & { port: number };

src/chain.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ interface Options {
2222

2323
export interface HandlerOpts {
2424
upstreamProxyUrlParsed: URL;
25+
ignoreUpstreamProxyCertificate: boolean;
2526
localAddress?: string;
2627
ipFamily?: number;
2728
dnsLookup?: typeof dns['lookup'];
@@ -83,8 +84,12 @@ export const chain = (
8384
options.headers.push('proxy-authorization', getBasicAuthorizationHeader(proxy));
8485
}
8586

86-
const fn = proxy.protocol === 'https:' ? https.request : http.request;
87-
const client = fn(proxy.origin, options as unknown as http.ClientRequestArgs);
87+
const client = proxy.protocol === 'https:'
88+
? https.request(proxy.origin, {
89+
...options as unknown as https.RequestOptions,
90+
rejectUnauthorized: !handlerOpts.ignoreUpstreamProxyCertificate,
91+
})
92+
: http.request(proxy.origin, options as unknown as http.RequestOptions);
8893

8994
client.once('socket', (targetSocket: SocketWithPreviousStats) => {
9095
// Socket can be re-used by multiple requests.

src/forward.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ interface Options {
2525

2626
export interface HandlerOpts {
2727
upstreamProxyUrlParsed: URL;
28+
ignoreUpstreamProxyCertificate: boolean;
2829
localAddress?: string;
2930
ipFamily?: number;
3031
dnsLookup?: typeof dns['lookup'];
@@ -79,10 +80,7 @@ export const forward = async (
7980
}
8081
}
8182

82-
const fn = origin!.startsWith('https:') ? https.request : http.request;
83-
84-
// We have to force cast `options` because @types/node doesn't support an array.
85-
const client = fn(origin!, options as unknown as http.ClientRequestArgs, async (clientResponse) => {
83+
const requestCallback = async (clientResponse: http.IncomingMessage) => {
8684
try {
8785
// This is necessary to prevent Node.js throwing an error
8886
let statusCode = clientResponse.statusCode!;
@@ -113,7 +111,16 @@ export const forward = async (
113111
// Client error, pipeline already destroys the streams, ignore.
114112
resolve();
115113
}
116-
});
114+
};
115+
116+
// We have to force cast `options` because @types/node doesn't support an array.
117+
const client = origin!.startsWith('https:')
118+
? https.request(origin!, {
119+
...options as unknown as https.RequestOptions,
120+
rejectUnauthorized: handlerOpts.upstreamProxyUrlParsed ? !handlerOpts.ignoreUpstreamProxyCertificate : undefined,
121+
}, requestCallback)
122+
123+
: http.request(origin!, options as unknown as http.RequestOptions, requestCallback);
117124

118125
client.once('socket', (socket: SocketWithPreviousStats) => {
119126
// Socket can be re-used by multiple requests.

src/server.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ type HandlerOpts = {
5555
srcHead: Buffer | null;
5656
trgParsed: URL | null;
5757
upstreamProxyUrlParsed: URL | null;
58+
ignoreUpstreamProxyCertificate: boolean;
5859
isHttp: boolean;
5960
customResponseFunction?: CustomResponseOpts['customResponseFunction'] | null;
6061
customConnectServer?: http.Server | null;
@@ -80,6 +81,7 @@ export type PrepareRequestFunctionResult = {
8081
requestAuthentication?: boolean;
8182
failMsg?: string;
8283
upstreamProxyUrl?: string | null;
84+
ignoreUpstreamProxyCertificate?: boolean;
8385
localAddress?: string;
8486
ipFamily?: number;
8587
dnsLookup?: typeof dns['lookup'];
@@ -341,6 +343,7 @@ export class Server extends EventEmitter {
341343
srcHead: null,
342344
trgParsed: null,
343345
upstreamProxyUrlParsed: null,
346+
ignoreUpstreamProxyCertificate: false,
344347
isHttp: false,
345348
srcResponse: null,
346349
customResponseFunction: null,
@@ -468,6 +471,10 @@ export class Server extends EventEmitter {
468471
}
469472
}
470473

474+
if (funcResult.ignoreUpstreamProxyCertificate !== undefined) {
475+
handlerOpts.ignoreUpstreamProxyCertificate = funcResult.ignoreUpstreamProxyCertificate;
476+
}
477+
471478
const { proxyChainId } = request.socket as Socket;
472479

473480
if (funcResult.customResponseFunction) {

src/tcp_tunnel_tools.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,15 @@ const getAddress = (server: net.Server) => {
1919
export async function createTunnel(
2020
proxyUrl: string,
2121
targetHost: string,
22-
options: {
22+
options?: {
2323
verbose?: boolean;
24+
ignoreProxyCertificate?: boolean;
2425
},
2526
callback?: (error: Error | null, result?: string) => void,
2627
): Promise<string> {
2728
const parsedProxyUrl = new URL(proxyUrl);
28-
if (parsedProxyUrl.protocol !== 'http:') {
29-
throw new Error(`The proxy URL must have the "http" protocol (was "${proxyUrl}")`);
29+
if (!['http:', 'https:'].includes(parsedProxyUrl.protocol)) {
30+
throw new Error(`The proxy URL must have the "http" or "https" protocol (was "${proxyUrl}")`);
3031
}
3132

3233
const url = new URL(`connect://${targetHost || ''}`);
@@ -67,7 +68,10 @@ export async function createTunnel(
6768
chain({
6869
request: { url: targetHost },
6970
sourceSocket,
70-
handlerOpts: { upstreamProxyUrlParsed: parsedProxyUrl },
71+
handlerOpts: {
72+
upstreamProxyUrlParsed: parsedProxyUrl,
73+
ignoreUpstreamProxyCertificate: options?.ignoreProxyCertificate ?? false,
74+
},
7175
server: server as net.Server & { log: typeof log },
7276
isPlain: true,
7377
});

0 commit comments

Comments
 (0)