292 lines
9.0 KiB
TypeScript
292 lines
9.0 KiB
TypeScript
import * as plugins from '../../plugins.js';
|
|
import { ForwardingHandler } from './base-handler.js';
|
|
import type { IForwardConfig } from '../config/forwarding-types.js';
|
|
import { ForwardingHandlerEvents } from '../config/forwarding-types.js';
|
|
|
|
/**
|
|
* Handler for HTTPS termination with HTTPS backend
|
|
*/
|
|
export class HttpsTerminateToHttpsHandler extends ForwardingHandler {
|
|
private secureContext: plugins.tls.SecureContext | null = null;
|
|
|
|
/**
|
|
* Create a new HTTPS termination with HTTPS backend handler
|
|
* @param config The forwarding configuration
|
|
*/
|
|
constructor(config: IForwardConfig) {
|
|
super(config);
|
|
|
|
// 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
|
|
*/
|
|
public async initialize(): Promise<void> {
|
|
// 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
|
|
*/
|
|
public setSecureContext(context: plugins.tls.SecureContext): void {
|
|
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
|
|
*/
|
|
public handleConnection(clientSocket: plugins.net.Socket): void {
|
|
// 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
|
|
});
|
|
|
|
// Handle TLS errors
|
|
tlsSocket.on('error', (error) => {
|
|
this.emit(ForwardingHandlerEvents.ERROR, {
|
|
remoteAddress,
|
|
error: `TLS error: ${error.message}`
|
|
});
|
|
|
|
if (!tlsSocket.destroyed) {
|
|
tlsSocket.destroy();
|
|
}
|
|
});
|
|
|
|
// The TLS socket will now emit HTTP traffic that can be processed
|
|
// In a real implementation, we would create an HTTP parser and handle
|
|
// the requests here, but for simplicity, we'll just forward the data
|
|
|
|
// Get the target from configuration
|
|
const target = this.getTargetFromConfig();
|
|
|
|
// Set up the connection to the HTTPS backend
|
|
const connectToBackend = () => {
|
|
const 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
|
|
}, () => {
|
|
this.emit(ForwardingHandlerEvents.DATA_FORWARDED, {
|
|
direction: 'outbound',
|
|
target: `${target.host}:${target.port}`,
|
|
tls: true
|
|
});
|
|
|
|
// Set up bidirectional data flow
|
|
tlsSocket.pipe(backendSocket);
|
|
backendSocket.pipe(tlsSocket);
|
|
});
|
|
|
|
backendSocket.on('error', (error) => {
|
|
this.emit(ForwardingHandlerEvents.ERROR, {
|
|
remoteAddress,
|
|
error: `Backend connection error: ${error.message}`
|
|
});
|
|
|
|
if (!tlsSocket.destroyed) {
|
|
tlsSocket.destroy();
|
|
}
|
|
});
|
|
|
|
// Handle close
|
|
backendSocket.on('close', () => {
|
|
if (!tlsSocket.destroyed) {
|
|
tlsSocket.destroy();
|
|
}
|
|
});
|
|
|
|
// Set timeout
|
|
const timeout = this.getTimeout();
|
|
backendSocket.setTimeout(timeout);
|
|
|
|
backendSocket.on('timeout', () => {
|
|
this.emit(ForwardingHandlerEvents.ERROR, {
|
|
remoteAddress,
|
|
error: 'Backend connection timeout'
|
|
});
|
|
|
|
if (!backendSocket.destroyed) {
|
|
backendSocket.destroy();
|
|
}
|
|
});
|
|
};
|
|
|
|
// Wait for the TLS handshake to complete before connecting to backend
|
|
tlsSocket.on('secure', () => {
|
|
connectToBackend();
|
|
});
|
|
|
|
// Handle close
|
|
tlsSocket.on('close', () => {
|
|
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
|
|
remoteAddress
|
|
});
|
|
});
|
|
|
|
// Set timeout
|
|
const timeout = this.getTimeout();
|
|
tlsSocket.setTimeout(timeout);
|
|
|
|
tlsSocket.on('timeout', () => {
|
|
this.emit(ForwardingHandlerEvents.ERROR, {
|
|
remoteAddress,
|
|
error: 'TLS connection timeout'
|
|
});
|
|
|
|
if (!tlsSocket.destroyed) {
|
|
tlsSocket.destroy();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handle an HTTP request by forwarding to the HTTPS backend
|
|
* @param req The HTTP request
|
|
* @param res The HTTP response
|
|
*/
|
|
public handleHttpRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void {
|
|
// 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();
|
|
}
|
|
}
|
|
} |