Skip to content
Open
Show file tree
Hide file tree
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
11 changes: 4 additions & 7 deletions src/anonymize_proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import type net from 'node:net';
import { URL } from 'node:url';

import { Server, SOCKS_PROTOCOLS } from './server.js';
import { nodeify } from './utils/nodeify.js';

// Dictionary, key is value returned from anonymizeProxy(), value is Server instance.
const anonymizedProxyUrlToServer: Record<string, Server> = {};
Expand All @@ -22,7 +21,6 @@ export interface AnonymizeProxyOptions {
*/
export const anonymizeProxy = async (
options: string | AnonymizeProxyOptions,
callback?: (error: Error | null) => void,
): Promise<string> => {
let proxyUrl: string;
let port = 0;
Expand Down Expand Up @@ -52,7 +50,7 @@ export const anonymizeProxy = async (

// 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);
return proxyUrl;
}

let server: Server & { port: number };
Expand Down Expand Up @@ -82,7 +80,7 @@ export const anonymizeProxy = async (
return url;
});

return nodeify(promise, callback);
return promise;
};

/**
Expand All @@ -94,23 +92,22 @@ export const anonymizeProxy = async (
export const closeAnonymizedProxy = async (
anonymizedProxyUrl: string,
closeConnections: boolean,
callback?: (error: Error | null, result?: boolean) => void,
): Promise<boolean> => {
if (typeof anonymizedProxyUrl !== 'string') {
throw new Error('The "anonymizedProxyUrl" parameter must be a string');
}

const server = anonymizedProxyUrlToServer[anonymizedProxyUrl];
if (!server) {
return nodeify(Promise.resolve(false), callback);
return false;
}

delete anonymizedProxyUrlToServer[anonymizedProxyUrl];

const promise = server.close(closeConnections).then(() => {
return true;
});
return nodeify(promise, callback);
return promise;
};

type Callback = ({
Expand Down
23 changes: 5 additions & 18 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import { RequestError } from './request_error.js';
import type { Socket, TLSSocket } from './socket.js';
import { badGatewayStatusCodes } from './statuses.js';
import { getTargetStats } from './utils/count_target_bytes.js';
import { nodeify } from './utils/nodeify.js';
import { normalizeUrlPort } from './utils/normalize_url_port.js';
import { parseAuthorizationHeader } from './utils/parse_authorization_header.js';
import { redactUrl } from './utils/redact_url.js';
Expand Down Expand Up @@ -666,12 +665,10 @@ export class Server extends EventEmitter {
/**
* Starts listening at a port specified in the constructor.
*/
async listen(callback?: (error: NodeJS.ErrnoException | null) => void): Promise<void> {
const promise = new Promise<void>((resolve, reject) => {
// Unfortunately server.listen() is not a normal function that fails on error,
// so we need this trickery
async listen(): Promise<void> {
return new Promise((resolve, reject) => {
const onError = (error: NodeJS.ErrnoException) => {
this.log(null, `Listen failed: ${error}`);
this.log(null, `Listen error: ${error}`);
removeListeners();
reject(error);
};
Expand All @@ -690,8 +687,6 @@ export class Server extends EventEmitter {
this.server.on('listening', onListening);
this.server.listen(this.port, this.host);
});

return nodeify(promise, callback);
}

/**
Expand Down Expand Up @@ -751,12 +746,7 @@ export class Server extends EventEmitter {
* Closes the proxy server.
* @param closeConnections If true, pending proxy connections are forcibly closed.
*/
async close(closeConnections: boolean, callback?: (error: NodeJS.ErrnoException | null) => void): Promise<void> {
if (typeof closeConnections === 'function') {
callback = closeConnections;
closeConnections = false;
}

async close(closeConnections = false): Promise<void> {
if (closeConnections) {
this.closeConnections();
}
Expand All @@ -765,10 +755,7 @@ export class Server extends EventEmitter {
const { server } = this;
// @ts-expect-error Let's make sure we can't access the server anymore.
this.server = null;
const promise = util.promisify(server.close).bind(server)();
return nodeify(promise, callback);
await util.promisify(server.close).bind(server)();
}

return nodeify(Promise.resolve(), callback);
}
}
9 changes: 3 additions & 6 deletions src/tcp_tunnel_tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import net from 'node:net';
import { URL } from 'node:url';

import { chain } from './chain.js';
import { nodeify } from './utils/nodeify.js';

const runningServers: Record<string, { server: net.Server, connections: Set<net.Socket> }> = {};

Expand All @@ -23,7 +22,6 @@ export async function createTunnel(
verbose?: boolean;
ignoreProxyCertificate?: boolean;
},
callback?: (error: Error | null, result?: string) => void,
): Promise<string> {
const parsedProxyUrl = new URL(proxyUrl);
if (!['http:', 'https:'].includes(parsedProxyUrl.protocol)) {
Expand Down Expand Up @@ -94,13 +92,12 @@ export async function createTunnel(
});
});

return nodeify(promise, callback);
return promise;
}

export async function closeTunnel(
serverPath: string,
closeConnections: boolean | undefined,
callback: (error: Error | null, result?: boolean) => void,
closeConnections?: boolean,
): Promise<boolean> {
const { hostname, port } = new URL(`tcp://${serverPath}`);
if (!hostname) throw new Error('serverPath must contain hostname');
Expand Down Expand Up @@ -131,5 +128,5 @@ export async function closeTunnel(
});
}));

