Skip to content
Merged
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"prepublishOnly": "npm run build",
"local-proxy": "node ./dist/run_locally.js",
"test": "nyc cross-env NODE_OPTIONS=--insecure-http-parser mocha",
"test:docker": "docker build --tag proxy-chain-tests --file test/Dockerfile . && docker run proxy-chain-tests",
"test:docker": "docker build --tag proxy-chain-tests --file test/Dockerfile . && docker run --add-host localhost-test:127.0.0.1 proxy-chain-tests",
"test:docker:all": "bash scripts/test-docker-all.sh",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
Expand Down
6 changes: 3 additions & 3 deletions scripts/test-docker-all.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
echo "Starting parallel Docker tests for Node 14, 16, and 18..."

# Run builds in parallel, capture PIDs.
docker build --build-arg NODE_IMAGE=node:14.21.3-bullseye --tag proxy-chain-tests:node14 --file test/Dockerfile . && docker run proxy-chain-tests:node14 &
docker build --build-arg NODE_IMAGE=node:14.21.3-bullseye --tag proxy-chain-tests:node14 --file test/Dockerfile . && docker run --add-host localhost-test:127.0.0.1 proxy-chain-tests:node14 &
pid14=$!
docker build --build-arg NODE_IMAGE=node:16.20.2-bookworm --tag proxy-chain-tests:node16 --file test/Dockerfile . && docker run proxy-chain-tests:node16 &
docker build --build-arg NODE_IMAGE=node:16.20.2-bookworm --tag proxy-chain-tests:node16 --file test/Dockerfile . && docker run --add-host localhost-test:127.0.0.1 proxy-chain-tests:node16 &
pid16=$!
docker build --build-arg NODE_IMAGE=node:18.20.8-bookworm --tag proxy-chain-tests:node18 --file test/Dockerfile . && docker run proxy-chain-tests:node18 &
docker build --build-arg NODE_IMAGE=node:18.20.8-bookworm --tag proxy-chain-tests:node18 --file test/Dockerfile . && docker run --add-host localhost-test:127.0.0.1 proxy-chain-tests:node18 &
pid18=$!

