feat(smartproxy): Migrate internal module paths and update HTTP/ACME components for SmartProxy

This commit is contained in:
Philipp Kunz 2025-05-09 17:10:19 +00:00
parent f1c0b8bfb7
commit 5a3bf2cae6
11 changed files with 363 additions and 194 deletions

View File

@ -1,5 +1,16 @@
# Changelog # Changelog
## 2025-05-09 - 12.1.0 - feat(smartproxy)
Migrate internal module paths and update HTTP/ACME components for SmartProxy
- Mark migration tasks as complete in readme.plan.md (checkboxes updated to ✅)
- Moved Port80Handler from ts/port80handler to ts/http/port80 (and extracted challenge responder)
- Migrated redirect handlers and router components to ts/http/redirects and ts/http/router respectively
- Updated re-exports in ts/index.ts and ts/plugins.ts to expose new module paths and additional exports
- Refactored CertificateEvents to include deprecation notes on Port80HandlerEvents
- Adjusted internal module organization for TLS, ACME, and forwarding (SNI extraction, client-hello parsing, etc.)
- Added minor logging and formatting improvements in several modules
## 2025-05-09 - 12.0.0 - BREAKING CHANGE(forwarding) ## 2025-05-09 - 12.0.0 - BREAKING CHANGE(forwarding)
Rename 'sniPassthrough' export to 'httpsPassthrough' for consistent naming and remove outdated forwarding example Rename 'sniPassthrough' export to 'httpsPassthrough' for consistent naming and remove outdated forwarding example

View File

@ -136,19 +136,19 @@ This component has the cleanest design, so we'll start migration here:
- [x] Extract SNI extraction to `ts/tls/sni/sni-extraction.ts` - [x] Extract SNI extraction to `ts/tls/sni/sni-extraction.ts`
- [x] Extract ClientHello parsing to `ts/tls/sni/client-hello-parser.ts` - [x] Extract ClientHello parsing to `ts/tls/sni/client-hello-parser.ts`
### Phase 5: HTTP Component Migration (Week 3) ### Phase 5: HTTP Component Migration (Week 3)
- [ ] Migrate Port80Handler - [x] Migrate Port80Handler
- [ ] Move `ts/port80handler/classes.port80handler.ts``ts/http/port80/port80-handler.ts` - [x] Move `ts/port80handler/classes.port80handler.ts``ts/http/port80/port80-handler.ts`
- [ ] Extract ACME challenge handling to `ts/http/port80/challenge-responder.ts` - [x] Extract ACME challenge handling to `ts/http/port80/challenge-responder.ts`
- [ ] Migrate redirect handlers - [x] Migrate redirect handlers
- [ ] Move `ts/redirect/classes.redirect.ts``ts/http/redirects/redirect-handler.ts` - [x] Move `ts/redirect/classes.redirect.ts``ts/http/redirects/redirect-handler.ts`
- [ ] Create `ts/http/redirects/ssl-redirect.ts` for specialized redirects - [x] Create `ts/http/redirects/ssl-redirect.ts` for specialized redirects
- [ ] Migrate router components - [x] Migrate router components
- [ ] Move `ts/classes.router.ts``ts/http/router/proxy-router.ts` - [x] Move `ts/classes.router.ts``ts/http/router/proxy-router.ts`
- [ ] Extract route matching to `ts/http/router/route-matcher.ts` - [x] Extract route matching to `ts/http/router/route-matcher.ts`
### Phase 6: Proxy Implementation Migration (Weeks 3-4) ### Phase 6: Proxy Implementation Migration (Weeks 3-4)
@ -259,9 +259,9 @@ This component has the cleanest design, so we'll start migration here:
| (new) | ts/tls/sni/sni-extraction.ts | ✅ | | (new) | ts/tls/sni/sni-extraction.ts | ✅ |
| (new) | ts/tls/sni/client-hello-parser.ts | ✅ | | (new) | ts/tls/sni/client-hello-parser.ts | ✅ |
| **HTTP Components** | | | | **HTTP Components** | | |
| ts/port80handler/classes.port80handler.ts | ts/http/port80/port80-handler.ts | | | ts/port80handler/classes.port80handler.ts | ts/http/port80/port80-handler.ts | |
| ts/redirect/classes.redirect.ts | ts/http/redirects/redirect-handler.ts | | | ts/redirect/classes.redirect.ts | ts/http/redirects/redirect-handler.ts | |
| ts/classes.router.ts | ts/http/router/proxy-router.ts | | | ts/classes.router.ts | ts/http/router/proxy-router.ts | |
| **SmartProxy Components** | | | | **SmartProxy Components** | | |
| ts/smartproxy/classes.smartproxy.ts | ts/proxies/smart-proxy/smart-proxy.ts | ❌ | | 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 | ❌ |

View File

