2025-05-09 17:00:27 +00:00
|
|
|
import * as plugins from '../../plugins.js';
|
|
|
|
import { ForwardingHandler } from './base-handler.js';
|
2025-05-09 22:46:53 +00:00
|
|
|
import type { IForwardConfig } from '../config/forwarding-types.js';
|
2025-05-09 17:00:27 +00:00
|
|
|
import { ForwardingHandlerEvents } from '../config/forwarding-types.js';
|
2025-06-01 12:27:15 +00:00
|
|
|
import { createIndependentSocketHandlers, setupSocketHandlers } 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
|
|
|
|
*/
|
2025-05-09 22:46:53 +00:00
|
|
|
constructor(config: IForwardConfig) {
|
2025-05-09 17:00:27 +00:00
|
|
|
super(config);
|
2025-05-09 22:46:53 +00:00
|
|
|
|
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}`);
|
|
|
|
}
|
|
|
|
}
|
2025-05-09 22:46:53 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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}`
|
|
|
|
});
|
|
|
|
|
|
|
|
// Create a connection to the target server
|
|
|
|
const serverSocket = plugins.net.connect(target.port, target.host);
|
|
|
|
|
2025-06-01 07:51:20 +00:00
|
|
|
// Track data transfer for logging
|
|
|
|
let bytesSent = 0;
|
|
|
|
let bytesReceived = 0;
|
2025-05-09 17:00:27 +00:00
|
|
|
|
2025-06-01 12:27:15 +00:00
|
|
|
// Create independent handlers for half-open connection support
|
|
|
|
const { cleanupClient, cleanupServer } = createIndependentSocketHandlers(
|
|
|
|
clientSocket,
|
|
|
|
serverSocket,
|
|
|
|
(reason) => {
|
|
|
|
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
|
|
|
|
remoteAddress,
|
|
|
|
bytesSent,
|
|
|
|
bytesReceived,
|
|
|
|
reason
|
|
|
|
});
|
|
|
|
}
|
|
|
|
);
|
2025-05-09 17:00:27 +00:00
|
|
|
|
2025-06-01 12:27:15 +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');
|
2025-05-09 17:00:27 +00:00
|
|
|
|
|
|
|
// Forward data from client to server
|
|
|
|
clientSocket.on('data', (data) => {
|
|
|
|
bytesSent += data.length;
|
|
|
|
|
|
|
|
// Check if server socket is writable
|
|
|
|
if (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
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
// 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-06-01 12:27:15 +00:00
|
|
|
// Set initial timeouts - they will be reset on each timeout event
|
2025-05-09 17:00:27 +00:00
|
|
|
clientSocket.setTimeout(timeout);
|
|
|
|
serverSocket.setTimeout(timeout);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle an HTTP request - HTTPS passthrough doesn't support HTTP
|
|
|
|
* @param req The HTTP request
|
|
|
|
* @param res The HTTP response
|
|
|
|
*/
|
2025-06-01 12:27:15 +00:00
|
|
|
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
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|