return nodeify(promise, callback);
return promise;
}
16 changes: 0 additions & 16 deletions src/utils/nodeify.ts

This file was deleted.

185 changes: 67 additions & 118 deletions test/anonymize_proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,129 +137,78 @@ describe('utils.anonymizeProxy', function () {
expectThrowsAsync(async () => { await anonymizeProxy({ url: 'https://whatever.com' }); });
});

it('keeps already anonymous proxies (both with callbacks and promises)', () => {
return Promise.resolve()
.then(() => {
return anonymizeProxy('http://whatever:4567');
})
.then((anonymousProxyUrl) => {
expect(anonymousProxyUrl).to.eql('http://whatever:4567');
})
.then(() => {
return new Promise((resolve, reject) => {
anonymizeProxy('http://whatever:4567', (err, result) => {
if (err) return reject(err);
resolve(result);
});
});
})
.then((anonymousProxyUrl) => {
expect(anonymousProxyUrl).to.eql('http://whatever:4567');
});
});
it('keeps already anonymous proxies', async () => {
const anonymousProxyUrl = await anonymizeProxy('http://whatever:4567');
expect(anonymousProxyUrl).to.eql('http://whatever:4567');

it('anonymizes authenticated upstream proxy (both with callbacks and promises)', () => {
let proxyUrl1;
let proxyUrl2;
return Promise.resolve()
.then(() => {
return Promise.all([
anonymizeProxy(`http://${proxyAuth.username}:${proxyAuth.password}@127.0.0.1:${proxyPort}`),
new Promise((resolve, reject) => {
anonymizeProxy(`http://${proxyAuth.username}:${proxyAuth.password}@127.0.0.1:${proxyPort}`, (err, result) => {
if (err) return reject(err);
resolve(result);
});
}),
]);
})
.then((results) => {
[proxyUrl1, proxyUrl2] = results;
expect(proxyUrl1).to.not.contain(`${proxyPort}`);
expect(proxyUrl2).to.not.contain(`${proxyPort}`);
expect(proxyUrl1).to.not.equal(proxyUrl2);
const anonymousProxyUrl2 = await anonymizeProxy('http://whatever:4567');
expect(anonymousProxyUrl2).to.eql('http://whatever:4567');
});

// Test call through proxy 1
wasProxyCalled = false;
return requestPromised({
uri: `http://localhost:${testServerPort}`,
proxy: proxyUrl1,
expectBodyContainsText: 'Hello World!',
});
})
.then(() => {
expect(wasProxyCalled).to.equal(true);
})
.then(() => {
// Test call through proxy 2
wasProxyCalled = false;
return requestPromised({
uri: `http://localhost:${testServerPort}`,
proxy: proxyUrl2,
expectBodyContainsText: 'Hello World!',
});
})
.then(() => {
expect(wasProxyCalled).to.equal(true);
})
.then(() => {
// Test again call through proxy 1
wasProxyCalled = false;
return requestPromised({
uri: `http://localhost:${testServerPort}`,
proxy: proxyUrl1,
expectBodyContainsText: 'Hello World!',
});
})
.then(() => {
expect(wasProxyCalled).to.equal(true);
})
.then(() => closeAnonymizedProxy(proxyUrl1, true))
.then((closed) => {
expect(closed).to.eql(true);
it('anonymizes authenticated upstream proxy', async () => {
const [proxyUrl1, proxyUrl2] = await Promise.all([
anonymizeProxy(`http://${proxyAuth.username}:${proxyAuth.password}@127.0.0.1:${proxyPort}`),
anonymizeProxy(`http://${proxyAuth.username}:${proxyAuth.password}@127.0.0.1:${proxyPort}`),
]);

expect(proxyUrl1).to.not.contain(`${proxyPort}`);
expect(proxyUrl2).to.not.contain(`${proxyPort}`);
expect(proxyUrl1).to.not.equal(proxyUrl2);

// Test call through proxy 1
wasProxyCalled = false;
await requestPromised({
uri: `http://localhost:${testServerPort}`,
proxy: proxyUrl1,
expectBodyContainsText: 'Hello World!',
});
expect(wasProxyCalled).to.equal(true);

// Test call through proxy 2
wasProxyCalled = false;
await requestPromised({
uri: `http://localhost:${testServerPort}`,
proxy: proxyUrl2,
expectBodyContainsText: 'Hello World!',
});
expect(wasProxyCalled).to.equal(true);

// Test again call through proxy 1
wasProxyCalled = false;
await requestPromised({
uri: `http://localhost:${testServerPort}`,
proxy: proxyUrl1,
expectBodyContainsText: 'Hello World!',
});
expect(wasProxyCalled).to.equal(true);

// Test proxy is really closed
return requestPromised({
uri: proxyUrl1,
})
.then(() => {
assert.fail();
})
.catch((err) => {
// Node.js 20+ may return 'socket hang up' instead of 'ECONNREFUSED'
const validErrors = ['ECONNREFUSED', 'socket hang up'];
expect(validErrors.some((e) => err.message.includes(e))).to.equal(true);
});
})
.then(() => {
// Test callback-style
return new Promise((resolve, reject) => {
closeAnonymizedProxy(proxyUrl2, true, (err, closed) => {
if (err) return reject(err);
resolve(closed);
});
});
})
.then((closed) => {
expect(closed).to.eql(true);
// Close proxy 1 and verify
const closed1 = await closeAnonymizedProxy(proxyUrl1, true);
expect(closed1).to.eql(true);

// Test the second-time call to close
return closeAnonymizedProxy(proxyUrl1, true);
})
.then((closed) => {
expect(closed).to.eql(false);

// Test callback-style
return new Promise((resolve, reject) => {
closeAnonymizedProxy(proxyUrl2, false, (err, closed2) => {
if (err) return reject(err);
resolve(closed2);
});
});
})
.then((closed) => {
expect(closed).to.eql(false);
// Test proxy is really closed
try {
await requestPromised({
uri: proxyUrl1,
});
assert.fail();
} catch (err) {
// Node.js 20+ may return 'socket hang up' instead of 'ECONNREFUSED'
const validErrors = ['ECONNREFUSED', 'socket hang up'];
expect(validErrors.some((e) => err.message.includes(e))).to.equal(true);
}

// Close proxy 2
const closed2 = await closeAnonymizedProxy(proxyUrl2, true);
expect(closed2).to.eql(true);

// Test the second-time call to close (should return false)
const closed1Again = await closeAnonymizedProxy(proxyUrl1, true);
expect(closed1Again).to.eql(false);

// Test another second-time call to close
const closed2Again = await closeAnonymizedProxy(proxyUrl2, false);
expect(closed2Again).to.eql(false);
});

it('handles many concurrent calls without port collision', () => {
Expand Down
Loading
Loading