@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.
261 lines • 20.3 kB
JavaScript
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 HTTPS backend
*/
export class HttpsTerminateToHttpsHandler extends ForwardingHandler {
/**
* Create a new HTTPS termination with HTTPS backend handler
* @param config The forwarding configuration
*/
constructor(config) {
super(config);
this.secureContext = null;
// Validate that this is an HTTPS terminate to HTTPS configuration
if (config.type !== 'https-terminate-to-https') {
throw new Error(`Invalid configuration type for HttpsTerminateToHttpsHandler: ${config.type}`);
}
}
/**
* Initialize the handler, setting up TLS context
*/
async initialize() {
// We need to load or create TLS certificates for termination
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 creating a new TLS connection to 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
});
this.emit(ForwardingHandlerEvents.CONNECTED, {
remoteAddress,
remotePort,
tls: true
});
// Variable to track backend socket
let backendSocket = null;
let isConnectedToBackend = false;
// Set up initial error handling for TLS socket
const tlsCleanupHandler = (reason) => {
if (!isConnectedToBackend) {
// If backend not connected yet, just emit disconnected event
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
remoteAddress,
reason
});
// Cleanup TLS socket if needed
if (!tlsSocket.destroyed) {
tlsSocket.destroy();
}
}
// If connected to backend, 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');
});
// Get the target from configuration
const target = this.getTargetFromConfig();
// Set up the connection to the HTTPS backend
const connectToBackend = () => {
backendSocket = plugins.tls.connect({
host: target.host,
port: target.port,
// In a real implementation, we would configure TLS options
rejectUnauthorized: false // For testing only, never use in production
}, () => {
isConnectedToBackend = true;
this.emit(ForwardingHandlerEvents.DATA_FORWARDED, {
direction: 'outbound',
target: `${target.host}:${target.port}`,
tls: true
});
// Set up bidirectional forwarding with proper cleanup
setupBidirectionalForwarding(tlsSocket, backendSocket, {
onCleanup: (reason) => {
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
remoteAddress,
reason
});
},
enableHalfOpen: false // Close both when one closes
});
// Set timeout for backend socket
backendSocket.setTimeout(timeout);
backendSocket.on('timeout', () => {
this.emit(ForwardingHandlerEvents.ERROR, {
remoteAddress,
error: 'Backend connection timeout'
});
// Let setupBidirectionalForwarding handle the cleanup
});
});
// Handle backend connection errors
backendSocket.on('error', (error) => {
this.emit(ForwardingHandlerEvents.ERROR, {
remoteAddress,
error: `Backend connection error: ${error.message}`
});
if (!isConnectedToBackend) {
// Connection failed, clean up TLS socket
if (!tlsSocket.destroyed) {
tlsSocket.destroy();
}
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
remoteAddress,
reason: `backend_connection_failed: ${error.message}`
});
}
// If connected, let setupBidirectionalForwarding handle cleanup
});
};
// Wait for the TLS handshake to complete before connecting to backend
tlsSocket.on('secure', () => {
connectToBackend();
});
}
/**
* Handle an HTTP request by forwarding to the HTTPS 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,
// In a real implementation, we would configure TLS options
rejectUnauthorized: false // For testing only, never use in production
};
// Create the proxy request using HTTPS
const proxyReq = plugins.https.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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaHR0cHMtdGVybWluYXRlLXRvLWh0dHBzLWhhbmRsZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi90cy9mb3J3YXJkaW5nL2hhbmRsZXJzL2h0dHBzLXRlcm1pbmF0ZS10by1odHRwcy1oYW5kbGVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sa0JBQWtCLENBQUM7QUFDNUMsT0FBTyxFQUFFLGlCQUFpQixFQUFFLE1BQU0sbUJBQW1CLENBQUM7QUFFdEQsT0FBTyxFQUFFLHVCQUF1QixFQUFFLE1BQU0sK0JBQStCLENBQUM7QUFDeEUsT0FBTyxFQUFFLG1CQUFtQixFQUFFLDRCQUE0QixFQUFFLDRCQUE0QixFQUFFLE1BQU0sa0NBQWtDLENBQUM7QUFFbkk7O0dBRUc7QUFDSCxNQUFNLE9BQU8sNEJBQTZCLFNBQVEsaUJBQWlCO0lBR2pFOzs7T0FHRztJQUNILFlBQVksTUFBc0I7UUFDaEMsS0FBSyxDQUFDLE1BQU0sQ0FBQyxDQUFDO1FBUFIsa0JBQWEsR0FBcUMsSUFBSSxDQUFDO1FBUzdELGtFQUFrRTtRQUNsRSxJQUFJLE1BQU0sQ0FBQyxJQUFJLEtBQUssMEJBQTBCLEVBQUUsQ0FBQztZQUMvQyxNQUFNLElBQUksS0FBSyxDQUFDLGdFQUFnRSxNQUFNLENBQUMsSUFBSSxFQUFFLENBQUMsQ0FBQztRQUNqRyxDQUFDO0lBQ0gsQ0FBQztJQUVEOztPQUVHO0lBQ0ksS0FBSyxDQUFDLFVBQVU7UUFDckIsNkRBQTZEO1FBQzdELElBQUksSUFBSSxDQUFDLE1BQU0sQ0FBQyxLQUFLLEVBQUUsVUFBVSxFQUFFLENBQUM7WUFDbEMsNENBQTRDO1lBQzVDLElBQUksQ0FBQyxhQUFhLEdBQUcsT0FBTyxDQUFDLEdBQUcsQ0FBQyxtQkFBbUIsQ0FBQztnQkFDbkQsR0FBRyxFQUFFLElBQUksQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLFVBQVUsQ0FBQyxHQUFHO2dCQUNyQyxJQUFJLEVB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