diff --git a/changelog.md b/changelog.md index 7f1f578..1384445 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # Changelog +## 2025-05-09 - 12.2.0 - feat(acme) +Add ACME interfaces for Port80Handler and refactor ChallengeResponder to use new acme-interfaces, enhancing event subscription and certificate workflows. + +- Introduce new file ts/http/port80/acme-interfaces.ts defining SmartAcme interfaces, ICertManager, Http01MemoryHandler, and related types. +- Refactor ts/http/port80/challenge-responder.ts to import types from acme-interfaces and improve event forwarding for certificate events. +- Update readme.plan.md to reflect migration of Port80Handler and addition of ACME interfaces. + ## 2025-05-09 - 12.1.0 - feat(smartproxy) Migrate internal module paths and update HTTP/ACME components for SmartProxy diff --git a/readme.plan.md b/readme.plan.md index 3b4c154..65af413 100644 --- a/readme.plan.md +++ b/readme.plan.md @@ -141,6 +141,7 @@ This component has the cleanest design, so we'll start migration here: - [x] Migrate Port80Handler - [x] Move `ts/port80handler/classes.port80handler.ts` → `ts/http/port80/port80-handler.ts` - [x] Extract ACME challenge handling to `ts/http/port80/challenge-responder.ts` + - [x] Create ACME interfaces in `ts/http/port80/acme-interfaces.ts` - [x] Migrate redirect handlers - [x] Move `ts/redirect/classes.redirect.ts` → `ts/http/redirects/redirect-handler.ts` @@ -153,13 +154,13 @@ This component has the cleanest design, so we'll start migration here: ### Phase 6: Proxy Implementation Migration (Weeks 3-4) - [ ] Migrate SmartProxy components - - [ ] First, migrate interfaces to `ts/proxies/smart-proxy/models/` + - [x] First, migrate interfaces to `ts/proxies/smart-proxy/models/` - [ ] Move core class: `ts/smartproxy/classes.smartproxy.ts` → `ts/proxies/smart-proxy/smart-proxy.ts` - [ ] Move supporting classes using consistent naming - - [ ] Normalize interface names (SmartProxyOptions instead of IPortProxySettings) + - [x] Normalize interface names (SmartProxyOptions instead of IPortProxySettings) - [ ] Migrate NetworkProxy components - - [ ] First, migrate interfaces to `ts/proxies/network-proxy/models/` + - [x] First, migrate interfaces to `ts/proxies/network-proxy/models/` - [ ] Move core class: `ts/networkproxy/classes.np.networkproxy.ts` → `ts/proxies/network-proxy/network-proxy.ts` - [ ] Move supporting classes using consistent naming @@ -260,11 +261,12 @@ This component has the cleanest design, so we'll start migration here: | (new) | ts/tls/sni/client-hello-parser.ts | ✅ | | **HTTP Components** | | | | ts/port80handler/classes.port80handler.ts | ts/http/port80/port80-handler.ts | ✅ | +| (new) | ts/http/port80/acme-interfaces.ts | ✅ | | ts/redirect/classes.redirect.ts | ts/http/redirects/redirect-handler.ts | ✅ | | ts/classes.router.ts | ts/http/router/proxy-router.ts | ✅ | | **SmartProxy Components** | | | | ts/smartproxy/classes.smartproxy.ts | ts/proxies/smart-proxy/smart-proxy.ts | ❌ | -| ts/smartproxy/classes.pp.interfaces.ts | ts/proxies/smart-proxy/models/interfaces.ts | ❌ | +| ts/smartproxy/classes.pp.interfaces.ts | ts/proxies/smart-proxy/models/interfaces.ts | ✅ | | ts/smartproxy/classes.pp.connectionhandler.ts | ts/proxies/smart-proxy/connection-handler.ts | ❌ | | ts/smartproxy/classes.pp.connectionmanager.ts | ts/proxies/smart-proxy/connection-manager.ts | ❌ | | ts/smartproxy/classes.pp.domainconfigmanager.ts | ts/proxies/smart-proxy/domain-config-manager.ts | ❌ | @@ -272,15 +274,21 @@ This component has the cleanest design, so we'll start migration here: | ts/smartproxy/classes.pp.securitymanager.ts | ts/proxies/smart-proxy/security-manager.ts | ❌ | | ts/smartproxy/classes.pp.timeoutmanager.ts | ts/proxies/smart-proxy/timeout-manager.ts | ❌ | | ts/smartproxy/classes.pp.networkproxybridge.ts | ts/proxies/smart-proxy/network-proxy-bridge.ts | ❌ | +| (new) | ts/proxies/smart-proxy/models/index.ts | ✅ | +| (new) | ts/proxies/smart-proxy/index.ts | ✅ | | **NetworkProxy Components** | | | | ts/networkproxy/classes.np.networkproxy.ts | ts/proxies/network-proxy/network-proxy.ts | ❌ | | ts/networkproxy/classes.np.certificatemanager.ts | ts/proxies/network-proxy/certificate-manager.ts | ❌ | | ts/networkproxy/classes.np.connectionpool.ts | ts/proxies/network-proxy/connection-pool.ts | ❌ | | ts/networkproxy/classes.np.requesthandler.ts | ts/proxies/network-proxy/request-handler.ts | ❌ | | ts/networkproxy/classes.np.websockethandler.ts | ts/proxies/network-proxy/websocket-handler.ts | ❌ | -| ts/networkproxy/classes.np.types.ts | ts/proxies/network-proxy/models/types.ts | ❌ | +| ts/networkproxy/classes.np.types.ts | ts/proxies/network-proxy/models/types.ts | ✅ | +| (new) | ts/proxies/network-proxy/models/index.ts | ✅ | +| (new) | ts/proxies/network-proxy/index.ts | ✅ | | **NFTablesProxy Components** | | | | ts/nfttablesproxy/classes.nftablesproxy.ts | ts/proxies/nftables-proxy/nftables-proxy.ts | ❌ | +| (new) | ts/proxies/nftables-proxy/index.ts | ✅ | +| (new) | ts/proxies/index.ts | ✅ | | **Forwarding System** | | | | ts/smartproxy/types/forwarding.types.ts | ts/forwarding/config/forwarding-types.ts | ✅ | | ts/smartproxy/forwarding/domain-config.ts | ts/forwarding/config/domain-config.ts | ✅ | diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 0467ab8..1ed03c2 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/smartproxy', - version: '12.1.0', + version: '12.2.0', description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.' } diff --git a/ts/http/port80/acme-interfaces.ts b/ts/http/port80/acme-interfaces.ts new file mode 100644 index 0000000..176901c --- /dev/null +++ b/ts/http/port80/acme-interfaces.ts @@ -0,0 +1,85 @@ +/** + * Type definitions for SmartAcme interfaces used by ChallengeResponder + * These reflect the actual SmartAcme API based on the documentation + */ +import * as plugins from '../../plugins.js'; + +/** + * Structure for SmartAcme certificate result + */ +export interface SmartAcmeCert { + id?: string; + domainName: string; + created?: number | Date | string; + privateKey: string; + publicKey: string; + csr?: string; + validUntil: number | Date | string; +} + +/** + * Structure for SmartAcme options + */ +export interface SmartAcmeOptions { + accountEmail: string; + certManager: ICertManager; + environment: 'production' | 'integration'; + challengeHandlers: IChallengeHandler[]; + challengePriority?: string[]; + retryOptions?: { + retries?: number; + factor?: number; + minTimeoutMs?: number; + maxTimeoutMs?: number; + }; +} + +/** + * Interface for certificate manager + */ +export interface ICertManager { + init(): Promise; + get(domainName: string): Promise; + put(cert: SmartAcmeCert): Promise; + delete(domainName: string): Promise; + close?(): Promise; +} + +/** + * Interface for challenge handler + */ +export interface IChallengeHandler { + getSupportedTypes(): string[]; + prepare(ch: T): Promise; + verify?(ch: T): Promise; + cleanup(ch: T): Promise; + checkWetherDomainIsSupported(domain: string): Promise; +} + +/** + * HTTP-01 challenge type + */ +export interface Http01Challenge { + type: string; // 'http-01' + token: string; + keyAuthorization: string; + webPath: string; +} + +/** + * HTTP-01 Memory Handler Interface + */ +export interface Http01MemoryHandler extends IChallengeHandler { + handleRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse, next?: () => void): void; +} + +/** + * SmartAcme main class interface + */ +export interface SmartAcme { + start(): Promise; + stop(): Promise; + getCertificateForDomain(domain: string): Promise; + on?(event: string, listener: (data: any) => void): void; + eventEmitter?: plugins.EventEmitter; +} \ No newline at end of file diff --git a/ts/http/port80/challenge-responder.ts b/ts/http/port80/challenge-responder.ts index 45d05bc..99b3c09 100644 --- a/ts/http/port80/challenge-responder.ts +++ b/ts/http/port80/challenge-responder.ts @@ -1,20 +1,27 @@ import * as plugins from '../../plugins.js'; import { IncomingMessage, ServerResponse } from 'http'; -import { - CertificateEvents +import { + CertificateEvents } from '../../certificate/events/certificate-events.js'; import type { CertificateData, CertificateFailure, CertificateExpiring } from '../../certificate/models/certificate-types.js'; +import type { + SmartAcme, + SmartAcmeCert, + SmartAcmeOptions, + Http01MemoryHandler +} from './acme-interfaces.js'; /** - * Handles ACME HTTP-01 challenge responses + * ChallengeResponder handles ACME HTTP-01 challenges by leveraging SmartAcme + * It acts as a bridge between the HTTP server and the ACME challenge verification process */ export class ChallengeResponder extends plugins.EventEmitter { - private smartAcme: plugins.smartacme.SmartAcme | null = null; - private http01Handler: plugins.smartacme.handlers.Http01MemoryHandler | null = null; + private smartAcme: SmartAcme | null = null; + private http01Handler: Http01MemoryHandler | null = null; /** * Creates a new challenge responder @@ -35,198 +42,134 @@ export class ChallengeResponder extends plugins.EventEmitter { */ public async initialize(): Promise { try { - // Initialize HTTP-01 challenge handler + // Create the HTTP-01 memory handler from SmartACME this.http01Handler = new plugins.smartacme.handlers.Http01MemoryHandler(); - - // Initialize SmartAcme with proper options - this.smartAcme = new plugins.smartacme.SmartAcme({ - accountEmail: this.email, - certManager: new plugins.smartacme.certmanagers.MemoryCertManager(), - environment: this.useProduction ? 'production' : 'integration', - challengeHandlers: [this.http01Handler], - challengePriority: ['http-01'], - }); // Ensure certificate store directory exists await this.ensureCertificateStore(); + // Create a MemoryCertManager for certificate storage + const certManager = new plugins.smartacme.certmanagers.MemoryCertManager(); + + // Initialize the SmartACME client with appropriate options + this.smartAcme = new plugins.smartacme.SmartAcme({ + accountEmail: this.email, + certManager: certManager, + environment: this.useProduction ? 'production' : 'integration', + challengeHandlers: [this.http01Handler], + challengePriority: ['http-01'] + }); + // Set up event forwarding from SmartAcme - this.setupEventForwarding(); - - // Start SmartAcme + this.setupEventListeners(); + + // Start the SmartACME client await this.smartAcme.start(); + console.log('ACME client initialized successfully'); } catch (error) { - throw new Error(`Failed to initialize ACME client: ${error instanceof Error ? error.message : String(error)}`); + const errorMessage = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to initialize ACME client: ${errorMessage}`); } } /** - * Sets up event forwarding from SmartAcme to this component - */ - private setupEventForwarding(): void { - if (!this.smartAcme) return; - - // Cast smartAcme to any since different versions have different event APIs - const smartAcmeAny = this.smartAcme as any; - - // Forward certificate events to our own emitter - if (typeof smartAcmeAny.on === 'function') { - smartAcmeAny.on('certificate', (data: any) => { - const certData: CertificateData = { - domain: data.domain, - certificate: data.cert || data.publicKey, - privateKey: data.key || data.privateKey, - expiryDate: new Date(data.expiryDate || data.validUntil), - source: 'http01' - }; - // Emit as issued or renewed based on the renewal flag - const eventType = data.isRenewal - ? CertificateEvents.CERTIFICATE_RENEWED - : CertificateEvents.CERTIFICATE_ISSUED; - this.emit(eventType, certData); - }); - - smartAcmeAny.on('error', (data: any) => { - const failure: CertificateFailure = { - domain: data.domain || 'unknown', - error: data.message || data.toString(), - isRenewal: false - }; - this.emit(CertificateEvents.CERTIFICATE_FAILED, failure); - }); - } else if (smartAcmeAny.eventEmitter && typeof smartAcmeAny.eventEmitter.on === 'function') { - // Alternative event emitter approach for newer versions - smartAcmeAny.eventEmitter.on('certificate', (data: any) => { - const certData: CertificateData = { - domain: data.domain, - certificate: data.cert || data.publicKey, - privateKey: data.key || data.privateKey, - expiryDate: new Date(data.expiryDate || data.validUntil), - source: 'http01' - }; - const eventType = data.isRenewal - ? CertificateEvents.CERTIFICATE_RENEWED - : CertificateEvents.CERTIFICATE_ISSUED; - this.emit(eventType, certData); - }); - - smartAcmeAny.eventEmitter.on('error', (data: any) => { - const failure: CertificateFailure = { - domain: data.domain || 'unknown', - error: data.message || data.toString(), - isRenewal: false - }; - this.emit(CertificateEvents.CERTIFICATE_FAILED, failure); - }); - } - } - - /** - * Ensure certificate store directory exists + * Ensure the certificate store directory exists */ private async ensureCertificateStore(): Promise { try { await plugins.fs.promises.mkdir(this.certificateStore, { recursive: true }); } catch (error) { - throw new Error(`Failed to create certificate store: ${error instanceof Error ? error.message : String(error)}`); + const errorMessage = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to create certificate store: ${errorMessage}`); } } /** - * Handle HTTP request and check if it's an ACME challenge - * @param req HTTP request - * @param res HTTP response - * @returns true if the request was handled as an ACME challenge + * Setup event listeners to forward SmartACME events to our own event emitter + */ + private setupEventListeners(): void { + if (!this.smartAcme) return; + + const setupEvents = (emitter: { on: (event: string, listener: (data: any) => void) => void }) => { + // Forward certificate events + emitter.on('certificate', (data: any) => { + const isRenewal = !!data.isRenewal; + + const certData: CertificateData = { + domain: data.domainName || data.domain, + certificate: data.publicKey || data.cert, + privateKey: data.privateKey || data.key, + expiryDate: new Date(data.validUntil || data.expiryDate || Date.now()), + source: 'http01', + isRenewal + }; + + const eventType = isRenewal + ? CertificateEvents.CERTIFICATE_RENEWED + : CertificateEvents.CERTIFICATE_ISSUED; + + this.emit(eventType, certData); + }); + + // Forward error events + emitter.on('error', (error: any) => { + const domain = error.domainName || error.domain || 'unknown'; + const failureData: CertificateFailure = { + domain, + error: error.message || String(error), + isRenewal: !!error.isRenewal + }; + + this.emit(CertificateEvents.CERTIFICATE_FAILED, failureData); + }); + }; + + // Check for direct event methods on SmartAcme + if (typeof this.smartAcme.on === 'function') { + setupEvents(this.smartAcme as any); + } + // Check for eventEmitter property + else if (this.smartAcme.eventEmitter) { + setupEvents(this.smartAcme.eventEmitter); + } + // If no proper event handling, log a warning + else { + console.warn('SmartAcme instance does not support expected event interface - events may not be forwarded'); + } + } + + /** + * Handle HTTP request by checking if it's an ACME challenge + * @param req HTTP request object + * @param res HTTP response object + * @returns true if the request was handled, false otherwise */ public handleRequest(req: IncomingMessage, res: ServerResponse): boolean { - if (!this.http01Handler) { - return false; - } + if (!this.http01Handler) return false; - const url = req.url || '/'; - - // Check if this is an ACME challenge request + // Check if this is an ACME challenge request (/.well-known/acme-challenge/*) + const url = req.url || ''; if (url.startsWith('/.well-known/acme-challenge/')) { - const token = url.split('/').pop() || ''; - - if (token && this.http01Handler) { - try { - // Try to delegate to the handler - casting to any for flexibility - const handler = this.http01Handler as any; - - // Different versions may have different handler methods - if (typeof handler.handleChallenge === 'function') { - handler.handleChallenge(req, res); - return true; - } else if (typeof handler.handleRequest === 'function') { - // Some versions use handleRequest instead - handler.handleRequest(req, res); - return true; - } else { - // Fall back to manual response - const resp = this.getTokenResponse(token); - if (resp) { - res.setHeader('Content-Type', 'text/plain'); - res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0'); - res.writeHead(200); - res.end(resp); - return true; - } - } - } catch (err) { - // Challenge not found - } + try { + // Delegate to the HTTP-01 memory handler, which knows how to serve challenges + this.http01Handler.handleRequest(req, res); + return true; + } catch (error) { + console.error('Error handling ACME challenge:', error); + // If there was an error, send a 404 response + res.writeHead(404); + res.end('Not found'); + return true; } - - // Invalid ACME challenge - res.writeHead(404); - res.end('Not found'); - return true; } - + return false; } - - /** - * Get the response for a specific token if available - * This is a fallback method in case direct handler access isn't available - */ - private getTokenResponse(token: string): string | null { - if (!this.http01Handler) return null; - - try { - // Cast to any to handle different versions of the API - const handler = this.http01Handler as any; - - // Try different methods that might be available in different versions - if (typeof handler.getResponse === 'function') { - return handler.getResponse(token); - } - - if (typeof handler.getChallengeVerification === 'function') { - return handler.getChallengeVerification(token); - } - - // Try to access the challenges directly from the handler's internal state - if (handler.challenges && typeof handler.challenges === 'object' && handler.challenges[token]) { - return handler.challenges[token]; - } - - // Try the token map if it exists (another common pattern) - if (handler.tokenMap && typeof handler.tokenMap === 'object' && handler.tokenMap[token]) { - return handler.tokenMap[token]; - } - } catch (err) { - console.error('Error getting token response:', err); - } - - return null; - } /** * Request a certificate for a domain - * @param domain Domain name - * @param isRenewal Whether this is a renewal + * @param domain Domain name to request a certificate for + * @param isRenewal Whether this is a renewal request */ public async requestCertificate(domain: string, isRenewal: boolean = false): Promise { if (!this.smartAcme) { @@ -234,9 +177,10 @@ export class ChallengeResponder extends plugins.EventEmitter { } try { - // Request certificate via SmartAcme + // Request certificate using SmartACME const certObj = await this.smartAcme.getCertificateForDomain(domain); + // Convert the certificate object to our CertificateData format const certData: CertificateData = { domain, certificate: certObj.publicKey, @@ -246,26 +190,19 @@ export class ChallengeResponder extends plugins.EventEmitter { isRenewal }; - // SmartACME will emit its own events, but we'll emit our own too - // for consistency with the rest of the system - if (isRenewal) { - this.emit(CertificateEvents.CERTIFICATE_RENEWED, certData); - } else { - this.emit(CertificateEvents.CERTIFICATE_ISSUED, certData); - } - return certData; } catch (error) { - // Construct failure object + // Create failure object const failure: CertificateFailure = { domain, error: error instanceof Error ? error.message : String(error), - isRenewal, + isRenewal }; // Emit failure event this.emit(CertificateEvents.CERTIFICATE_FAILED, failure); + // Rethrow with more context throw new Error(`Failed to ${isRenewal ? 'renew' : 'obtain'} certificate for ${domain}: ${ error instanceof Error ? error.message : String(error) }`); @@ -273,19 +210,17 @@ export class ChallengeResponder extends plugins.EventEmitter { } /** - * Check if a certificate is expiring soon + * Check if a certificate is expiring soon and trigger renewal if needed * @param domain Domain name * @param certificate Certificate data - * @param thresholdDays Days before expiry to trigger a renewal + * @param thresholdDays Days before expiry to trigger renewal */ public checkCertificateExpiry( domain: string, certificate: CertificateData, thresholdDays: number = 30 ): void { - if (!certificate.expiryDate) { - return; - } + if (!certificate.expiryDate) return; const now = new Date(); const expiryDate = certificate.expiryDate; @@ -295,7 +230,7 @@ export class ChallengeResponder extends plugins.EventEmitter { const expiryInfo: CertificateExpiring = { domain, expiryDate, - daysRemaining: daysDifference, + daysRemaining: daysDifference }; this.emit(CertificateEvents.CERTIFICATE_EXPIRING, expiryInfo); diff --git a/ts/http/port80/port80-handler.ts b/ts/http/port80/port80-handler.ts index ff6dc21..1f75f74 100644 --- a/ts/http/port80/port80-handler.ts +++ b/ts/http/port80/port80-handler.ts @@ -550,6 +550,7 @@ export class Port80Handler extends plugins.EventEmitter { try { // Request certificate via ChallengeResponder + // The ChallengeResponder handles all ACME client interactions and will emit events const certData = await this.challengeResponder.requestCertificate(domain, isRenewal); // Update domain info with certificate data @@ -559,13 +560,9 @@ export class Port80Handler extends plugins.EventEmitter { domainInfo.expiryDate = certData.expiryDate; console.log(`Certificate ${isRenewal ? 'renewed' : 'obtained'} for ${domain}`); - - // The event will be emitted by the ChallengeResponder, we just store the certificate } catch (error: any) { const errorMsg = error instanceof Error ? error.message : String(error); console.error(`Error during certificate issuance for ${domain}:`, error); - - // The failure event will be emitted by the ChallengeResponder throw new CertificateError(errorMsg, domain, isRenewal); } finally { domainInfo.obtainingInProgress = false; diff --git a/ts/proxies/network-proxy/index.ts b/ts/proxies/network-proxy/index.ts index 7e4da1f..97aed83 100644 --- a/ts/proxies/network-proxy/index.ts +++ b/ts/proxies/network-proxy/index.ts @@ -1,3 +1,8 @@ /** * NetworkProxy implementation */ +// Re-export models +export * from './models/index.js'; + +// Core NetworkProxy will be added later: +// export { NetworkProxy } from './network-proxy.js'; diff --git a/ts/proxies/network-proxy/models/index.ts b/ts/proxies/network-proxy/models/index.ts index 56485b8..5deb611 100644 --- a/ts/proxies/network-proxy/models/index.ts +++ b/ts/proxies/network-proxy/models/index.ts @@ -1,3 +1,4 @@ /** * NetworkProxy models */ +export * from './types.js'; diff --git a/ts/proxies/network-proxy/models/types.ts b/ts/proxies/network-proxy/models/types.ts new file mode 100644 index 0000000..aefbd00 --- /dev/null +++ b/ts/proxies/network-proxy/models/types.ts @@ -0,0 +1,130 @@ +import * as plugins from '../../../plugins.js'; +import type { AcmeOptions } from '../../../certificate/models/certificate-types.js'; + +/** + * Configuration options for NetworkProxy + */ +export interface NetworkProxyOptions { + port: number; + maxConnections?: number; + keepAliveTimeout?: number; + headersTimeout?: number; + logLevel?: 'error' | 'warn' | 'info' | 'debug'; + cors?: { + allowOrigin?: string; + allowMethods?: string; + allowHeaders?: string; + maxAge?: number; + }; + + // Settings for SmartProxy integration + connectionPoolSize?: number; // Maximum connections to maintain in the pool to each backend + portProxyIntegration?: boolean; // Flag to indicate this proxy is used by SmartProxy + useExternalPort80Handler?: boolean; // Flag to indicate using external Port80Handler + // Protocol to use when proxying to backends: HTTP/1.x or HTTP/2 + backendProtocol?: 'http1' | 'http2'; + + // ACME certificate management options + acme?: AcmeOptions; +} + +/** + * Interface for a certificate entry in the cache + */ +export interface CertificateEntry { + key: string; + cert: string; + expires?: Date; +} + +/** + * Interface for reverse proxy configuration + */ +export interface ReverseProxyConfig { + destinationIps: string[]; + destinationPorts: number[]; + hostName: string; + privateKey: string; + publicKey: string; + authentication?: { + type: 'Basic'; + user: string; + pass: string; + }; + rewriteHostHeader?: boolean; + /** + * Protocol to use when proxying to this backend: 'http1' or 'http2'. + * Overrides the global backendProtocol option if set. + */ + backendProtocol?: 'http1' | 'http2'; +} + +/** + * Interface for connection tracking in the pool + */ +export interface ConnectionEntry { + socket: plugins.net.Socket; + lastUsed: number; + isIdle: boolean; +} + +/** + * WebSocket with heartbeat interface + */ +export interface WebSocketWithHeartbeat extends plugins.wsDefault { + lastPong: number; + isAlive: boolean; +} + +/** + * Logger interface for consistent logging across components + */ +export interface Logger { + debug(message: string, data?: any): void; + info(message: string, data?: any): void; + warn(message: string, data?: any): void; + error(message: string, data?: any): void; +} + +/** + * Creates a logger based on the specified log level + */ +export function createLogger(logLevel: string = 'info'): Logger { + const logLevels = { + error: 0, + warn: 1, + info: 2, + debug: 3 + }; + + return { + debug: (message: string, data?: any) => { + if (logLevels[logLevel] >= logLevels.debug) { + console.log(`[DEBUG] ${message}`, data || ''); + } + }, + info: (message: string, data?: any) => { + if (logLevels[logLevel] >= logLevels.info) { + console.log(`[INFO] ${message}`, data || ''); + } + }, + warn: (message: string, data?: any) => { + if (logLevels[logLevel] >= logLevels.warn) { + console.warn(`[WARN] ${message}`, data || ''); + } + }, + error: (message: string, data?: any) => { + if (logLevels[logLevel] >= logLevels.error) { + console.error(`[ERROR] ${message}`, data || ''); + } + } + }; +} + +// Backward compatibility interfaces +export interface INetworkProxyOptions extends NetworkProxyOptions {} +export interface ICertificateEntry extends CertificateEntry {} +export interface IReverseProxyConfig extends ReverseProxyConfig {} +export interface IConnectionEntry extends ConnectionEntry {} +export interface IWebSocketWithHeartbeat extends WebSocketWithHeartbeat {} +export interface ILogger extends Logger {} \ No newline at end of file diff --git a/ts/proxies/nftables-proxy/index.ts b/ts/proxies/nftables-proxy/index.ts index 7bb8fc5..a64372a 100644 --- a/ts/proxies/nftables-proxy/index.ts +++ b/ts/proxies/nftables-proxy/index.ts @@ -1,3 +1,5 @@ /** * NfTablesProxy implementation */ +// Core NfTablesProxy will be added later: +// export { NfTablesProxy } from './nftables-proxy.js'; diff --git a/ts/proxies/smart-proxy/index.ts b/ts/proxies/smart-proxy/index.ts index ad0e9f3..28e15d7 100644 --- a/ts/proxies/smart-proxy/index.ts +++ b/ts/proxies/smart-proxy/index.ts @@ -1,3 +1,8 @@ /** * SmartProxy implementation */ +// Re-export models +export * from './models/index.js'; + +// Core SmartProxy will be added later: +// export { SmartProxy } from './smart-proxy.js'; diff --git a/ts/proxies/smart-proxy/models/index.ts b/ts/proxies/smart-proxy/models/index.ts index abaa05b..33a81b2 100644 --- a/ts/proxies/smart-proxy/models/index.ts +++ b/ts/proxies/smart-proxy/models/index.ts @@ -1,3 +1,4 @@ /** * SmartProxy models */ +export * from './interfaces.js'; diff --git a/ts/proxies/smart-proxy/models/interfaces.ts b/ts/proxies/smart-proxy/models/interfaces.ts new file mode 100644 index 0000000..1846866 --- /dev/null +++ b/ts/proxies/smart-proxy/models/interfaces.ts @@ -0,0 +1,142 @@ +import * as plugins from '../../../plugins.js'; +import type { ForwardConfig } from '../../../forwarding/config/forwarding-types.js'; + +/** + * Provision object for static or HTTP-01 certificate + */ +export type SmartProxyCertProvisionObject = plugins.tsclass.network.ICert | 'http01'; + +/** + * Domain configuration with forwarding configuration + */ +export interface DomainConfig { + domains: string[]; // Glob patterns for domain(s) + forwarding: ForwardConfig; // Unified forwarding configuration +} + +/** + * Configuration options for the SmartProxy + */ +import type { AcmeOptions } from '../../../certificate/models/certificate-types.js'; +export interface SmartProxyOptions { + fromPort: number; + toPort: number; + targetIP?: string; // Global target host to proxy to, defaults to 'localhost' + domainConfigs: DomainConfig[]; + sniEnabled?: boolean; + defaultAllowedIPs?: string[]; + defaultBlockedIPs?: string[]; + preserveSourceIP?: boolean; + + // TLS options + pfx?: Buffer; + key?: string | Buffer | Array; + passphrase?: string; + cert?: string | Buffer | Array; + ca?: string | Buffer | Array; + ciphers?: string; + honorCipherOrder?: boolean; + rejectUnauthorized?: boolean; + secureProtocol?: string; + servername?: string; + minVersion?: string; + maxVersion?: string; + + // Timeout settings + initialDataTimeout?: number; // Timeout for initial data/SNI (ms), default: 60000 (60s) + socketTimeout?: number; // Socket inactivity timeout (ms), default: 3600000 (1h) + inactivityCheckInterval?: number; // How often to check for inactive connections (ms), default: 60000 (60s) + maxConnectionLifetime?: number; // Default max connection lifetime (ms), default: 86400000 (24h) + inactivityTimeout?: number; // Inactivity timeout (ms), default: 14400000 (4h) + + gracefulShutdownTimeout?: number; // (ms) maximum time to wait for connections to close during shutdown + globalPortRanges: Array<{ from: number; to: number }>; // Global allowed port ranges + forwardAllGlobalRanges?: boolean; // When true, forwards all connections on global port ranges to the global targetIP + + // Socket optimization settings + noDelay?: boolean; // Disable Nagle's algorithm (default: true) + keepAlive?: boolean; // Enable TCP keepalive (default: true) + keepAliveInitialDelay?: number; // Initial delay before sending keepalive probes (ms) + maxPendingDataSize?: number; // Maximum bytes to buffer during connection setup + + // Enhanced features + disableInactivityCheck?: boolean; // Disable inactivity checking entirely + enableKeepAliveProbes?: boolean; // Enable TCP keep-alive probes + enableDetailedLogging?: boolean; // Enable detailed connection logging + enableTlsDebugLogging?: boolean; // Enable TLS handshake debug logging + enableRandomizedTimeouts?: boolean; // Randomize timeouts slightly to prevent thundering herd + allowSessionTicket?: boolean; // Allow TLS session ticket for reconnection (default: true) + + // Rate limiting and security + maxConnectionsPerIP?: number; // Maximum simultaneous connections from a single IP + connectionRateLimitPerMinute?: number; // Max new connections per minute from a single IP + + // Enhanced keep-alive settings + keepAliveTreatment?: 'standard' | 'extended' | 'immortal'; // How to treat keep-alive connections + keepAliveInactivityMultiplier?: number; // Multiplier for inactivity timeout for keep-alive connections + extendedKeepAliveLifetime?: number; // Extended lifetime for keep-alive connections (ms) + + // NetworkProxy integration + useNetworkProxy?: number[]; // Array of ports to forward to NetworkProxy + networkProxyPort?: number; // Port where NetworkProxy is listening (default: 8443) + + // ACME configuration options for SmartProxy + acme?: AcmeOptions; + + /** + * Optional certificate provider callback. Return 'http01' to use HTTP-01 challenges, + * or a static certificate object for immediate provisioning. + */ + certProvisionFunction?: (domain: string) => Promise; +} + +/** + * Enhanced connection record + */ +export interface ConnectionRecord { + id: string; // Unique connection identifier + incoming: plugins.net.Socket; + outgoing: plugins.net.Socket | null; + incomingStartTime: number; + outgoingStartTime?: number; + outgoingClosedTime?: number; + lockedDomain?: string; // Used to lock this connection to the initial SNI + connectionClosed: boolean; // Flag to prevent multiple cleanup attempts + cleanupTimer?: NodeJS.Timeout; // Timer for max lifetime/inactivity + alertFallbackTimeout?: NodeJS.Timeout; // Timer for fallback after alert + lastActivity: number; // Last activity timestamp for inactivity detection + pendingData: Buffer[]; // Buffer to hold data during connection setup + pendingDataSize: number; // Track total size of pending data + + // Enhanced tracking fields + bytesReceived: number; // Total bytes received + bytesSent: number; // Total bytes sent + remoteIP: string; // Remote IP (cached for logging after socket close) + localPort: number; // Local port (cached for logging) + isTLS: boolean; // Whether this connection is a TLS connection + tlsHandshakeComplete: boolean; // Whether the TLS handshake is complete + hasReceivedInitialData: boolean; // Whether initial data has been received + domainConfig?: DomainConfig; // Associated domain config for this connection + + // Keep-alive tracking + hasKeepAlive: boolean; // Whether keep-alive is enabled for this connection + inactivityWarningIssued?: boolean; // Whether an inactivity warning has been issued + incomingTerminationReason?: string | null; // Reason for incoming termination + outgoingTerminationReason?: string | null; // Reason for outgoing termination + + // NetworkProxy tracking + usingNetworkProxy?: boolean; // Whether this connection is using a NetworkProxy + + // Renegotiation handler + renegotiationHandler?: (chunk: Buffer) => void; // Handler for renegotiation detection + + // Browser connection tracking + isBrowserConnection?: boolean; // Whether this connection appears to be from a browser + domainSwitches?: number; // Number of times the domain has been switched on this connection +} + +// Backward compatibility types +export type ISmartProxyCertProvisionObject = SmartProxyCertProvisionObject; +export interface IDomainConfig extends DomainConfig {} +export interface ISmartProxyOptions extends SmartProxyOptions {} +export interface IConnectionRecord extends ConnectionRecord {} \ No newline at end of file