fix(client-certificates): improve close handling from target and proxy (#32158)

This commit is contained in:
Max Schmitt 2024-08-15 10:21:10 +02:00 committed by GitHub
parent aac3a84321
commit a1d32d997c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -79,20 +79,32 @@ class SocksProxyConnection {
target!: net.Socket; target!: net.Socket;
// In case of http, we just pipe data to the target socket and they are |undefined|. // In case of http, we just pipe data to the target socket and they are |undefined|.
internal: stream.Duplex | undefined; internal: stream.Duplex | undefined;
internalTLS: tls.TLSSocket | undefined;
private _targetCloseEventListener: () => void; private _targetCloseEventListener: () => void;
private _dummyServer: tls.Server | undefined;
private _closed = false;
constructor(socksProxy: ClientCertificatesProxy, uid: string, host: string, port: number) { constructor(socksProxy: ClientCertificatesProxy, uid: string, host: string, port: number) {
this.socksProxy = socksProxy; this.socksProxy = socksProxy;
this.uid = uid; this.uid = uid;
this.host = host; this.host = host;
this.port = port; this.port = port;
this._targetCloseEventListener = () => this.socksProxy._socksProxy.sendSocketEnd({ uid: this.uid }); this._targetCloseEventListener = () => {
// Close the other end and cleanup TLS resources.
this.socksProxy._socksProxy.sendSocketEnd({ uid: this.uid });
this.internalTLS?.destroy();
this._dummyServer?.close();
};
} }
async connect() { async connect() {
this.target = await createSocket(rewriteToLocalhostIfNeeded(this.host), this.port); this.target = await createSocket(rewriteToLocalhostIfNeeded(this.host), this.port);
this.target.once('close', this._targetCloseEventListener); this.target.once('close', this._targetCloseEventListener);
this.target.once('error', error => this.socksProxy._socksProxy.sendSocketError({ uid: this.uid, error: error.message })); this.target.once('error', error => this.socksProxy._socksProxy.sendSocketError({ uid: this.uid, error: error.message }));
if (this._closed) {
this.target.destroy();
return;
}
this.socksProxy._socksProxy.socketConnected({ this.socksProxy._socksProxy.socketConnected({
uid: this.uid, uid: this.uid,
host: this.target.localAddress!, host: this.target.localAddress!,
@ -101,8 +113,11 @@ class SocksProxyConnection {
} }
public onClose() { public onClose() {
this.internal?.destroy(); // Close the other end and cleanup TLS resources.
this.target.destroy(); this.target.destroy();
this.internalTLS?.destroy();
this._dummyServer?.close();
this._closed = true;
} }
public onData(data: Buffer) { public onData(data: Buffer) {
@ -132,20 +147,18 @@ class SocksProxyConnection {
}); });
this.socksProxy.alpnCache.get(rewriteToLocalhostIfNeeded(this.host), this.port, alpnProtocolChosenByServer => { this.socksProxy.alpnCache.get(rewriteToLocalhostIfNeeded(this.host), this.port, alpnProtocolChosenByServer => {
debugLogger.log('client-certificates', `Proxy->Target ${this.host}:${this.port} chooses ALPN ${alpnProtocolChosenByServer}`); debugLogger.log('client-certificates', `Proxy->Target ${this.host}:${this.port} chooses ALPN ${alpnProtocolChosenByServer}`);
const dummyServer = tls.createServer({ if (this._closed)
return;
this._dummyServer = tls.createServer({
...dummyServerTlsOptions, ...dummyServerTlsOptions,
ALPNProtocols: alpnProtocolChosenByServer === 'h2' ? ['h2', 'http/1.1'] : ['http/1.1'], ALPNProtocols: alpnProtocolChosenByServer === 'h2' ? ['h2', 'http/1.1'] : ['http/1.1'],
}); });
this.internal?.once('close', () => dummyServer.close()); this._dummyServer.emit('connection', this.internal);
dummyServer.emit('connection', this.internal); this._dummyServer.once('secureConnection', internalTLS => {
dummyServer.once('secureConnection', internalTLS => { this.internalTLS = internalTLS;
debugLogger.log('client-certificates', `Browser->Proxy ${this.host}:${this.port} chooses ALPN ${internalTLS.alpnProtocol}`); debugLogger.log('client-certificates', `Browser->Proxy ${this.host}:${this.port} chooses ALPN ${internalTLS.alpnProtocol}`);
let targetTLS: tls.TLSSocket | undefined = undefined; let targetTLS: tls.TLSSocket | undefined = undefined;
const closeBothSockets = () => {
internalTLS.end();
targetTLS?.end();
};
const handleError = (error: Error) => { const handleError = (error: Error) => {
error = rewriteOpenSSLErrorIfNeeded(error); error = rewriteOpenSSLErrorIfNeeded(error);
@ -156,7 +169,8 @@ class SocksProxyConnection {
// This method is available only in Node.js 20+ // This method is available only in Node.js 20+
if ('performServerHandshake' in http2) { if ('performServerHandshake' in http2) {
// In case of an 'error' event on the target connection, we still need to perform the http2 handshake on the browser side. // In case of an 'error' event on the target connection, we still need to perform the http2 handshake on the browser side.
// This is an async operation, so we need to intercept the close event to prevent the socket from being closed too early. // This is an async operation, so we need to remove the listener to prevent the socket from being closed too early.
// This means we call this._targetCloseEventListener manually.
this.target.removeListener('close', this._targetCloseEventListener); this.target.removeListener('close', this._targetCloseEventListener);
// @ts-expect-error // @ts-expect-error
const session: http2.ServerHttp2Session = http2.performServerHandshake(internalTLS); const session: http2.ServerHttp2Session = http2.performServerHandshake(internalTLS);
@ -165,14 +179,16 @@ class SocksProxyConnection {
'content-type': 'text/html', 'content-type': 'text/html',
[http2.constants.HTTP2_HEADER_STATUS]: 503, [http2.constants.HTTP2_HEADER_STATUS]: 503,
}); });
stream.end(responseBody, () => { const cleanup = () => {
session.close(); session.close();
closeBothSockets(); this.target.destroy();
}); this._targetCloseEventListener();
stream.once('error', () => closeBothSockets()); };
stream.end(responseBody, cleanup);
stream.once('error', cleanup);
}); });
} else { } else {
closeBothSockets(); this.target.destroy();
} }
} else { } else {
internalTLS.end([ internalTLS.end([
@ -182,7 +198,7 @@ class SocksProxyConnection {
'', '',
responseBody, responseBody,
].join('\r\n')); ].join('\r\n'));
closeBothSockets(); this.target.destroy();
} }
}; };
@ -194,7 +210,11 @@ class SocksProxyConnection {
return; return;
} }
const tlsOptions: tls.ConnectionOptions = { if (this._closed) {
internalTLS.destroy();
return;
}
targetTLS = tls.connect({
socket: this.target, socket: this.target,
host: this.host, host: this.host,
port: this.port, port: this.port,
@ -202,18 +222,14 @@ class SocksProxyConnection {
ALPNProtocols: [internalTLS.alpnProtocol || 'http/1.1'], ALPNProtocols: [internalTLS.alpnProtocol || 'http/1.1'],
servername: !net.isIP(this.host) ? this.host : undefined, servername: !net.isIP(this.host) ? this.host : undefined,
secureContext, secureContext,
}; });
targetTLS = tls.connect(tlsOptions);
targetTLS.once('secureConnect', () => { targetTLS.once('secureConnect', () => {
internalTLS.pipe(targetTLS); internalTLS.pipe(targetTLS);
targetTLS.pipe(internalTLS); targetTLS.pipe(internalTLS);
}); });
internalTLS.once('close', () => closeBothSockets()); internalTLS.once('error', () => this.target.destroy());
internalTLS.once('error', () => closeBothSockets());
targetTLS.once('error', handleError); targetTLS.once('error', handleError);
}); });
}); });