diff --git a/README.md b/README.md index 7a805eb1..44934f7a 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ const server = new ProxyChain.Server({ // requiring Basic authentication. Here you can verify user credentials. requestAuthentication: username !== 'bob' || password !== 'TopSecret', - // Sets up an upstream HTTP/SOCKS proxy to which all the requests are forwarded. + // Sets up an upstream HTTP/HTTPS/SOCKS proxy to which all the requests are forwarded. // If null, the proxy works in direct mode, i.e. the connection is forwarded directly // to the target server. This field is ignored if "requestAuthentication" is true. // The username and password must be URI-encoded. @@ -76,6 +76,10 @@ const server = new ProxyChain.Server({ // Or use SOCKS4/5 proxy, e.g. // upstreamProxyUrl: `socks://username:password@proxy.example.com:1080`, + // Applies to HTTPS upstream proxy. If set to true, requests made to the proxy will + // ignore certificate errors. Useful when upstream proxy uses self-signed certificate. By default "false". + ignoreUpstreamProxyCertificate: true + // If "requestAuthentication" is true, you can use the following property // to define a custom error message to return to the client instead of the default "Proxy credentials required" failMsg: 'Bad username or password, please try again.', @@ -368,10 +372,13 @@ The package also provides several utility functions. ### `anonymizeProxy({ url, port }, callback)` -Parses and validates a HTTP proxy URL. If the proxy requires authentication, +Parses and validates a HTTP/HTTPS proxy URL. If the proxy requires authentication, then the function starts an open local proxy server that forwards to the proxy. 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. +For HTTPS proxy with self-signed certificate, set `ignoreProxyCertificate` property of the first argument to `true` to ignore certificate errors in +proxy requests. + The function takes an optional callback that receives the anonymous proxy URL. If no callback is supplied, the function returns a promise that resolves to a String with 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. ### `createTunnel(proxyUrl, targetHost, options, callback)` -Creates a TCP tunnel to `targetHost` that goes through a HTTP proxy server +Creates a TCP tunnel to `targetHost` that goes through a HTTP/HTTPS proxy server specified by the `proxyUrl` parameter. The optional `options` parameter is an object with the following properties: - `port: Number` - Enables specifying the local port to listen at. By default `0`, which means a random port will be selected. - `hostname: String` - Local hostname to listen at. By default `localhost`. +- `ignoreProxyCertificate` - For HTTPS proxy, ignore certificate errors in proxy requests. Useful for proxy with self-signed certificate. By default `false`. - `verbose: Boolean` - If `true`, the functions logs a lot. By default `false`. The result of the function is a local endpoint in a form of `hostname:port`. diff --git a/src/anonymize_proxy.ts b/src/anonymize_proxy.ts index 1ef509f0..9b7cbd8e 100644 --- a/src/anonymize_proxy.ts +++ b/src/anonymize_proxy.ts @@ -12,10 +12,12 @@ const anonymizedProxyUrlToServer: Record = {}; export interface AnonymizeProxyOptions { url: string; port: number; + ignoreProxyCertificate?: boolean; } /** - * Parses and validates a HTTP proxy URL. If the proxy requires authentication, then the function + * Parses and validates a HTTP proxy URL. If the proxy requires authentication, + * or if it is an HTTPS proxy and `ignoreProxyCertificate` is `true`, then the function * starts an open local proxy server that forwards to the upstream proxy. */ export const anonymizeProxy = async ( @@ -24,6 +26,7 @@ export const anonymizeProxy = async ( ): Promise => { let proxyUrl: string; let port = 0; + let ignoreProxyCertificate = false; if (typeof options === 'string') { proxyUrl = options; @@ -36,15 +39,19 @@ export const anonymizeProxy = async ( 'Invalid "port" option: only values equals or between 0-65535 are valid', ); } + + if (options.ignoreProxyCertificate !== undefined) { + ignoreProxyCertificate = options.ignoreProxyCertificate; + } } const parsedProxyUrl = new URL(proxyUrl); - if (!['http:', ...SOCKS_PROTOCOLS].includes(parsedProxyUrl.protocol)) { - throw new Error(`Invalid "proxyUrl" provided: URL must have one of the following protocols: "http", ${SOCKS_PROTOCOLS.map((p) => `"${p.replace(':', '')}"`).join(', ')} (was "${parsedProxyUrl}")`); + if (!['http:', 'https:', ...SOCKS_PROTOCOLS].includes(parsedProxyUrl.protocol)) { + 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}")`); } - // If upstream proxy requires no password, return it directly - if (!parsedProxyUrl.username && !parsedProxyUrl.password) { + // If upstream proxy requires no password or if there is no need to ignore HTTPS proxy cert errors, return it directly + if (!parsedProxyUrl.username && !parsedProxyUrl.password && (!ignoreProxyCertificate || parsedProxyUrl.protocol !== 'https:')) { return nodeify(Promise.resolve(proxyUrl), callback); } @@ -60,6 +67,7 @@ export const anonymizeProxy = async ( return { requestAuthentication: false, upstreamProxyUrl: proxyUrl, + ignoreUpstreamProxyCertificate: ignoreProxyCertificate, }; }, }) as Server & { port: number }; diff --git a/src/chain.ts b/src/chain.ts index f1a3576c..86cb8962 100644 --- a/src/chain.ts +++ b/src/chain.ts @@ -22,6 +22,7 @@ interface Options { export interface HandlerOpts { upstreamProxyUrlParsed: URL; + ignoreUpstreamProxyCertificate: boolean; localAddress?: string; ipFamily?: number; dnsLookup?: typeof dns['lookup']; @@ -83,8 +84,12 @@ export const chain = ( options.headers.push('proxy-authorization', getBasicAuthorizationHeader(proxy)); } - const fn = proxy.protocol === 'https:' ? https.request : http.request; - const client = fn(proxy.origin, options as unknown as http.ClientRequestArgs); + const client = proxy.protocol === 'https:' + ? https.request(proxy.origin, { + ...options as unknown as https.RequestOptions, + rejectUnauthorized: !handlerOpts.ignoreUpstreamProxyCertificate, + }) + : http.request(proxy.origin, options as unknown as http.RequestOptions); client.once('socket', (targetSocket: SocketWithPreviousStats) => { // Socket can be re-used by multiple requests. diff --git a/src/forward.ts b/src/forward.ts index a9e66d54..b7c656f6 100644 --- a/src/forward.ts +++ b/src/forward.ts @@ -25,6 +25,7 @@ interface Options { export interface HandlerOpts { upstreamProxyUrlParsed: URL; + ignoreUpstreamProxyCertificate: boolean; localAddress?: string; ipFamily?: number; dnsLookup?: typeof dns['lookup']; @@ -79,10 +80,7 @@ export const forward = async ( } } - const fn = origin!.startsWith('https:') ? https.request : http.request; - - // We have to force cast `options` because @types/node doesn't support an array. - const client = fn(origin!, options as unknown as http.ClientRequestArgs, async (clientResponse) => { + const requestCallback = async (clientResponse: http.IncomingMessage) => { try { // This is necessary to prevent Node.js throwing an error let statusCode = clientResponse.statusCode!; @@ -113,7 +111,16 @@ export const forward = async ( // Client error, pipeline already destroys the streams, ignore. resolve(); } - }); + }; + + // We have to force cast `options` because @types/node doesn't support an array. + const client = origin!.startsWith('https:') + ? https.request(origin!, { + ...options as unknown as https.RequestOptions, + rejectUnauthorized: handlerOpts.upstreamProxyUrlParsed ? !handlerOpts.ignoreUpstreamProxyCertificate : undefined, + }, requestCallback) + + : http.request(origin!, options as unknown as http.RequestOptions, requestCallback); client.once('socket', (socket: SocketWithPreviousStats) => { // Socket can be re-used by multiple requests. diff --git a/src/server.ts b/src/server.ts index 23247f24..41fba80d 100644 --- a/src/server.ts +++ b/src/server.ts @@ -55,6 +55,7 @@ type HandlerOpts = { srcHead: Buffer | null; trgParsed: URL | null; upstreamProxyUrlParsed: URL | null; + ignoreUpstreamProxyCertificate: boolean; isHttp: boolean; customResponseFunction?: CustomResponseOpts['customResponseFunction'] | null; customConnectServer?: http.Server | null; @@ -80,6 +81,7 @@ export type PrepareRequestFunctionResult = { requestAuthentication?: boolean; failMsg?: string; upstreamProxyUrl?: string | null; + ignoreUpstreamProxyCertificate?: boolean; localAddress?: string; ipFamily?: number; dnsLookup?: typeof dns['lookup']; @@ -341,6 +343,7 @@ export class Server extends EventEmitter { srcHead: null, trgParsed: null, upstreamProxyUrlParsed: null, + ignoreUpstreamProxyCertificate: false, isHttp: false, srcResponse: null, customResponseFunction: null, @@ -468,6 +471,10 @@ export class Server extends EventEmitter { } } + if (funcResult.ignoreUpstreamProxyCertificate !== undefined) { + handlerOpts.ignoreUpstreamProxyCertificate = funcResult.ignoreUpstreamProxyCertificate; + } + const { proxyChainId } = request.socket as Socket; if (funcResult.customResponseFunction) { diff --git a/src/tcp_tunnel_tools.ts b/src/tcp_tunnel_tools.ts index e9b40a19..f3c2e001 100644 --- a/src/tcp_tunnel_tools.ts +++ b/src/tcp_tunnel_tools.ts @@ -19,14 +19,15 @@ const getAddress = (server: net.Server) => { export async function createTunnel( proxyUrl: string, targetHost: string, - options: { + options?: { verbose?: boolean; + ignoreProxyCertificate?: boolean; }, callback?: (error: Error | null, result?: string) => void, ): Promise { const parsedProxyUrl = new URL(proxyUrl); - if (parsedProxyUrl.protocol !== 'http:') { - throw new Error(`The proxy URL must have the "http" protocol (was "${proxyUrl}")`); + if (!['http:', 'https:'].includes(parsedProxyUrl.protocol)) { + throw new Error(`The proxy URL must have the "http" or "https" protocol (was "${proxyUrl}")`); } const url = new URL(`connect://${targetHost || ''}`); @@ -67,7 +68,10 @@ export async function createTunnel( chain({ request: { url: targetHost }, sourceSocket, - handlerOpts: { upstreamProxyUrlParsed: parsedProxyUrl }, + handlerOpts: { + upstreamProxyUrlParsed: parsedProxyUrl, + ignoreUpstreamProxyCertificate: options?.ignoreProxyCertificate ?? false, + }, server: server as net.Server & { log: typeof log }, isPlain: true, });