UNPKG

@push.rocks/smartproxy

Version:

A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.

276 lines 22.1 kB
import * as plugins from '../../plugins.js'; import { ForwardingHandler } from './base-handler.js'; import { ForwardingHandlerEvents } from '../config/forwarding-types.js'; import { setupSocketHandlers, createSocketWithErrorHandler, setupBidirectionalForwarding } from '../../core/utils/socket-utils.js'; /** * Handler for HTTPS termination with HTTP backend */ export class HttpsTerminateToHttpHandler extends ForwardingHandler { /** * Create a new HTTPS termination with HTTP backend handler * @param config The forwarding configuration */ constructor(config) { super(config); this.tlsServer = null; this.secureContext = null; // Validate that this is an HTTPS terminate to HTTP configuration if (config.type !== 'https-terminate-to-http') { throw new Error(`Invalid configuration type for HttpsTerminateToHttpHandler: ${config.type}`); } } /** * Initialize the handler, setting up TLS context */ async initialize() { // We need to load or create TLS certificates if (this.config.https?.customCert) { // Use custom certificate from configuration this.secureContext = plugins.tls.createSecureContext({ key: this.config.https.customCert.key, cert: this.config.https.customCert.cert }); this.emit(ForwardingHandlerEvents.CERTIFICATE_LOADED, { source: 'config', domain: this.config.target.host }); } else if (this.config.acme?.enabled) { // Request certificate through ACME if needed this.emit(ForwardingHandlerEvents.CERTIFICATE_NEEDED, { domain: Array.isArray(this.config.target.host) ? this.config.target.host[0] : this.config.target.host, useProduction: this.config.acme.production || false }); // In a real implementation, we would wait for the certificate to be issued // For now, we'll use a dummy context this.secureContext = plugins.tls.createSecureContext({ key: '-----BEGIN PRIVATE KEY-----\nDummy key\n-----END PRIVATE KEY-----', cert: '-----BEGIN CERTIFICATE-----\nDummy cert\n-----END CERTIFICATE-----' }); } else { throw new Error('HTTPS termination requires either a custom certificate or ACME enabled'); } } /** * Set the secure context for TLS termination * Called when a certificate is available * @param context The secure context */ setSecureContext(context) { this.secureContext = context; } /** * Handle a TLS/SSL socket connection by terminating TLS and forwarding to HTTP backend * @param clientSocket The incoming socket from the client */ handleConnection(clientSocket) { // Make sure we have a secure context if (!this.secureContext) { clientSocket.destroy(new Error('TLS secure context not initialized')); return; } const remoteAddress = clientSocket.remoteAddress || 'unknown'; const remotePort = clientSocket.remotePort || 0; // Create a TLS socket using our secure context const tlsSocket = new plugins.tls.TLSSocket(clientSocket, { secureContext: this.secureContext, isServer: true, server: this.tlsServer || undefined }); this.emit(ForwardingHandlerEvents.CONNECTED, { remoteAddress, remotePort, tls: true }); // Variables to track connections let backendSocket = null; let dataBuffer = Buffer.alloc(0); let connectionEstablished = false; let forwardingSetup = false; // Set up initial error handling for TLS socket const tlsCleanupHandler = (reason) => { if (!forwardingSetup) { // If forwarding not set up yet, emit disconnected and cleanup this.emit(ForwardingHandlerEvents.DISCONNECTED, { remoteAddress, reason }); dataBuffer = Buffer.alloc(0); connectionEstablished = false; if (!tlsSocket.destroyed) { tlsSocket.destroy(); } if (backendSocket && !backendSocket.destroyed) { backendSocket.destroy(); } } // If forwarding is setup, setupBidirectionalForwarding will handle cleanup }; setupSocketHandlers(tlsSocket, tlsCleanupHandler, undefined, 'tls'); // Set timeout const timeout = this.getTimeout(); tlsSocket.setTimeout(timeout); tlsSocket.on('timeout', () => { this.emit(ForwardingHandlerEvents.ERROR, { remoteAddress, error: 'TLS connection timeout' }); tlsCleanupHandler('timeout'); }); // Handle TLS data tlsSocket.on('data', (data) => { // If backend connection already established, just forward the data if (connectionEstablished && backendSocket && !backendSocket.destroyed) { backendSocket.write(data); return; } // Append to buffer dataBuffer = Buffer.concat([dataBuffer, data]); // Very basic HTTP parsing - in a real implementation, use http-parser if (dataBuffer.includes(Buffer.from('\r\n\r\n')) && !connectionEstablished) { const target = this.getTargetFromConfig(); // Create backend connection with immediate error handling backendSocket = createSocketWithErrorHandler({ port: target.port, host: target.host, onError: (error) => { this.emit(ForwardingHandlerEvents.ERROR, { error: error.message, code: error.code || 'UNKNOWN', remoteAddress, target: `${target.host}:${target.port}` }); // Clean up the TLS socket since we can't forward if (!tlsSocket.destroyed) { tlsSocket.destroy(); } this.emit(ForwardingHandlerEvents.DISCONNECTED, { remoteAddress, reason: `backend_connection_failed: ${error.message}` }); }, onConnect: () => { connectionEstablished = true; // Send buffered data if (dataBuffer.length > 0) { backendSocket.write(dataBuffer); dataBuffer = Buffer.alloc(0); } // Now set up bidirectional forwarding with proper cleanup forwardingSetup = true; setupBidirectionalForwarding(tlsSocket, backendSocket, { onCleanup: (reason) => { this.emit(ForwardingHandlerEvents.DISCONNECTED, { remoteAddress, reason }); dataBuffer = Buffer.alloc(0); connectionEstablished = false; forwardingSetup = false; }, enableHalfOpen: false // Close both when one closes }); } }); // Additional error logging for backend socket backendSocket.on('error', (error) => { if (!connectionEstablished) { // Connection failed during setup this.emit(ForwardingHandlerEvents.ERROR, { remoteAddress, error: `Target connection error: ${error.message}` }); } // If connected, setupBidirectionalForwarding handles cleanup }); } }); } /** * Handle an HTTP request by forwarding to the HTTP backend * @param req The HTTP request * @param res The HTTP response */ handleHttpRequest(req, res) { // Check if we should redirect to HTTPS if (this.config.http?.redirectToHttps) { this.redirectToHttps(req, res); return; } // Get the target from configuration const target = this.getTargetFromConfig(); // Create custom headers with variable substitution const variables = { clientIp: req.socket.remoteAddress || 'unknown' }; // Prepare headers, merging with any custom headers from config const headers = this.applyCustomHeaders(req.headers, variables); // Create the proxy request options const options = { hostname: target.host, port: target.port, path: req.url, method: req.method, headers }; // Create the proxy request const proxyReq = plugins.http.request(options, (proxyRes) => { // Copy status code and headers from the proxied response res.writeHead(proxyRes.statusCode || 500, proxyRes.headers); // Pipe the proxy response to the client response proxyRes.pipe(res); // Track response size for logging let responseSize = 0; proxyRes.on('data', (chunk) => { responseSize += chunk.length; }); proxyRes.on('end', () => { this.emit(ForwardingHandlerEvents.HTTP_RESPONSE, { statusCode: proxyRes.statusCode, headers: proxyRes.headers, size: responseSize }); }); }); // Handle errors in the proxy request proxyReq.on('error', (error) => { this.emit(ForwardingHandlerEvents.ERROR, { remoteAddress: req.socket.remoteAddress, error: `Proxy request error: ${error.message}` }); // Send an error response if headers haven't been sent yet if (!res.headersSent) { res.writeHead(502, { 'Content-Type': 'text/plain' }); res.end(`Error forwarding request: ${error.message}`); } else { // Just end the response if headers have already been sent res.end(); } }); // Track request details for logging let requestSize = 0; req.on('data', (chunk) => { requestSize += chunk.length; }); // Log the request this.emit(ForwardingHandlerEvents.HTTP_REQUEST, { method: req.method, url: req.url, headers: req.headers, remoteAddress: req.socket.remoteAddress, target: `${target.host}:${target.port}` }); // Pipe the client request to the proxy request if (req.readable) { req.pipe(proxyReq); } else { proxyReq.end(); } } } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaHR0cHMtdGVybWluYXRlLXRvLWh0dHAtaGFuZGxlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3RzL2ZvcndhcmRpbmcvaGFuZGxlcnMvaHR0cHMtdGVybWluYXRlLXRvLWh0dHAtaGFuZGxlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGtCQUFrQixDQUFDO0FBQzVDLE9BQU8sRUFBRSxpQkFBaUIsRUFBRSxNQUFNLG1CQUFtQixDQUFDO0FBRXRELE9BQU8sRUFBRSx1QkFBdUIsRUFBRSxNQUFNLCtCQUErQixDQUFDO0FBQ3hFLE9BQU8sRUFBRSxtQkFBbUIsRUFBRSw0QkFBNEIsRUFBRSw0QkFBNEIsRUFBRSxNQUFNLGtDQUFrQyxDQUFDO0FBRW5JOztHQUVHO0FBQ0gsTUFBTSxPQUFPLDJCQUE0QixTQUFRLGlCQUFpQjtJQUloRTs7O09BR0c7SUFDSCxZQUFZLE1BQXNCO1FBQ2hDLEtBQUssQ0FBQyxNQUFNLENBQUMsQ0FBQztRQVJSLGNBQVMsR0FBOEIsSUFBSSxDQUFDO1FBQzVDLGtCQUFhLEdBQXFDLElBQUksQ0FBQztRQVM3RCxpRUFBaUU7UUFDakUsSUFBSSxNQUFNLENBQUMsSUFBSSxLQUFLLHlCQUF5QixFQUFFLENBQUM7WUFDOUMsTUFBTSxJQUFJLEtBQUssQ0FBQywrREFBK0QsTUFBTSxDQUFDLElBQUksRUFBRSxDQUFDLENBQUM7UUFDaEcsQ0FBQztJQUNILENBQUM7SUFFRDs7T0FFRztJQUNJLEtBQUssQ0FBQyxVQUFVO1FBQ3JCLDZDQUE2QztRQUM3QyxJQUFJLElBQUksQ0FBQyxNQUFNLENBQUMsS0FBSyxFQUFFLFVBQVUsRUFBRSxDQUFDO1lBQ2xDLDRDQUE0QztZQUM1QyxJQUFJLENBQUMsYUFBYSxHQUFHLE9BQU8sQ0FBQyxHQUFHLENBQUMsbUJBQW1CLENBQUM7Z0JBQ25ELEdBQUcsRUFBRSxJQUFJLENBQUMsTUFBTSxDQUFDLEtBQUssQ0FBQyxVQUFV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