464 lines
15 KiB
TypeScript
464 lines
15 KiB
TypeScript
import * as plugins from '../../plugins.js';
|
|
import { NetworkProxy } from '../network-proxy/index.js';
|
|
import { Port80Handler } from '../../http/port80/port80-handler.js';
|
|
import { Port80HandlerEvents } from '../../core/models/common-types.js';
|
|
import { subscribeToPort80Handler } from '../../core/utils/event-utils.js';
|
|
import type { ICertificateData } from '../../certificate/models/certificate-types.js';
|
|
import type { IConnectionRecord, ISmartProxyOptions } from './models/interfaces.js';
|
|
import type { IRouteConfig } from './models/route-types.js';
|
|
|
|
/**
|
|
* Manages NetworkProxy integration for TLS termination
|
|
*
|
|
* NetworkProxyBridge connects SmartProxy with NetworkProxy to handle TLS termination.
|
|
* It converts route configurations to NetworkProxy configuration format and manages
|
|
* certificate provisioning through Port80Handler when ACME is enabled.
|
|
*
|
|
* It is used by SmartProxy for routes that have:
|
|
* - TLS mode of 'terminate' or 'terminate-and-reencrypt'
|
|
* - Certificate set to 'auto' or custom certificate
|
|
*/
|
|
export class NetworkProxyBridge {
|
|
private networkProxy: NetworkProxy | null = null;
|
|
private port80Handler: Port80Handler | null = null;
|
|
|
|
constructor(private settings: ISmartProxyOptions) {}
|
|
|
|
/**
|
|
* Set the Port80Handler to use for certificate management
|
|
*/
|
|
public setPort80Handler(handler: Port80Handler): void {
|
|
this.port80Handler = handler;
|
|
|
|
// Subscribe to certificate events
|
|
subscribeToPort80Handler(handler, {
|
|
onCertificateIssued: this.handleCertificateEvent.bind(this),
|
|
onCertificateRenewed: this.handleCertificateEvent.bind(this)
|
|
});
|
|
|
|
// If NetworkProxy is already initialized, connect it with Port80Handler
|
|
if (this.networkProxy) {
|
|
this.networkProxy.setExternalPort80Handler(handler);
|
|
}
|
|
|
|
console.log('Port80Handler connected to NetworkProxyBridge');
|
|
}
|
|
|
|
/**
|
|
* Initialize NetworkProxy instance
|
|
*/
|
|
public async initialize(): Promise<void> {
|
|
if (!this.networkProxy && this.settings.useNetworkProxy && this.settings.useNetworkProxy.length > 0) {
|
|
// Configure NetworkProxy options based on PortProxy settings
|
|
const networkProxyOptions: any = {
|
|
port: this.settings.networkProxyPort!,
|
|
portProxyIntegration: true,
|
|
logLevel: this.settings.enableDetailedLogging ? 'debug' : 'info',
|
|
useExternalPort80Handler: !!this.port80Handler // Use Port80Handler if available
|
|
};
|
|
|
|
|
|
this.networkProxy = new NetworkProxy(networkProxyOptions);
|
|
|
|
console.log(`Initialized NetworkProxy on port ${this.settings.networkProxyPort}`);
|
|
|
|
// Connect Port80Handler if available
|
|
if (this.port80Handler) {
|
|
this.networkProxy.setExternalPort80Handler(this.port80Handler);
|
|
}
|
|
|
|
// Apply route configurations to NetworkProxy
|
|
await this.syncRoutesToNetworkProxy(this.settings.routes || []);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle certificate issuance or renewal events
|
|
*/
|
|
private handleCertificateEvent(data: ICertificateData): void {
|
|
if (!this.networkProxy) return;
|
|
|
|
console.log(`Received certificate for ${data.domain} from Port80Handler, updating NetworkProxy`);
|
|
|
|
try {
|
|
// Find existing config for this domain
|
|
const existingConfigs = this.networkProxy.getProxyConfigs()
|
|
.filter(config => config.hostName === data.domain);
|
|
|
|
if (existingConfigs.length > 0) {
|
|
// Update existing configs with new certificate
|
|
for (const config of existingConfigs) {
|
|
config.privateKey = data.privateKey;
|
|
config.publicKey = data.certificate;
|
|
}
|
|
|
|
// Apply updated configs
|
|
this.networkProxy.updateProxyConfigs(existingConfigs)
|
|
.then(() => console.log(`Updated certificate for ${data.domain} in NetworkProxy`))
|
|
.catch(err => console.log(`Error updating certificate in NetworkProxy: ${err}`));
|
|
} else {
|
|
// Create a new config for this domain
|
|
console.log(`No existing config found for ${data.domain}, creating new config in NetworkProxy`);
|
|
}
|
|
} catch (err) {
|
|
console.log(`Error handling certificate event: ${err}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Apply an external (static) certificate into NetworkProxy
|
|
*/
|
|
public applyExternalCertificate(data: ICertificateData): void {
|
|
if (!this.networkProxy) {
|
|
console.log(`NetworkProxy not initialized: cannot apply external certificate for ${data.domain}`);
|
|
return;
|
|
}
|
|
this.handleCertificateEvent(data);
|
|
}
|
|
|
|
/**
|
|
* Get the NetworkProxy instance
|
|
*/
|
|
public getNetworkProxy(): NetworkProxy | null {
|
|
return this.networkProxy;
|
|
}
|
|
|
|
/**
|
|
* Get the NetworkProxy port
|
|
*/
|
|
public getNetworkProxyPort(): number {
|
|
return this.networkProxy ? this.networkProxy.getListeningPort() : this.settings.networkProxyPort || 8443;
|
|
}
|
|
|
|
/**
|
|
* Start NetworkProxy
|
|
*/
|
|
public async start(): Promise<void> {
|
|
if (this.networkProxy) {
|
|
await this.networkProxy.start();
|
|
console.log(`NetworkProxy started on port ${this.settings.networkProxyPort}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stop NetworkProxy
|
|
*/
|
|
public async stop(): Promise<void> {
|
|
if (this.networkProxy) {
|
|
try {
|
|
console.log('Stopping NetworkProxy...');
|
|
await this.networkProxy.stop();
|
|
console.log('NetworkProxy stopped successfully');
|
|
} catch (err) {
|
|
console.log(`Error stopping NetworkProxy: ${err}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Register domains with Port80Handler
|
|
*/
|
|
public registerDomainsWithPort80Handler(domains: string[]): void {
|
|
if (!this.port80Handler) {
|
|
console.log('Cannot register domains - Port80Handler not initialized');
|
|
return;
|
|
}
|
|
|
|
for (const domain of domains) {
|
|
// Skip wildcards
|
|
if (domain.includes('*')) {
|
|
console.log(`Skipping wildcard domain for ACME: ${domain}`);
|
|
continue;
|
|
}
|
|
|
|
// Register the domain
|
|
try {
|
|
this.port80Handler.addDomain({
|
|
domainName: domain,
|
|
sslRedirect: true,
|
|
acmeMaintenance: true
|
|
});
|
|
|
|
console.log(`Registered domain with Port80Handler: ${domain}`);
|
|
} catch (err) {
|
|
console.log(`Error registering domain ${domain} with Port80Handler: ${err}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Forwards a TLS connection to a NetworkProxy for handling
|
|
*/
|
|
public forwardToNetworkProxy(
|
|
connectionId: string,
|
|
socket: plugins.net.Socket,
|
|
record: IConnectionRecord,
|
|
initialData: Buffer,
|
|
customProxyPort?: number,
|
|
onError?: (reason: string) => void
|
|
): void {
|
|
// Ensure NetworkProxy is initialized
|
|
if (!this.networkProxy) {
|
|
console.log(
|
|
`[${connectionId}] NetworkProxy not initialized. Cannot forward connection.`
|
|
);
|
|
if (onError) {
|
|
onError('network_proxy_not_initialized');
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Use the custom port if provided, otherwise use the default NetworkProxy port
|
|
const proxyPort = customProxyPort || this.networkProxy.getListeningPort();
|
|
const proxyHost = 'localhost'; // Assuming NetworkProxy runs locally
|
|
|
|
if (this.settings.enableDetailedLogging) {
|
|
console.log(
|
|
`[${connectionId}] Forwarding TLS connection to NetworkProxy at ${proxyHost}:${proxyPort}`
|
|
);
|
|
}
|
|
|
|
// Create a connection to the NetworkProxy
|
|
const proxySocket = plugins.net.connect({
|
|
host: proxyHost,
|
|
port: proxyPort,
|
|
});
|
|
|
|
// Store the outgoing socket in the record
|
|
record.outgoing = proxySocket;
|
|
record.outgoingStartTime = Date.now();
|
|
record.usingNetworkProxy = true;
|
|
|
|
// Set up error handlers
|
|
proxySocket.on('error', (err) => {
|
|
console.log(`[${connectionId}] Error connecting to NetworkProxy: ${err.message}`);
|
|
if (onError) {
|
|
onError('network_proxy_connect_error');
|
|
}
|
|
});
|
|
|
|
// Handle connection to NetworkProxy
|
|
proxySocket.on('connect', () => {
|
|
if (this.settings.enableDetailedLogging) {
|
|
console.log(`[${connectionId}] Connected to NetworkProxy at ${proxyHost}:${proxyPort}`);
|
|
}
|
|
|
|
// First send the initial data that contains the TLS ClientHello
|
|
proxySocket.write(initialData);
|
|
|
|
// Now set up bidirectional piping between client and NetworkProxy
|
|
socket.pipe(proxySocket);
|
|
proxySocket.pipe(socket);
|
|
|
|
// Update activity on data transfer (caller should handle this)
|
|
if (this.settings.enableDetailedLogging) {
|
|
console.log(`[${connectionId}] TLS connection successfully forwarded to NetworkProxy`);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Synchronizes routes to NetworkProxy
|
|
*
|
|
* This method converts route configurations to NetworkProxy format and updates
|
|
* the NetworkProxy with the converted configurations. It handles:
|
|
*
|
|
* - Extracting domain, target, and certificate information from routes
|
|
* - Converting TLS mode settings to NetworkProxy configuration
|
|
* - Applying security and advanced settings
|
|
* - Registering domains for ACME certificate provisioning when needed
|
|
*
|
|
* @param routes The route configurations to sync to NetworkProxy
|
|
*/
|
|
public async syncRoutesToNetworkProxy(routes: IRouteConfig[]): Promise<void> {
|
|
if (!this.networkProxy) {
|
|
console.log('Cannot sync configurations - NetworkProxy not initialized');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Get SSL certificates from assets
|
|
// Import fs directly since it's not in plugins
|
|
const fs = await import('fs');
|
|
|
|
let certPair;
|
|
try {
|
|
certPair = {
|
|
key: fs.readFileSync('assets/certs/key.pem', 'utf8'),
|
|
cert: fs.readFileSync('assets/certs/cert.pem', 'utf8'),
|
|
};
|
|
} catch (certError) {
|
|
console.log(`Warning: Could not read default certificates: ${certError}`);
|
|
console.log(
|
|
'Using empty certificate placeholders - ACME will generate proper certificates if enabled'
|
|
);
|
|
|
|
// Use empty placeholders - NetworkProxy will use its internal defaults
|
|
// or ACME will generate proper ones if enabled
|
|
certPair = {
|
|
key: '',
|
|
cert: '',
|
|
};
|
|
}
|
|
|
|
// Convert routes to NetworkProxy configs
|
|
const proxyConfigs = this.convertRoutesToNetworkProxyConfigs(routes, certPair);
|
|
|
|
// Update the proxy configs
|
|
await this.networkProxy.updateProxyConfigs(proxyConfigs);
|
|
console.log(`Synced ${proxyConfigs.length} configurations to NetworkProxy`);
|
|
} catch (err) {
|
|
console.log(`Error syncing routes to NetworkProxy: ${err}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert routes to NetworkProxy configuration format
|
|
*
|
|
* This method transforms route-based configuration to NetworkProxy's configuration format.
|
|
* It processes each route and creates appropriate NetworkProxy configs for domains
|
|
* that require TLS termination.
|
|
*
|
|
* @param routes Array of route configurations to convert
|
|
* @param defaultCertPair Default certificate to use if no custom certificate is specified
|
|
* @returns Array of NetworkProxy configurations
|
|
*/
|
|
public convertRoutesToNetworkProxyConfigs(
|
|
routes: IRouteConfig[],
|
|
defaultCertPair: { key: string; cert: string }
|
|
): plugins.tsclass.network.IReverseProxyConfig[] {
|
|
const configs: plugins.tsclass.network.IReverseProxyConfig[] = [];
|
|
|
|
for (const route of routes) {
|
|
// Skip routes without domains
|
|
if (!route.match.domains) continue;
|
|
|
|
// Skip non-forward routes
|
|
if (route.action.type !== 'forward') continue;
|
|
|
|
// Skip routes without TLS configuration
|
|
if (!route.action.tls || !route.action.target) continue;
|
|
|
|
// Get domains from route
|
|
const domains = Array.isArray(route.match.domains)
|
|
? route.match.domains
|
|
: [route.match.domains];
|
|
|
|
// Create a config for each domain
|
|
for (const domain of domains) {
|
|
// Determine if this route requires TLS termination
|
|
const needsTermination = route.action.tls.mode === 'terminate' ||
|
|
route.action.tls.mode === 'terminate-and-reencrypt';
|
|
|
|
// Skip passthrough domains for NetworkProxy
|
|
if (route.action.tls.mode === 'passthrough') continue;
|
|
|
|
// Get certificate
|
|
let certKey = defaultCertPair.key;
|
|
let certCert = defaultCertPair.cert;
|
|
|
|
// Use custom certificate if specified
|
|
if (route.action.tls.certificate !== 'auto' && typeof route.action.tls.certificate === 'object') {
|
|
certKey = route.action.tls.certificate.key;
|
|
certCert = route.action.tls.certificate.cert;
|
|
}
|
|
|
|
// Determine target hosts and ports
|
|
const targetHosts = Array.isArray(route.action.target.host)
|
|
? route.action.target.host
|
|
: [route.action.target.host];
|
|
|
|
const targetPort = route.action.target.port;
|
|
|
|
// Create NetworkProxy config
|
|
const config: plugins.tsclass.network.IReverseProxyConfig = {
|
|
hostName: domain,
|
|
privateKey: certKey,
|
|
publicKey: certCert,
|
|
destinationIps: targetHosts,
|
|
destinationPorts: [targetPort],
|
|
proxyConfig: {
|
|
targetIsTls: route.action.tls.mode === 'terminate-and-reencrypt',
|
|
allowHTTP1: true,
|
|
// Apply any other NetworkProxy-specific settings
|
|
...(route.action.advanced ? {
|
|
preserveHost: true,
|
|
timeout: route.action.advanced.timeout,
|
|
headers: route.action.advanced.headers
|
|
} : {})
|
|
}
|
|
};
|
|
|
|
configs.push(config);
|
|
}
|
|
}
|
|
|
|
return configs;
|
|
}
|
|
|
|
/**
|
|
* @deprecated This method is deprecated and will be removed in a future version.
|
|
* Use syncRoutesToNetworkProxy() instead.
|
|
*
|
|
* This legacy method exists only for backward compatibility and
|
|
* simply forwards to syncRoutesToNetworkProxy().
|
|
*/
|
|
public async syncDomainConfigsToNetworkProxy(): Promise<void> {
|
|
console.log('Method syncDomainConfigsToNetworkProxy is deprecated. Use syncRoutesToNetworkProxy instead.');
|
|
await this.syncRoutesToNetworkProxy(this.settings.routes || []);
|
|
}
|
|
|
|
/**
|
|
* Request a certificate for a specific domain
|
|
*/
|
|
public async requestCertificate(domain: string): Promise<boolean> {
|
|
// Delegate to Port80Handler if available
|
|
if (this.port80Handler) {
|
|
try {
|
|
// Check if the domain is already registered
|
|
const cert = this.port80Handler.getCertificate(domain);
|
|
if (cert) {
|
|
console.log(`Certificate already exists for ${domain}`);
|
|
return true;
|
|
}
|
|
|
|
// Register the domain for certificate issuance
|
|
this.port80Handler.addDomain({
|
|
domainName: domain,
|
|
sslRedirect: true,
|
|
acmeMaintenance: true
|
|
});
|
|
|
|
console.log(`Domain ${domain} registered for certificate issuance`);
|
|
return true;
|
|
} catch (err) {
|
|
console.log(`Error requesting certificate: ${err}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Fall back to NetworkProxy if Port80Handler is not available
|
|
if (!this.networkProxy) {
|
|
console.log('Cannot request certificate - NetworkProxy not initialized');
|
|
return false;
|
|
}
|
|
|
|
if (!this.settings.acme?.enabled) {
|
|
console.log('Cannot request certificate - ACME is not enabled');
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
const result = await this.networkProxy.requestCertificate(domain);
|
|
if (result) {
|
|
console.log(`Certificate request for ${domain} submitted successfully`);
|
|
} else {
|
|
console.log(`Certificate request for ${domain} failed`);
|
|
}
|
|
return result;
|
|
} catch (err) {
|
|
console.log(`Error requesting certificate: ${err}`);
|
|
return false;
|
|
}
|
|
}
|
|
} |