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:
parent
6d1a3802ca
commit
4ac1df059f
@ -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
|
||||
|
||||
|
@ -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 | ✅ |
|
||||
|
@ -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.'
|
||||
}
|
||||
|
85
ts/http/port80/acme-interfaces.ts
Normal file
85
ts/http/port80/acme-interfaces.ts
Normal 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;
|
||||
}
|
@ -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);
|
||||
// Delegate to the HTTP-01 memory handler, which knows how to serve challenges
|
||||
this.http01Handler.handleRequest(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
|
||||
}
|
||||
}
|
||||
|
||||
// Invalid ACME challenge
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
@ -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;
|
||||
|
@ -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';
|
||||
|
@ -1,3 +1,4 @@
|
||||
/**
|
||||
* NetworkProxy models
|
||||
*/
|
||||
export * from './types.js';
|
||||
|
130
ts/proxies/network-proxy/models/types.ts
Normal file
130
ts/proxies/network-proxy/models/types.ts
Normal 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 {}
|
@ -1,3 +1,5 @@
|
||||
/**
|
||||
* NfTablesProxy implementation
|
||||
*/
|
||||
// Core NfTablesProxy will be added later:
|
||||
// export { NfTablesProxy } from './nftables-proxy.js';
|
||||
|
@ -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';
|
||||
|
@ -1,3 +1,4 @@
|
||||
/**
|
||||
* SmartProxy models
|
||||
*/
|
||||
export * from './interfaces.js';
|
||||
|
142
ts/proxies/smart-proxy/models/interfaces.ts
Normal file
142
ts/proxies/smart-proxy/models/interfaces.ts
Normal 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 {}
|
Loading…
x
Reference in New Issue
Block a user