update structure
This commit is contained in:
127
ts/forwarding/handlers/base-handler.ts
Normal file
127
ts/forwarding/handlers/base-handler.ts
Normal file
@ -0,0 +1,127 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import type {
|
||||
ForwardConfig,
|
||||
IForwardingHandler
|
||||
} from '../config/forwarding-types.js';
|
||||
import { ForwardingHandlerEvents } from '../config/forwarding-types.js';
|
||||
|
||||
/**
|
||||
* Base class for all forwarding handlers
|
||||
*/
|
||||
export abstract class ForwardingHandler extends plugins.EventEmitter implements IForwardingHandler {
|
||||
/**
|
||||
* Create a new ForwardingHandler
|
||||
* @param config The forwarding configuration
|
||||
*/
|
||||
constructor(protected config: ForwardConfig) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the handler
|
||||
* Base implementation does nothing, subclasses should override as needed
|
||||
*/
|
||||
public async initialize(): Promise<void> {
|
||||
// Base implementation - no initialization needed
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a new socket connection
|
||||
* @param socket The incoming socket connection
|
||||
*/
|
||||
public abstract handleConnection(socket: plugins.net.Socket): void;
|
||||
|
||||
/**
|
||||
* Handle an HTTP request
|
||||
* @param req The HTTP request
|
||||
* @param res The HTTP response
|
||||
*/
|
||||
public abstract handleHttpRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void;
|
||||
|
||||
/**
|
||||
* Get a target from the configuration, supporting round-robin selection
|
||||
* @returns A resolved target object with host and port
|
||||
*/
|
||||
protected getTargetFromConfig(): { host: string, port: number } {
|
||||
const { target } = this.config;
|
||||
|
||||
// Handle round-robin host selection
|
||||
if (Array.isArray(target.host)) {
|
||||
if (target.host.length === 0) {
|
||||
throw new Error('No target hosts specified');
|
||||
}
|
||||
|
||||
// Simple round-robin selection
|
||||
const randomIndex = Math.floor(Math.random() * target.host.length);
|
||||
return {
|
||||
host: target.host[randomIndex],
|
||||
port: target.port
|
||||
};
|
||||
}
|
||||
|
||||
// Single host
|
||||
return {
|
||||
host: target.host,
|
||||
port: target.port
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect an HTTP request to HTTPS
|
||||
* @param req The HTTP request
|
||||
* @param res The HTTP response
|
||||
*/
|
||||
protected redirectToHttps(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void {
|
||||
const host = req.headers.host || '';
|
||||
const path = req.url || '/';
|
||||
const redirectUrl = `https://${host}${path}`;
|
||||
|
||||
res.writeHead(301, {
|
||||
'Location': redirectUrl,
|
||||
'Cache-Control': 'no-cache'
|
||||
});
|
||||
res.end(`Redirecting to ${redirectUrl}`);
|
||||
|
||||
this.emit(ForwardingHandlerEvents.HTTP_RESPONSE, {
|
||||
statusCode: 301,
|
||||
headers: { 'Location': redirectUrl },
|
||||
size: 0
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply custom headers from configuration
|
||||
* @param headers The original headers
|
||||
* @param variables Variables to replace in the headers
|
||||
* @returns The headers with custom values applied
|
||||
*/
|
||||
protected applyCustomHeaders(
|
||||
headers: Record<string, string | string[] | undefined>,
|
||||
variables: Record<string, string>
|
||||
): Record<string, string | string[] | undefined> {
|
||||
const customHeaders = this.config.advanced?.headers || {};
|
||||
const result = { ...headers };
|
||||
|
||||
// Apply custom headers with variable substitution
|
||||
for (const [key, value] of Object.entries(customHeaders)) {
|
||||
let processedValue = value;
|
||||
|
||||
// Replace variables in the header value
|
||||
for (const [varName, varValue] of Object.entries(variables)) {
|
||||
processedValue = processedValue.replace(`{${varName}}`, varValue);
|
||||
}
|
||||
|
||||
result[key] = processedValue;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the timeout for this connection from configuration
|
||||
* @returns Timeout in milliseconds
|
||||
*/
|
||||
protected getTimeout(): number {
|
||||
return this.config.advanced?.timeout || 60000; // Default: 60 seconds
|
||||
}
|
||||
}
|
140
ts/forwarding/handlers/http-handler.ts
Normal file
140
ts/forwarding/handlers/http-handler.ts
Normal file
@ -0,0 +1,140 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import { ForwardingHandler } from './base-handler.js';
|
||||
import type { ForwardConfig } from '../config/forwarding-types.js';
|
||||
import { ForwardingHandlerEvents } from '../config/forwarding-types.js';
|
||||
|
||||
/**
|
||||
* Handler for HTTP-only forwarding
|
||||
*/
|
||||
export class HttpForwardingHandler extends ForwardingHandler {
|
||||
/**
|
||||
* Create a new HTTP forwarding handler
|
||||
* @param config The forwarding configuration
|
||||
*/
|
||||
constructor(config: ForwardConfig) {
|
||||
super(config);
|
||||
|
||||
// Validate that this is an HTTP-only configuration
|
||||
if (config.type !== 'http-only') {
|
||||
throw new Error(`Invalid configuration type for HttpForwardingHandler: ${config.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a raw socket connection
|
||||
* HTTP handler doesn't do much with raw sockets as it mainly processes
|
||||
* parsed HTTP requests
|
||||
*/
|
||||
public handleConnection(socket: plugins.net.Socket): void {
|
||||
// For HTTP, we mainly handle parsed requests, but we can still set up
|
||||
// some basic connection tracking
|
||||
const remoteAddress = socket.remoteAddress || 'unknown';
|
||||
|
||||
socket.on('close', (hadError) => {
|
||||
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
|
||||
remoteAddress,
|
||||
hadError
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('error', (error) => {
|
||||
this.emit(ForwardingHandlerEvents.ERROR, {
|
||||
remoteAddress,
|
||||
error: error.message
|
||||
});
|
||||
});
|
||||
|
||||
this.emit(ForwardingHandlerEvents.CONNECTED, {
|
||||
remoteAddress
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an HTTP request
|
||||
* @param req The HTTP request
|
||||
* @param res The HTTP response
|
||||
*/
|
||||
public handleHttpRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void {
|
||||
// Get the target from configuration
|
||||
const target = this.getTargetFromConfig();
|
||||
|
||||
// Create a custom headers object with variables for 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 bytes 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();
|
||||
}
|
||||
}
|
||||
}
|
182
ts/forwarding/handlers/https-passthrough-handler.ts
Normal file
182
ts/forwarding/handlers/https-passthrough-handler.ts
Normal file
@ -0,0 +1,182 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import { ForwardingHandler } from './base-handler.js';
|
||||
import type { ForwardConfig } from '../config/forwarding-types.js';
|
||||
import { ForwardingHandlerEvents } from '../config/forwarding-types.js';
|
||||
|
||||
/**
|
||||
* 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: ForwardConfig) {
|
||||
super(config);
|
||||
|
||||
// Validate that this is an HTTPS passthrough configuration
|
||||
if (config.type !== 'https-passthrough') {
|
||||
throw new Error(`Invalid configuration type for HttpsPassthroughHandler: ${config.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
// Handle errors on the server socket
|
||||
serverSocket.on('error', (error) => {
|
||||
this.emit(ForwardingHandlerEvents.ERROR, {
|
||||
remoteAddress,
|
||||
error: `Target connection error: ${error.message}`
|
||||
});
|
||||
|
||||
// Close the client socket if it's still open
|
||||
if (!clientSocket.destroyed) {
|
||||
clientSocket.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle errors on the client socket
|
||||
clientSocket.on('error', (error) => {
|
||||
this.emit(ForwardingHandlerEvents.ERROR, {
|
||||
remoteAddress,
|
||||
error: `Client connection error: ${error.message}`
|
||||
});
|
||||
|
||||
// Close the server socket if it's still open
|
||||
if (!serverSocket.destroyed) {
|
||||
serverSocket.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
// Track data transfer for logging
|
||||
let bytesSent = 0;
|
||||
let bytesReceived = 0;
|
||||
|
||||
// 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
|
||||
});
|
||||
});
|
||||
|
||||
// Handle connection close
|
||||
const handleClose = () => {
|
||||
if (!clientSocket.destroyed) {
|
||||
clientSocket.destroy();
|
||||
}
|
||||
|
||||
if (!serverSocket.destroyed) {
|
||||
serverSocket.destroy();
|
||||
}
|
||||
|
||||
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
|
||||
remoteAddress,
|
||||
bytesSent,
|
||||
bytesReceived
|
||||
});
|
||||
};
|
||||
|
||||
// Set up close handlers
|
||||
clientSocket.on('close', handleClose);
|
||||
serverSocket.on('close', handleClose);
|
||||
|
||||
// Set timeouts
|
||||
const timeout = this.getTimeout();
|
||||
clientSocket.setTimeout(timeout);
|
||||
serverSocket.setTimeout(timeout);
|
||||
|
||||
// Handle timeouts
|
||||
clientSocket.on('timeout', () => {
|
||||
this.emit(ForwardingHandlerEvents.ERROR, {
|
||||
remoteAddress,
|
||||
error: 'Client connection timeout'
|
||||
});
|
||||
handleClose();
|
||||
});
|
||||
|
||||
serverSocket.on('timeout', () => {
|
||||
this.emit(ForwardingHandlerEvents.ERROR, {
|
||||
remoteAddress,
|
||||
error: 'Server connection timeout'
|
||||
});
|
||||
handleClose();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
// 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
|
||||
});
|
||||
}
|
||||
}
|
264
ts/forwarding/handlers/https-terminate-to-http-handler.ts
Normal file
264
ts/forwarding/handlers/https-terminate-to-http-handler.ts
Normal file
@ -0,0 +1,264 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import { ForwardingHandler } from './base-handler.js';
|
||||
import type { ForwardConfig } from '../config/forwarding-types.js';
|
||||
import { ForwardingHandlerEvents } from '../config/forwarding-types.js';
|
||||
|
||||
/**
|
||||
* Handler for HTTPS termination with HTTP backend
|
||||
*/
|
||||
export class HttpsTerminateToHttpHandler extends ForwardingHandler {
|
||||
private tlsServer: plugins.tls.Server | null = null;
|
||||
private secureContext: plugins.tls.SecureContext | null = null;
|
||||
|
||||
/**
|
||||
* Create a new HTTPS termination with HTTP backend handler
|
||||
* @param config The forwarding configuration
|
||||
*/
|
||||
constructor(config: ForwardConfig) {
|
||||
super(config);
|
||||
|
||||
// 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
|
||||
*/
|
||||
public async initialize(): Promise<void> {
|
||||
// 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
|
||||
*/
|
||||
public setSecureContext(context: plugins.tls.SecureContext): void {
|
||||
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
|
||||
*/
|
||||
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,
|
||||
server: this.tlsServer || undefined
|
||||
});
|
||||
|
||||
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 log the data
|
||||
|
||||
let dataBuffer = Buffer.alloc(0);
|
||||
|
||||
tlsSocket.on('data', (data) => {
|
||||
// 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'))) {
|
||||
const target = this.getTargetFromConfig();
|
||||
|
||||
// Simple example: forward the data to an HTTP server
|
||||
const socket = plugins.net.connect(target.port, target.host, () => {
|
||||
socket.write(dataBuffer);
|
||||
dataBuffer = Buffer.alloc(0);
|
||||
|
||||
// Set up bidirectional data flow
|
||||
tlsSocket.pipe(socket);
|
||||
socket.pipe(tlsSocket);
|
||||
});
|
||||
|
||||
socket.on('error', (error) => {
|
||||
this.emit(ForwardingHandlerEvents.ERROR, {
|
||||
remoteAddress,
|
||||
error: `Target connection error: ${error.message}`
|
||||
});
|
||||
|
||||
if (!tlsSocket.destroyed) {
|
||||
tlsSocket.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 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 HTTP 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
|
||||
};
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
292
ts/forwarding/handlers/https-terminate-to-https-handler.ts
Normal file
292
ts/forwarding/handlers/https-terminate-to-https-handler.ts
Normal file
@ -0,0 +1,292 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import { ForwardingHandler } from './base-handler.js';
|
||||
import type { ForwardConfig } 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: ForwardConfig) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
9
ts/forwarding/handlers/index.ts
Normal file
9
ts/forwarding/handlers/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Forwarding handler implementations
|
||||
*/
|
||||
|
||||
export { ForwardingHandler } from './base-handler.js';
|
||||
export { HttpForwardingHandler } from './http-handler.js';
|
||||
export { HttpsPassthroughHandler } from './https-passthrough-handler.js';
|
||||
export { HttpsTerminateToHttpHandler } from './https-terminate-to-http-handler.js';
|
||||
export { HttpsTerminateToHttpsHandler } from './https-terminate-to-https-handler.js';
|
Reference in New Issue
Block a user