feat(acme): Add ACME interfaces for Port80Handler and refactor ChallengeResponder to use new acme-interfaces, enhancing event subscription and certificate workflows.

This commit is contained in:
Philipp Kunz 2025-05-09 17:28:27 +00:00
parent 6d1a3802ca
commit 4ac1df059f
13 changed files with 510 additions and 192 deletions

View File

@ -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

View File

@ -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 | ✅ |

View File

@ -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.'
}

View File

@ -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<any>[];
challengePriority?: string[];
retryOptions?: {
retries?: number;
factor?: number;
minTimeoutMs?: number;
maxTimeoutMs?: number;
};
}
/**
* Interface for certificate manager
*/
export interface ICertManager {
init(): Promise<void>;
get(domainName: string): Promise<SmartAcmeCert | null>;
put(cert: SmartAcmeCert): Promise<SmartAcmeCert>;
delete(domainName: string): Promise<void>;
close?(): Promise<void>;
}
/**
* Interface for challenge handler
*/
export interface IChallengeHandler<T> {
getSupportedTypes(): string[];
prepare(ch: T): Promise<void>;
verify?(ch: T): Promise<void>;
cleanup(ch: T): Promise<void>;
checkWetherDomainIsSupported(domain: string): Promise<boolean>;
}
/**
* 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<Http01Challenge> {
handleRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse, next?: () => void): void;
}
/**
* SmartAcme main class interface
*/
export interface SmartAcme {
start(): Promise<void>;
stop(): Promise<void>;
getCertificateForDomain(domain: string): Promise<SmartAcmeCert>;
on?(event: string, listener: (data: any) => void): void;
eventEmitter?: plugins.EventEmitter;
}

View File

@ -8,13 +8,20 @@ import type {
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<void> {
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();
this.setupEventListeners();
// Start SmartAcme
// 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<void> {
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<CertificateData> {
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);

View File

@ -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;

View File

@ -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';

View File

@ -1,3 +1,4 @@
/**
* NetworkProxy models
*/
export * from './types.js';

View File

@ -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 {}

View File

@ -1,3 +1,5 @@
/**
* NfTablesProxy implementation
*/
// Core NfTablesProxy will be added later:
// export { NfTablesProxy } from './nftables-proxy.js';

View File

@ -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';

View File

@ -1,3 +1,4 @@
/**
* SmartProxy models
*/
export * from './interfaces.js';

View File

@ -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<Buffer | string>;
passphrase?: string;
cert?: string | Buffer | Array<string | Buffer>;
ca?: string | Buffer | Array<string | Buffer>;
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<SmartProxyCertProvisionObject>;
}
/**
* 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 {}