# Wait for all and capture exit codes.
Expand Down
199 changes: 105 additions & 94 deletions test/https-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,128 +14,139 @@ it('handles TLS handshake failures gracefully and continues accepting connection
this.timeout(10000);

const tlsErrors = [];
let server;
let badSocket;
let goodSocket;

let server = new Server({
port: 0,
serverType: 'https',
httpsOptions: {
key: sslKey,
cert: sslCrt,
},
});
try {
server = new Server({
port: 0,
serverType: 'https',
httpsOptions: {
key: sslKey,
cert: sslCrt,
},
});

server.on('tlsError', ({ error }) => {
tlsErrors.push(error);
});
server.on('tlsError', ({ error }) => {
tlsErrors.push(error);
});

await server.listen();
const serverPort = server.port;
await server.listen();
const serverPort = server.port;

// Make invalid TLS connection.
const badSocket = tls.connect({
port: serverPort,
host: '127.0.0.1',
rejectUnauthorized: false,
minVersion: 'TLSv1',
maxVersion: 'TLSv1',
});
// Make invalid TLS connection.
badSocket = tls.connect({
port: serverPort,
host: '127.0.0.1',
rejectUnauthorized: false,
minVersion: 'TLSv1',
maxVersion: 'TLSv1',
});

const badSocketErrorOccurred = await new Promise((resolve, reject) => {
let errorOccurred = false;
const badSocketErrorOccurred = await new Promise((resolve, reject) => {
let errorOccurred = false;

badSocket.on('error', () => {
errorOccurred = true;
// Expected: TLS handshake will fail due to version mismatch.
});
badSocket.on('error', () => {
errorOccurred = true;
// Expected: TLS handshake will fail due to version mismatch.
});

badSocket.on('close', () => {
resolve(errorOccurred);
});
badSocket.on('close', () => {
resolve(errorOccurred);
});

badSocket.setTimeout(5000, () => {
badSocket.destroy();
reject(new Error('Bad socket timed out before error'));
});
badSocket.setTimeout(5000, () => {
badSocket.destroy();
reject(new Error('Bad socket timed out before error'));
});

});
});

await wait(100);
await wait(100);

expect(badSocketErrorOccurred).to.equal(true);
expect(badSocketErrorOccurred).to.equal(true);

// Make a valid TLS connection to prove server still works.
const goodSocket = tls.connect({
port: serverPort,
host: '127.0.0.1',
rejectUnauthorized: false,
});
// Make a valid TLS connection to prove server still works.
goodSocket = tls.connect({
port: serverPort,
host: '127.0.0.1',
rejectUnauthorized: false,
});

// Wait for secure connection.
const goodSocketConnected = await new Promise((resolve, reject) => {
let isConnected = false;
// Wait for secure connection.
const goodSocketConnected = await new Promise((resolve, reject) => {
let isConnected = false;

const timeout = setTimeout(() => {
goodSocket.destroy();
reject(new Error('Good socket connection timed out'));
}, 5000);
const timeout = setTimeout(() => {
goodSocket.destroy();
reject(new Error('Good socket connection timed out'));
}, 5000);

goodSocket.on('error', (err) => {
clearTimeout(timeout);
goodSocket.destroy();
reject(err);
});
goodSocket.on('error', (err) => {
clearTimeout(timeout);
goodSocket.destroy();
reject(err);
});

goodSocket.on('secureConnect', () => {
isConnected = true;
clearTimeout(timeout);
resolve(isConnected);
});
goodSocket.on('secureConnect', () => {
isConnected = true;
clearTimeout(timeout);
resolve(isConnected);
});

goodSocket.on('close', () => {
clearTimeout(timeout);
goodSocket.on('close', () => {
clearTimeout(timeout);
});
});
});

expect(goodSocketConnected).to.equal(true, 'Good socket should have connected');
expect(goodSocketConnected).to.equal(true, 'Good socket should have connected');

// Write the CONNECT request.
goodSocket.write('CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\n\r\n');
// Write the CONNECT request.
goodSocket.write('CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\n\r\n');

const response = await new Promise((resolve, reject) => {
const goodSocketTimeout = setTimeout(() => {
goodSocket.destroy();
reject(new Error('Good socket connection timed out'));
}, 5000);
const response = await new Promise((resolve, reject) => {
const goodSocketTimeout = setTimeout(() => {
goodSocket.destroy();
reject(new Error('Good socket connection timed out'));
}, 5000);

goodSocket.on('error', (err) => {
clearTimeout(goodSocketTimeout);
goodSocket.destroy();
reject(err);
});
goodSocket.on('error', (err) => {
clearTimeout(goodSocketTimeout);
goodSocket.destroy();
reject(err);
});

goodSocket.on('data', (data) => {
clearTimeout(goodSocketTimeout);
goodSocket.destroy();
resolve(data.toString());
});
goodSocket.on('data', (data) => {
clearTimeout(goodSocketTimeout);
goodSocket.destroy();
resolve(data.toString());
});

goodSocket.on('close', () => {
clearTimeout(goodSocketTimeout);
goodSocket.on('close', () => {
clearTimeout(goodSocketTimeout);
});
});
});

await wait(100);

expect(response).to.be.equal('HTTP/1.1 200 Connection Established\r\n\r\n');
await wait(100);

expect(tlsErrors.length).to.be.equal(1);
expect(tlsErrors[0].library).to.be.equal('SSL routines');
expect(tlsErrors[0].reason).to.be.equal('unsupported protocol');
expect(tlsErrors[0].code).to.be.equal('ERR_SSL_UNSUPPORTED_PROTOCOL');
expect(response).to.be.equal('HTTP/1.1 200 Connection Established\r\n\r\n');

// Cleanup.
server.close(true);
server = null;
expect(tlsErrors.length).to.be.equal(1);
expect(tlsErrors[0].library).to.be.equal('SSL routines');
expect(tlsErrors[0].reason).to.be.equal('unsupported protocol');
expect(tlsErrors[0].code).to.be.equal('ERR_SSL_UNSUPPORTED_PROTOCOL');
} finally {
if (badSocket && !badSocket.destroyed) {
badSocket.destroy();
}
if (goodSocket && !goodSocket.destroyed) {
goodSocket.destroy();
}
if (server) {
await server.close(true);
}
}
});

describe('HTTPS proxy server resource cleanup', () => {
Expand Down