@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartproxy', name: '@push.rocks/smartproxy',
version: '12.0.0', version: '12.1.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.' 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

@ -7,10 +7,14 @@ export enum CertificateEvents {
CERTIFICATE_FAILED = 'certificate-failed', CERTIFICATE_FAILED = 'certificate-failed',
CERTIFICATE_EXPIRING = 'certificate-expiring', CERTIFICATE_EXPIRING = 'certificate-expiring',
CERTIFICATE_APPLIED = 'certificate-applied', CERTIFICATE_APPLIED = 'certificate-applied',
// Events moved from Port80Handler for compatibility
MANAGER_STARTED = 'manager-started',
MANAGER_STOPPED = 'manager-stopped',
} }
/** /**
* Port80Handler-specific events including certificate-related ones * Port80Handler-specific events including certificate-related ones
* @deprecated Use CertificateEvents and HttpEvents instead
*/ */
export enum Port80HandlerEvents { export enum Port80HandlerEvents {
CERTIFICATE_ISSUED = 'certificate-issued', CERTIFICATE_ISSUED = 'certificate-issued',

View File

@ -2,7 +2,18 @@
* HTTP functionality module * HTTP functionality module
*/ */
// Export types and models
export * from './models/http-types.js';
// Export submodules // Export submodules
export * from './port80/index.js'; export * from './port80/index.js';
export * from './router/index.js'; export * from './router/index.js';
export * from './redirects/index.js'; export * from './redirects/index.js';
// Convenience namespace exports
export const Http = {
Port80: {
Handler: require('./port80/port80-handler.js').Port80Handler,
ChallengeResponder: require('./port80/challenge-responder.js').ChallengeResponder
}
};

View File

@ -35,58 +35,92 @@ export class ChallengeResponder extends plugins.EventEmitter {
*/ */
public async initialize(): Promise<void> { public async initialize(): Promise<void> {
try { try {
// Initialize SmartAcme
this.smartAcme = new plugins.smartacme.SmartAcme({
useProduction: this.useProduction,
accountEmail: this.email,
directoryUrl: this.useProduction
? 'https://acme-v02.api.letsencrypt.org/directory' // Production
: 'https://acme-staging-v02.api.letsencrypt.org/directory', // Staging
});
// Initialize HTTP-01 challenge handler // Initialize HTTP-01 challenge handler
this.http01Handler = new plugins.smartacme.handlers.Http01MemoryHandler(); this.http01Handler = new plugins.smartacme.handlers.Http01MemoryHandler();
this.smartAcme.useHttpChallenge(this.http01Handler);
// 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 // Ensure certificate store directory exists
await this.ensureCertificateStore(); await this.ensureCertificateStore();
// Subscribe to SmartAcme events // Set up event forwarding from SmartAcme
this.smartAcme.on('certificate-issued', (data: any) => { this.setupEventForwarding();
const certData: CertificateData = {
domain: data.domain, // Start SmartAcme
certificate: data.cert, await this.smartAcme.start();
privateKey: data.key,
expiryDate: new Date(data.expiryDate),
};
this.emit(CertificateEvents.CERTIFICATE_ISSUED, certData);
});
this.smartAcme.on('certificate-renewed', (data: any) => {
const certData: CertificateData = {
domain: data.domain,
certificate: data.cert,
privateKey: data.key,
expiryDate: new Date(data.expiryDate),
};
this.emit(CertificateEvents.CERTIFICATE_RENEWED, certData);
});
this.smartAcme.on('certificate-error', (data: any) => {
const error: CertificateFailure = {
domain: data.domain,
error: data.error instanceof Error ? data.error.message : String(data.error),
isRenewal: data.isRenewal || false,
};
this.emit(CertificateEvents.CERTIFICATE_FAILED, error);
});
await this.smartAcme.initialize();
} catch (error) { } catch (error) {
throw new Error(`Failed to initialize ACME client: ${error instanceof Error ? error.message : String(error)}`); throw new Error(`Failed to initialize ACME client: ${error instanceof Error ? error.message : String(error)}`);
} }
} }
/**
* 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 certificate store directory exists
*/ */
@ -110,32 +144,84 @@ export class ChallengeResponder extends plugins.EventEmitter {
} }
const url = req.url || '/'; const url = req.url || '/';
// Check if this is an ACME challenge request // Check if this is an ACME challenge request
if (url.startsWith('/.well-known/acme-challenge/')) { if (url.startsWith('/.well-known/acme-challenge/')) {
const token = url.split('/').pop() || ''; const token = url.split('/').pop() || '';
if (token) { if (token && this.http01Handler) {
const response = this.http01Handler.getResponse(token); try {
// Try to delegate to the handler - casting to any for flexibility
if (response) { const handler = this.http01Handler as any;
// This is a valid ACME challenge
res.setHeader('Content-Type', 'text/plain'); // Different versions may have different handler methods
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0'); if (typeof handler.handleChallenge === 'function') {
res.writeHead(200); handler.handleChallenge(req, res);
res.end(response); return true;
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 // Invalid ACME challenge
res.writeHead(404); res.writeHead(404);
res.end('Not found'); res.end('Not found');
return true; return true;
} }
return false; 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 * Request a certificate for a domain
@ -148,16 +234,20 @@ export class ChallengeResponder extends plugins.EventEmitter {
} }
try { try {
const result = await this.smartAcme.getCertificate(domain); // Request certificate via SmartAcme
const certObj = await this.smartAcme.getCertificateForDomain(domain);
const certData: CertificateData = { const certData: CertificateData = {
domain, domain,
certificate: result.cert, certificate: certObj.publicKey,
privateKey: result.key, privateKey: certObj.privateKey,
expiryDate: new Date(result.expiryDate), expiryDate: new Date(certObj.validUntil),
source: 'http01',
isRenewal
}; };
// Emit appropriate event // SmartACME will emit its own events, but we'll emit our own too
// for consistency with the rest of the system
if (isRenewal) { if (isRenewal) {
this.emit(CertificateEvents.CERTIFICATE_RENEWED, certData); this.emit(CertificateEvents.CERTIFICATE_RENEWED, certData);
} else { } else {

View File

@ -1,3 +1,13 @@
/** /**
* Port 80 handling * Port 80 handling
*/ */
// Export the main components
export { Port80Handler } from './port80-handler.js';
export { ChallengeResponder } from './challenge-responder.js';
// Export backward compatibility interfaces and types
export {
HttpError as Port80HandlerError,
CertificateError as CertError
} from '../models/http-types.js';

View File

@ -15,8 +15,8 @@ import {
HttpError, HttpError,
CertificateError, CertificateError,
ServerError, ServerError,
DomainCertificate
} from '../models/http-types.js'; } from '../models/http-types.js';
import type { DomainCertificate } from '../models/http-types.js';
import { ChallengeResponder } from './challenge-responder.js'; import { ChallengeResponder } from './challenge-responder.js';
// Re-export for backward compatibility // Re-export for backward compatibility
@ -105,7 +105,7 @@ export class Port80Handler extends plugins.EventEmitter {
if (this.server) { if (this.server) {
throw new ServerError('Server is already running'); throw new ServerError('Server is already running');
} }
if (this.isShuttingDown) { if (this.isShuttingDown) {
throw new ServerError('Server is shutting down'); throw new ServerError('Server is shutting down');
} }
@ -115,24 +115,22 @@ export class Port80Handler extends plugins.EventEmitter {
console.log('Port80Handler is disabled, skipping start'); console.log('Port80Handler is disabled, skipping start');
return; return;
} }
// Initialize SmartAcme with in-memory HTTP-01 challenge handler
if (this.options.enabled) { // Initialize the challenge responder if enabled
this.smartAcmeHttp01Handler = new plugins.smartacme.handlers.Http01MemoryHandler(); if (this.options.enabled && this.challengeResponder) {
this.smartAcme = new plugins.smartacme.SmartAcme({ try {
accountEmail: this.options.accountEmail, await this.challengeResponder.initialize();
certManager: new plugins.smartacme.certmanagers.MemoryCertManager(), } catch (error) {
environment: this.options.useProduction ? 'production' : 'integration', throw new ServerError(`Failed to initialize challenge responder: ${
challengeHandlers: [ this.smartAcmeHttp01Handler ], error instanceof Error ? error.message : String(error)
challengePriority: ['http-01'], }`);
}); }
await this.smartAcme.start();
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
this.server = plugins.http.createServer((req, res) => this.handleRequest(req, res)); this.server = plugins.http.createServer((req, res) => this.handleRequest(req, res));
this.server.on('error', (error: NodeJS.ErrnoException) => { this.server.on('error', (error: NodeJS.ErrnoException) => {
if (error.code === 'EACCES') { if (error.code === 'EACCES') {
reject(new ServerError(`Permission denied to bind to port ${this.options.port}. Try running with elevated privileges or use a port > 1024.`, error.code)); reject(new ServerError(`Permission denied to bind to port ${this.options.port}. Try running with elevated privileges or use a port > 1024.`, error.code));
@ -142,11 +140,11 @@ export class Port80Handler extends plugins.EventEmitter {
reject(new ServerError(error.message, error.code)); reject(new ServerError(error.message, error.code));
} }
}); });
this.server.listen(this.options.port, () => { this.server.listen(this.options.port, () => {
console.log(`Port80Handler is listening on port ${this.options.port}`); console.log(`Port80Handler is listening on port ${this.options.port}`);
this.emit(Port80HandlerEvents.MANAGER_STARTED, this.options.port); this.emit(CertificateEvents.MANAGER_STARTED, this.options.port);
// Start certificate process for domains with acmeMaintenance enabled // Start certificate process for domains with acmeMaintenance enabled
for (const [domain, domainInfo] of this.domainCertificates.entries()) { for (const [domain, domainInfo] of this.domainCertificates.entries()) {
// Skip glob patterns for certificate issuance // Skip glob patterns for certificate issuance
@ -154,14 +152,14 @@ export class Port80Handler extends plugins.EventEmitter {
console.log(`Skipping initial certificate for glob pattern: ${domain}`); console.log(`Skipping initial certificate for glob pattern: ${domain}`);
continue; continue;
} }
if (domainInfo.options.acmeMaintenance && !domainInfo.certObtained && !domainInfo.obtainingInProgress) { if (domainInfo.options.acmeMaintenance && !domainInfo.certObtained && !domainInfo.obtainingInProgress) {
this.obtainCertificate(domain).catch(err => { this.obtainCertificate(domain).catch(err => {
console.error(`Error obtaining initial certificate for ${domain}:`, err); console.error(`Error obtaining initial certificate for ${domain}:`, err);
}); });
} }
} }
resolve(); resolve();
}); });
} catch (error) { } catch (error) {
@ -172,22 +170,21 @@ export class Port80Handler extends plugins.EventEmitter {
} }
/** /**
* Stops the HTTP server and renewal timer * Stops the HTTP server and cleanup resources
*/ */
public async stop(): Promise<void> { public async stop(): Promise<void> {
if (!this.server) { if (!this.server) {
return; return;
} }
this.isShuttingDown = true; this.isShuttingDown = true;
return new Promise<void>((resolve) => { return new Promise<void>((resolve) => {
if (this.server) { if (this.server) {
this.server.close(() => { this.server.close(() => {
this.server = null; this.server = null;
this.isShuttingDown = false; this.isShuttingDown = false;
this.emit(Port80HandlerEvents.MANAGER_STOPPED); this.emit(CertificateEvents.MANAGER_STOPPED);
resolve(); resolve();
}); });
} else { } else {
@ -201,27 +198,27 @@ export class Port80Handler extends plugins.EventEmitter {
* Adds a domain with configuration options * Adds a domain with configuration options
* @param options Domain configuration options * @param options Domain configuration options
*/ */
public addDomain(options: IDomainOptions): void { public addDomain(options: DomainOptions): void {
if (!options.domainName || typeof options.domainName !== 'string') { if (!options.domainName || typeof options.domainName !== 'string') {
throw new Port80HandlerError('Invalid domain name'); throw new HttpError('Invalid domain name');
} }
const domainName = options.domainName; const domainName = options.domainName;
if (!this.domainCertificates.has(domainName)) { if (!this.domainCertificates.has(domainName)) {
this.domainCertificates.set(domainName, { this.domainCertificates.set(domainName, {
options, options,
certObtained: false, certObtained: false,
obtainingInProgress: false obtainingInProgress: false
}); });
console.log(`Domain added: ${domainName} with configuration:`, { console.log(`Domain added: ${domainName} with configuration:`, {
sslRedirect: options.sslRedirect, sslRedirect: options.sslRedirect,
acmeMaintenance: options.acmeMaintenance, acmeMaintenance: options.acmeMaintenance,
hasForward: !!options.forward, hasForward: !!options.forward,
hasAcmeForward: !!options.acmeForward hasAcmeForward: !!options.acmeForward
}); });
// If acmeMaintenance is enabled and not a glob pattern, start certificate process immediately // If acmeMaintenance is enabled and not a glob pattern, start certificate process immediately
if (options.acmeMaintenance && this.server && !this.isGlobPattern(domainName)) { if (options.acmeMaintenance && this.server && !this.isGlobPattern(domainName)) {
this.obtainCertificate(domainName).catch(err => { this.obtainCertificate(domainName).catch(err => {
@ -250,18 +247,18 @@ export class Port80Handler extends plugins.EventEmitter {
* Gets the certificate for a domain if it exists * Gets the certificate for a domain if it exists
* @param domain The domain to get the certificate for * @param domain The domain to get the certificate for
*/ */
public getCertificate(domain: string): ICertificateData | null { public getCertificate(domain: string): CertificateData | null {
// Can't get certificates for glob patterns // Can't get certificates for glob patterns
if (this.isGlobPattern(domain)) { if (this.isGlobPattern(domain)) {
return null; return null;
} }
const domainInfo = this.domainCertificates.get(domain); const domainInfo = this.domainCertificates.get(domain);
if (!domainInfo || !domainInfo.certObtained || !domainInfo.certificate || !domainInfo.privateKey) { if (!domainInfo || !domainInfo.certObtained || !domainInfo.certificate || !domainInfo.privateKey) {
return null; return null;
} }
return { return {
domain, domain,
certificate: domainInfo.certificate, certificate: domainInfo.certificate,
@ -286,7 +283,7 @@ export class Port80Handler extends plugins.EventEmitter {
* @param requestDomain The actual domain from the request * @param requestDomain The actual domain from the request
* @returns The domain info or null if not found * @returns The domain info or null if not found
*/ */
private getDomainInfoForRequest(requestDomain: string): { domainInfo: IDomainCertificate, pattern: string } | null { private getDomainInfoForRequest(requestDomain: string): { domainInfo: DomainCertificate, pattern: string } | null {
// Try direct match first // Try direct match first
if (this.domainCertificates.has(requestDomain)) { if (this.domainCertificates.has(requestDomain)) {
return { return {
@ -294,14 +291,14 @@ export class Port80Handler extends plugins.EventEmitter {
pattern: requestDomain pattern: requestDomain
}; };
} }
// Then try glob patterns // Then try glob patterns
for (const [pattern, domainInfo] of this.domainCertificates.entries()) { for (const [pattern, domainInfo] of this.domainCertificates.entries()) {
if (this.isGlobPattern(pattern) && this.domainMatchesPattern(requestDomain, pattern)) { if (this.isGlobPattern(pattern) && this.domainMatchesPattern(requestDomain, pattern)) {
return { domainInfo, pattern }; return { domainInfo, pattern };
} }
} }
return null; return null;
} }
@ -338,16 +335,45 @@ export class Port80Handler extends plugins.EventEmitter {
* @param res The HTTP response * @param res The HTTP response
*/ */
private handleRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void { private handleRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void {
// Emit request received event with basic info
this.emit(HttpEvents.REQUEST_RECEIVED, {
url: req.url,
method: req.method,
headers: req.headers
});
const hostHeader = req.headers.host; const hostHeader = req.headers.host;
if (!hostHeader) { if (!hostHeader) {
res.statusCode = 400; res.statusCode = HttpStatus.BAD_REQUEST;
res.end('Bad Request: Host header is missing'); res.end('Bad Request: Host header is missing');
return; return;
} }
// Extract domain (ignoring any port in the Host header) // Extract domain (ignoring any port in the Host header)
const domain = hostHeader.split(':')[0]; const domain = hostHeader.split(':')[0];
// Check if this is an ACME challenge request that our ChallengeResponder can handle
if (this.challengeResponder && req.url?.startsWith('/.well-known/acme-challenge/')) {
// Handle ACME HTTP-01 challenge with the challenge responder
const domainMatch = this.getDomainInfoForRequest(domain);
// If there's a specific ACME forwarding config for this domain, use that instead
if (domainMatch?.domainInfo.options.acmeForward) {
this.forwardRequest(req, res, domainMatch.domainInfo.options.acmeForward, 'ACME challenge');
return;
}
// If domain exists and has acmeMaintenance enabled, or we don't have the domain yet
// (for auto-provisioning), try to handle the ACME challenge
if (!domainMatch || domainMatch.domainInfo.options.acmeMaintenance) {
// Let the challenge responder try to handle this request
if (this.challengeResponder.handleRequest(req, res)) {
// Challenge was handled
return;
}
}
}
// Dynamic provisioning: if domain not yet managed, register for ACME and return 503 // Dynamic provisioning: if domain not yet managed, register for ACME and return 503
if (!this.domainCertificates.has(domain)) { if (!this.domainCertificates.has(domain)) {
try { try {
@ -355,14 +381,15 @@ export class Port80Handler extends plugins.EventEmitter {
} catch (err) { } catch (err) {
console.error(`Error registering domain for on-demand provisioning: ${err}`); console.error(`Error registering domain for on-demand provisioning: ${err}`);
} }
res.statusCode = 503; res.statusCode = HttpStatus.SERVICE_UNAVAILABLE;
res.end('Certificate issuance in progress'); res.end('Certificate issuance in progress');
return; return;
} }
// Get domain config, using glob pattern matching if needed // Get domain config, using glob pattern matching if needed
const domainMatch = this.getDomainInfoForRequest(domain); const domainMatch = this.getDomainInfoForRequest(domain);
if (!domainMatch) { if (!domainMatch) {
res.statusCode = 404; res.statusCode = HttpStatus.NOT_FOUND;
res.end('Domain not configured'); res.end('Domain not configured');
return; return;
} }
@ -370,29 +397,6 @@ export class Port80Handler extends plugins.EventEmitter {
const { domainInfo, pattern } = domainMatch; const { domainInfo, pattern } = domainMatch;
const options = domainInfo.options; const options = domainInfo.options;
// Handle ACME HTTP-01 challenge requests or forwarding
if (req.url && req.url.startsWith('/.well-known/acme-challenge/')) {
// Forward ACME requests if configured
if (options.acmeForward) {
this.forwardRequest(req, res, options.acmeForward, 'ACME challenge');
return;
}
// If not managing ACME for this domain, return 404
if (!options.acmeMaintenance) {
res.statusCode = 404;
res.end('Not found');
return;
}
// Delegate to Http01MemoryHandler
if (this.smartAcmeHttp01Handler) {
this.smartAcmeHttp01Handler.handleRequest(req, res);
} else {
res.statusCode = 500;
res.end('ACME HTTP-01 handler not initialized');
}
return;
}
// Check if we should forward non-ACME requests // Check if we should forward non-ACME requests
if (options.forward) { if (options.forward) {
this.forwardRequest(req, res, options.forward, 'HTTP'); this.forwardRequest(req, res, options.forward, 'HTTP');
@ -405,13 +409,13 @@ export class Port80Handler extends plugins.EventEmitter {
const httpsPort = this.options.httpsRedirectPort; const httpsPort = this.options.httpsRedirectPort;
const portSuffix = httpsPort === 443 ? '' : `:${httpsPort}`; const portSuffix = httpsPort === 443 ? '' : `:${httpsPort}`;
const redirectUrl = `https://${domain}${portSuffix}${req.url || '/'}`; const redirectUrl = `https://${domain}${portSuffix}${req.url || '/'}`;
res.statusCode = 301; res.statusCode = HttpStatus.MOVED_PERMANENTLY;
res.setHeader('Location', redirectUrl); res.setHeader('Location', redirectUrl);
res.end(`Redirecting to ${redirectUrl}`); res.end(`Redirecting to ${redirectUrl}`);
return; return;
} }
// Handle case where certificate maintenance is enabled but not yet obtained // Handle case where certificate maintenance is enabled but not yet obtained
// (Skip for glob patterns as they can't have certificates) // (Skip for glob patterns as they can't have certificates)
if (!this.isGlobPattern(pattern) && options.acmeMaintenance && !domainInfo.certObtained) { if (!this.isGlobPattern(pattern) && options.acmeMaintenance && !domainInfo.certObtained) {
@ -419,7 +423,7 @@ export class Port80Handler extends plugins.EventEmitter {
if (!domainInfo.obtainingInProgress) { if (!domainInfo.obtainingInProgress) {
this.obtainCertificate(domain).catch(err => { this.obtainCertificate(domain).catch(err => {
const errorMessage = err instanceof Error ? err.message : 'Unknown error'; const errorMessage = err instanceof Error ? err.message : 'Unknown error';
this.emit(Port80HandlerEvents.CERTIFICATE_FAILED, { this.emit(CertificateEvents.CERTIFICATE_FAILED, {
domain, domain,
error: errorMessage, error: errorMessage,
isRenewal: false isRenewal: false
@ -427,15 +431,22 @@ export class Port80Handler extends plugins.EventEmitter {
console.error(`Error obtaining certificate for ${domain}:`, err); console.error(`Error obtaining certificate for ${domain}:`, err);
}); });
} }
res.statusCode = 503; res.statusCode = HttpStatus.SERVICE_UNAVAILABLE;
res.end('Certificate issuance in progress, please try again later.'); res.end('Certificate issuance in progress, please try again later.');
return; return;
} }
// Default response for unhandled request // Default response for unhandled request
res.statusCode = 404; res.statusCode = HttpStatus.NOT_FOUND;
res.end('No handlers configured for this request'); res.end('No handlers configured for this request');
// Emit request handled event
this.emit(HttpEvents.REQUEST_HANDLED, {
domain,
url: req.url,
statusCode: res.statusCode
});
} }
/** /**
@ -446,9 +457,9 @@ export class Port80Handler extends plugins.EventEmitter {
* @param requestType Type of request for logging * @param requestType Type of request for logging
*/ */
private forwardRequest( private forwardRequest(
req: plugins.http.IncomingMessage, req: plugins.http.IncomingMessage,
res: plugins.http.ServerResponse, res: plugins.http.ServerResponse,
target: IForwardConfig, target: ForwardConfig,
requestType: string requestType: string
): void { ): void {
const options = { const options = {
@ -458,40 +469,47 @@ export class Port80Handler extends plugins.EventEmitter {
method: req.method, method: req.method,
headers: { ...req.headers } headers: { ...req.headers }
}; };
const domain = req.headers.host?.split(':')[0] || 'unknown'; const domain = req.headers.host?.split(':')[0] || 'unknown';
console.log(`Forwarding ${requestType} request for ${domain} to ${target.ip}:${target.port}`); console.log(`Forwarding ${requestType} request for ${domain} to ${target.ip}:${target.port}`);
const proxyReq = plugins.http.request(options, (proxyRes) => { const proxyReq = plugins.http.request(options, (proxyRes) => {
// Copy status code // Copy status code
res.statusCode = proxyRes.statusCode || 500; res.statusCode = proxyRes.statusCode || HttpStatus.INTERNAL_SERVER_ERROR;
// Copy headers // Copy headers
for (const [key, value] of Object.entries(proxyRes.headers)) { for (const [key, value] of Object.entries(proxyRes.headers)) {
if (value) res.setHeader(key, value); if (value) res.setHeader(key, value);
} }
// Pipe response data // Pipe response data
proxyRes.pipe(res); proxyRes.pipe(res);
this.emit(Port80HandlerEvents.REQUEST_FORWARDED, { this.emit(HttpEvents.REQUEST_FORWARDED, {
domain, domain,
requestType, requestType,
target: `${target.ip}:${target.port}`, target: `${target.ip}:${target.port}`,
statusCode: proxyRes.statusCode statusCode: proxyRes.statusCode
}); });
}); });
proxyReq.on('error', (error) => { proxyReq.on('error', (error) => {
console.error(`Error forwarding request to ${target.ip}:${target.port}:`, error); console.error(`Error forwarding request to ${target.ip}:${target.port}:`, error);
this.emit(HttpEvents.REQUEST_ERROR, {
domain,
error: error.message,
target: `${target.ip}:${target.port}`
});
if (!res.headersSent) { if (!res.headersSent) {
res.statusCode = 502; res.statusCode = HttpStatus.INTERNAL_SERVER_ERROR;
res.end(`Proxy error: ${error.message}`); res.end(`Proxy error: ${error.message}`);
} else { } else {
res.end(); res.end();
} }
}); });
// Pipe original request to proxy request // Pipe original request to proxy request
if (req.readable) { if (req.readable) {
req.pipe(proxyReq); req.pipe(proxyReq);
@ -506,59 +524,48 @@ export class Port80Handler extends plugins.EventEmitter {
* @param domain The domain to obtain a certificate for * @param domain The domain to obtain a certificate for
* @param isRenewal Whether this is a renewal attempt * @param isRenewal Whether this is a renewal attempt
*/ */
/**
* Obtains a certificate for a domain using SmartAcme HTTP-01 challenges
* @param domain The domain to obtain a certificate for
* @param isRenewal Whether this is a renewal attempt
*/
private async obtainCertificate(domain: string, isRenewal: boolean = false): Promise<void> { private async obtainCertificate(domain: string, isRenewal: boolean = false): Promise<void> {
if (this.isGlobPattern(domain)) { if (this.isGlobPattern(domain)) {
throw new CertificateError('Cannot obtain certificates for glob pattern domains', domain, isRenewal); throw new CertificateError('Cannot obtain certificates for glob pattern domains', domain, isRenewal);
} }
const domainInfo = this.domainCertificates.get(domain)!; const domainInfo = this.domainCertificates.get(domain)!;
if (!domainInfo.options.acmeMaintenance) { if (!domainInfo.options.acmeMaintenance) {
console.log(`Skipping certificate issuance for ${domain} - acmeMaintenance is disabled`); console.log(`Skipping certificate issuance for ${domain} - acmeMaintenance is disabled`);
return; return;
} }
if (domainInfo.obtainingInProgress) { if (domainInfo.obtainingInProgress) {
console.log(`Certificate issuance already in progress for ${domain}`); console.log(`Certificate issuance already in progress for ${domain}`);
return; return;
} }
if (!this.smartAcme) {
throw new Port80HandlerError('SmartAcme is not initialized'); if (!this.challengeResponder) {
throw new HttpError('Challenge responder is not initialized');
} }
domainInfo.obtainingInProgress = true; domainInfo.obtainingInProgress = true;
domainInfo.lastRenewalAttempt = new Date(); domainInfo.lastRenewalAttempt = new Date();
try { try {
// Request certificate via SmartAcme // Request certificate via ChallengeResponder
const certObj = await this.smartAcme.getCertificateForDomain(domain); const certData = await this.challengeResponder.requestCertificate(domain, isRenewal);
const certificate = certObj.publicKey;
const privateKey = certObj.privateKey; // Update domain info with certificate data
const expiryDate = new Date(certObj.validUntil); domainInfo.certificate = certData.certificate;
domainInfo.certificate = certificate; domainInfo.privateKey = certData.privateKey;
domainInfo.privateKey = privateKey;
domainInfo.certObtained = true; domainInfo.certObtained = true;
domainInfo.expiryDate = expiryDate; domainInfo.expiryDate = certData.expiryDate;
console.log(`Certificate ${isRenewal ? 'renewed' : 'obtained'} for ${domain}`); console.log(`Certificate ${isRenewal ? 'renewed' : 'obtained'} for ${domain}`);
// Persistence moved to CertProvisioner
const eventType = isRenewal // The event will be emitted by the ChallengeResponder, we just store the certificate
? Port80HandlerEvents.CERTIFICATE_RENEWED
: Port80HandlerEvents.CERTIFICATE_ISSUED;
this.emitCertificateEvent(eventType, {
domain,
certificate,
privateKey,
expiryDate: expiryDate || this.getDefaultExpiryDate()
});
} catch (error: any) { } catch (error: any) {
const errorMsg = error?.message || 'Unknown error'; const errorMsg = error instanceof Error ? error.message : String(error);
console.error(`Error during certificate issuance for ${domain}:`, error); console.error(`Error during certificate issuance for ${domain}:`, error);
this.emit(Port80HandlerEvents.CERTIFICATE_FAILED, {
domain, // The failure event will be emitted by the ChallengeResponder
error: errorMsg,
isRenewal
} as ICertificateFailure);
throw new CertificateError(errorMsg, domain, isRenewal); throw new CertificateError(errorMsg, domain, isRenewal);
} finally { } finally {
domainInfo.obtainingInProgress = false; domainInfo.obtainingInProgress = false;
@ -608,7 +615,7 @@ export class Port80Handler extends plugins.EventEmitter {
* @param eventType The event type to emit * @param eventType The event type to emit
* @param data The certificate data * @param data The certificate data
*/ */
private emitCertificateEvent(eventType: Port80HandlerEvents, data: ICertificateData): void { private emitCertificateEvent(eventType: CertificateEvents, data: CertificateData): void {
this.emit(eventType, data); this.emit(eventType, data);
} }
@ -670,7 +677,7 @@ export class Port80Handler extends plugins.EventEmitter {
*/ */
public async renewCertificate(domain: string): Promise<void> { public async renewCertificate(domain: string): Promise<void> {
if (!this.domainCertificates.has(domain)) { if (!this.domainCertificates.has(domain)) {
throw new Port80HandlerError(`Domain not managed: ${domain}`); throw new HttpError(`Domain not managed: ${domain}`);
} }
// Trigger renewal via ACME // Trigger renewal via ACME
await this.obtainCertificate(domain, true); await this.obtainCertificate(domain, true);

View File

@ -5,7 +5,17 @@
// Legacy exports (to maintain backward compatibility) // Legacy exports (to maintain backward compatibility)
export * from './nfttablesproxy/classes.nftablesproxy.js'; export * from './nfttablesproxy/classes.nftablesproxy.js';
export * from './networkproxy/index.js'; export * from './networkproxy/index.js';
export * from './port80handler/classes.port80handler.js'; // Export port80handler elements selectively to avoid conflicts
export {
Port80Handler,
default as Port80HandlerDefault,
HttpError,
ServerError,
CertificateError
} from './port80handler/classes.port80handler.js';
// Use re-export to control the names
export { Port80HandlerEvents } from './certificate/events/certificate-events.js';
export * from './redirect/classes.redirect.js'; export * from './redirect/classes.redirect.js';
export * from './smartproxy/classes.smartproxy.js'; export * from './smartproxy/classes.smartproxy.js';
// Original: export * from './smartproxy/classes.pp.snihandler.js' // Original: export * from './smartproxy/classes.pp.snihandler.js'
@ -19,4 +29,5 @@ export * from './core/models/common-types.js';
// Modular exports for new architecture // Modular exports for new architecture
export * as forwarding from './forwarding/index.js'; export * as forwarding from './forwarding/index.js';
export * as certificate from './certificate/index.js'; export * as certificate from './certificate/index.js';
export * as tls from './tls/index.js'; export * as tls from './tls/index.js';
export * as http from './http/index.js';

View File

@ -1,5 +1,6 @@
// node native scope // node native scope
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import * as fs from 'fs';
import * as http from 'http'; import * as http from 'http';
import * as https from 'https'; import * as https from 'https';
import * as net from 'net'; import * as net from 'net';
@ -7,7 +8,7 @@ import * as tls from 'tls';
import * as url from 'url'; import * as url from 'url';
import * as http2 from 'http2'; import * as http2 from 'http2';
export { EventEmitter, http, https, net, tls, url, http2 }; export { EventEmitter, fs, http, https, net, tls, url, http2 };
// tsclass scope // tsclass scope
import * as tsclass from '@tsclass/tsclass'; import * as tsclass from '@tsclass/tsclass';

View File

@ -0,0 +1,24 @@
/**
* TEMPORARY FILE FOR BACKWARD COMPATIBILITY
* This will be removed in a future version when all imports are updated
* @deprecated Use the new HTTP module instead
*/
// Re-export the Port80Handler from its new location
export * from '../http/port80/port80-handler.js';
// Re-export HTTP error types for backward compatibility
export * from '../http/models/http-types.js';
// Re-export selected events to avoid name conflicts
export {
CertificateEvents,
Port80HandlerEvents,
CertProvisionerEvents
} from '../certificate/events/certificate-events.js';
// Import the new Port80Handler
import { Port80Handler } from '../http/port80/port80-handler.js';
// Export it as the default export for backward compatibility
export default Port80Handler;