smartproxy/ts/forwarding/handlers/https-passthrough-handler.ts

185 lines
6.1 KiB
TypeScript
Raw Permalink Normal View History

2025-05-09 17:00:27 +00:00
import * as plugins from '../../plugins.js';
import { ForwardingHandler } from './base-handler.js';
import type { IForwardConfig } from '../config/forwarding-types.js';
2025-05-09 17:00:27 +00:00
import { ForwardingHandlerEvents } from '../config/forwarding-types.js';
import { createIndependentSocketHandlers, setupSocketHandlers, createSocketWithErrorHandler } from '../../core/utils/socket-utils.js';
2025-05-09 17:00:27 +00:00
/**
* Handler for HTTPS passthrough (SNI forwarding without termination)
*/
export class HttpsPassthroughHandler extends ForwardingHandler {
/**
* Create a new HTTPS passthrough handler
* @param config The forwarding configuration
*/
constructor(config: IForwardConfig) {
2025-05-09 17:00:27 +00:00
super(config);
2025-05-09 17:00:27 +00:00
// Validate that this is an HTTPS passthrough configuration
if (config.type !== 'https-passthrough') {
throw new Error(`Invalid configuration type for HttpsPassthroughHandler: ${config.type}`);
}
}
/**
* Initialize the handler
* HTTPS passthrough handler doesn't need special initialization
*/
public async initialize(): Promise<void> {
// Basic initialization from parent class
await super.initialize();
}
2025-05-09 17:00:27 +00:00
/**
* Handle a TLS/SSL socket connection by forwarding it without termination
* @param clientSocket The incoming socket from the client
*/
public handleConnection(clientSocket: plugins.net.Socket): void {
// Get the target from configuration
const target = this.getTargetFromConfig();
// Log the connection
const remoteAddress = clientSocket.remoteAddress || 'unknown';
const remotePort = clientSocket.remotePort || 0;
this.emit(ForwardingHandlerEvents.CONNECTED, {
remoteAddress,
remotePort,
target: `${target.host}:${target.port}`
});
// Track data transfer for logging
let bytesSent = 0;
let bytesReceived = 0;
let serverSocket: plugins.net.Socket | null = null;
let cleanupClient: ((reason: string) => Promise<void>) | null = null;
let cleanupServer: ((reason: string) => Promise<void>) | null = null;
2025-05-09 17:00:27 +00:00
// Create a connection to the target server with immediate error handling
serverSocket = createSocketWithErrorHandler({
port: target.port,
host: target.host,
onError: async (error) => {
// Server connection failed - clean up client socket immediately
this.emit(ForwardingHandlerEvents.ERROR, {
error: error.message,
code: (error as any).code || 'UNKNOWN',
remoteAddress,
target: `${target.host}:${target.port}`
});
// Clean up the client socket since we can't forward
if (!clientSocket.destroyed) {
clientSocket.destroy();
}
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
remoteAddress,
bytesSent: 0,
bytesReceived: 0,
reason: `server_connection_failed: ${error.message}`
});
},
onConnect: () => {
// Connection successful - set up forwarding handlers
const handlers = createIndependentSocketHandlers(
clientSocket,
serverSocket!,
(reason) => {
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
remoteAddress,
bytesSent,
bytesReceived,
reason
});
}
);
cleanupClient = handlers.cleanupClient;
cleanupServer = handlers.cleanupServer;
2025-05-09 17:00:27 +00:00
// Setup handlers with custom timeout handling that doesn't close connections
const timeout = this.getTimeout();
setupSocketHandlers(clientSocket, cleanupClient, (socket) => {
// Just reset timeout, don't close
socket.setTimeout(timeout);
}, 'client');
setupSocketHandlers(serverSocket!, cleanupServer, (socket) => {
// Just reset timeout, don't close
socket.setTimeout(timeout);
}, 'server');
// Forward data from client to server
clientSocket.on('data', (data) => {
bytesSent += data.length;
// Check if server socket is writable
if (serverSocket && serverSocket.writable) {
const flushed = serverSocket.write(data);
// Handle backpressure
if (!flushed) {
clientSocket.pause();
serverSocket.once('drain', () => {
clientSocket.resume();
});
}
}
this.emit(ForwardingHandlerEvents.DATA_FORWARDED, {
direction: 'outbound',
bytes: data.length,
total: bytesSent
2025-05-09 17:00:27 +00:00
});
});
2025-05-09 17:00:27 +00:00
// Forward data from server to client
serverSocket!.on('data', (data) => {
bytesReceived += data.length;
// Check if client socket is writable
if (clientSocket.writable) {
const flushed = clientSocket.write(data);
// Handle backpressure
if (!flushed) {
serverSocket!.pause();
clientSocket.once('drain', () => {
serverSocket!.resume();
});
}
}
this.emit(ForwardingHandlerEvents.DATA_FORWARDED, {
direction: 'inbound',
bytes: data.length,
total: bytesReceived
2025-05-09 17:00:27 +00:00
});
});
// Set initial timeouts - they will be reset on each timeout event
clientSocket.setTimeout(timeout);
serverSocket!.setTimeout(timeout);
2025-05-09 17:00:27 +00:00
}
});
}
/**
* Handle an HTTP request - HTTPS passthrough doesn't support HTTP
* @param req The HTTP request
* @param res The HTTP response
*/
public handleHttpRequest(_req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void {
2025-05-09 17:00:27 +00:00
// HTTPS passthrough doesn't support HTTP requests
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('HTTP not supported for this domain');
this.emit(ForwardingHandlerEvents.HTTP_RESPONSE, {
statusCode: 404,
headers: { 'Content-Type': 'text/plain' },
size: 'HTTP not supported for this domain'.length
});
}
}