feat(smartproxy): Migrate internal module paths and update HTTP/ACME components for SmartProxy
This commit is contained in:
parent
f1c0b8bfb7
commit
5a3bf2cae6
11
changelog.md
11
changelog.md
@ -1,5 +1,16 @@
|
||||
# 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)
|
||||
Rename 'sniPassthrough' export to 'httpsPassthrough' for consistent naming and remove outdated forwarding example
|
||||
|
||||
|
@ -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 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
|
||||
- [ ] Move `ts/port80handler/classes.port80handler.ts` → `ts/http/port80/port80-handler.ts`
|
||||
- [ ] Extract ACME challenge handling to `ts/http/port80/challenge-responder.ts`
|
||||
- [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`
|
||||
|
||||
- [ ] Migrate redirect handlers
|
||||
- [ ] Move `ts/redirect/classes.redirect.ts` → `ts/http/redirects/redirect-handler.ts`
|
||||
- [ ] Create `ts/http/redirects/ssl-redirect.ts` for specialized redirects
|
||||
- [x] Migrate redirect handlers
|
||||
- [x] Move `ts/redirect/classes.redirect.ts` → `ts/http/redirects/redirect-handler.ts`
|
||||
- [x] Create `ts/http/redirects/ssl-redirect.ts` for specialized redirects
|
||||
|
||||
- [ ] Migrate router components
|
||||
- [ ] Move `ts/classes.router.ts` → `ts/http/router/proxy-router.ts`
|
||||
- [ ] Extract route matching to `ts/http/router/route-matcher.ts`
|
||||
- [x] Migrate router components
|
||||
- [x] Move `ts/classes.router.ts` → `ts/http/router/proxy-router.ts`
|
||||
- [x] Extract route matching to `ts/http/router/route-matcher.ts`
|
||||
|
||||
### 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/client-hello-parser.ts | ✅ |
|
||||
| **HTTP Components** | | |
|
||||
| ts/port80handler/classes.port80handler.ts | ts/http/port80/port80-handler.ts | ❌ |
|
||||
| ts/redirect/classes.redirect.ts | ts/http/redirects/redirect-handler.ts | ❌ |
|
||||
| ts/classes.router.ts | ts/http/router/proxy-router.ts | ❌ |
|
||||
| ts/port80handler/classes.port80handler.ts | ts/http/port80/port80-handler.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 | ❌ |
|
||||
|
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
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.'
|
||||
}
|
||||
|
@ -7,10 +7,14 @@ export enum CertificateEvents {
|
||||
CERTIFICATE_FAILED = 'certificate-failed',
|
||||
CERTIFICATE_EXPIRING = 'certificate-expiring',
|
||||
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
|
||||
* @deprecated Use CertificateEvents and HttpEvents instead
|
||||
*/
|
||||
export enum Port80HandlerEvents {
|
||||
CERTIFICATE_ISSUED = 'certificate-issued',
|
||||
|
@ -2,7 +2,18 @@
|
||||
* HTTP functionality module
|
||||
*/
|
||||
|
||||
// Export types and models
|
||||
export * from './models/http-types.js';
|
||||
|
||||
// Export submodules
|
||||
export * from './port80/index.js';
|
||||
export * from './router/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
|
||||
}
|
||||
};
|
||||
|
@ -35,58 +35,92 @@ export class ChallengeResponder extends plugins.EventEmitter {
|
||||
*/
|
||||
public async initialize(): Promise<void> {
|
||||
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
|
||||
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
|
||||
await this.ensureCertificateStore();
|
||||
|
||||
// Subscribe to SmartAcme events
|
||||
this.smartAcme.on('certificate-issued', (data: any) => {
|
||||
const certData: CertificateData = {
|
||||
domain: data.domain,
|
||||
certificate: data.cert,
|
||||
privateKey: data.key,
|
||||
expiryDate: new Date(data.expiryDate),
|
||||
};
|
||||
this.emit(CertificateEvents.CERTIFICATE_ISSUED, certData);
|
||||
});
|
||||
// Set up event forwarding from SmartAcme
|
||||
this.setupEventForwarding();
|
||||
|
||||
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();
|
||||
// Start SmartAcme
|
||||
await this.smartAcme.start();
|
||||
} catch (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
|
||||
*/
|
||||
@ -115,16 +149,32 @@ export class ChallengeResponder extends plugins.EventEmitter {
|
||||
if (url.startsWith('/.well-known/acme-challenge/')) {
|
||||
const token = url.split('/').pop() || '';
|
||||
|
||||
if (token) {
|
||||
const response = this.http01Handler.getResponse(token);
|
||||
if (token && this.http01Handler) {
|
||||
try {
|
||||
// Try to delegate to the handler - casting to any for flexibility
|
||||
const handler = this.http01Handler as any;
|
||||
|
||||
if (response) {
|
||||
// This is a valid ACME challenge
|
||||
res.setHeader('Content-Type', 'text/plain');
|
||||
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
|
||||
res.writeHead(200);
|
||||
res.end(response);
|
||||
return true;
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,6 +187,42 @@ export class ChallengeResponder extends plugins.EventEmitter {
|
||||
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
|
||||
@ -148,16 +234,20 @@ export class ChallengeResponder extends plugins.EventEmitter {
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.smartAcme.getCertificate(domain);
|
||||
// Request certificate via SmartAcme
|
||||
const certObj = await this.smartAcme.getCertificateForDomain(domain);
|
||||
|
||||
const certData: CertificateData = {
|
||||
domain,
|
||||
certificate: result.cert,
|
||||
privateKey: result.key,
|
||||
expiryDate: new Date(result.expiryDate),
|
||||
certificate: certObj.publicKey,
|
||||
privateKey: certObj.privateKey,
|
||||
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) {
|
||||
this.emit(CertificateEvents.CERTIFICATE_RENEWED, certData);
|
||||
} else {
|
||||
|
@ -1,3 +1,13 @@
|
||||
/**
|
||||
* 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';
|
||||
|
@ -15,8 +15,8 @@ import {
|
||||
HttpError,
|
||||
CertificateError,
|
||||
ServerError,
|
||||
DomainCertificate
|
||||
} from '../models/http-types.js';
|
||||
import type { DomainCertificate } from '../models/http-types.js';
|
||||
import { ChallengeResponder } from './challenge-responder.js';
|
||||
|
||||
// Re-export for backward compatibility
|
||||
@ -115,22 +115,20 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
console.log('Port80Handler is disabled, skipping start');
|
||||
return;
|
||||
}
|
||||
// Initialize SmartAcme with in-memory HTTP-01 challenge handler
|
||||
if (this.options.enabled) {
|
||||
this.smartAcmeHttp01Handler = new plugins.smartacme.handlers.Http01MemoryHandler();
|
||||
this.smartAcme = new plugins.smartacme.SmartAcme({
|
||||
accountEmail: this.options.accountEmail,
|
||||
certManager: new plugins.smartacme.certmanagers.MemoryCertManager(),
|
||||
environment: this.options.useProduction ? 'production' : 'integration',
|
||||
challengeHandlers: [ this.smartAcmeHttp01Handler ],
|
||||
challengePriority: ['http-01'],
|
||||
});
|
||||
await this.smartAcme.start();
|
||||
|
||||
// Initialize the challenge responder if enabled
|
||||
if (this.options.enabled && this.challengeResponder) {
|
||||
try {
|
||||
await this.challengeResponder.initialize();
|
||||
} catch (error) {
|
||||
throw new ServerError(`Failed to initialize challenge responder: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`);
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
|
||||
this.server = plugins.http.createServer((req, res) => this.handleRequest(req, res));
|
||||
|
||||
this.server.on('error', (error: NodeJS.ErrnoException) => {
|
||||
@ -145,7 +143,7 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
|
||||
this.server.listen(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
|
||||
for (const [domain, domainInfo] of this.domainCertificates.entries()) {
|
||||
@ -172,7 +170,7 @@ 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> {
|
||||
if (!this.server) {
|
||||
@ -181,13 +179,12 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
|
||||
this.isShuttingDown = true;
|
||||
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
if (this.server) {
|
||||
this.server.close(() => {
|
||||
this.server = null;
|
||||
this.isShuttingDown = false;
|
||||
this.emit(Port80HandlerEvents.MANAGER_STOPPED);
|
||||
this.emit(CertificateEvents.MANAGER_STOPPED);
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
@ -201,9 +198,9 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
* Adds a domain with configuration options
|
||||
* @param options Domain configuration options
|
||||
*/
|
||||
public addDomain(options: IDomainOptions): void {
|
||||
public addDomain(options: DomainOptions): void {
|
||||
if (!options.domainName || typeof options.domainName !== 'string') {
|
||||
throw new Port80HandlerError('Invalid domain name');
|
||||
throw new HttpError('Invalid domain name');
|
||||
}
|
||||
|
||||
const domainName = options.domainName;
|
||||
@ -250,7 +247,7 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
* Gets the certificate for a domain if it exists
|
||||
* @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
|
||||
if (this.isGlobPattern(domain)) {
|
||||
return null;
|
||||
@ -286,7 +283,7 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
* @param requestDomain The actual domain from the request
|
||||
* @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
|
||||
if (this.domainCertificates.has(requestDomain)) {
|
||||
return {
|
||||
@ -338,9 +335,16 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
* @param res The HTTP response
|
||||
*/
|
||||
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;
|
||||
if (!hostHeader) {
|
||||
res.statusCode = 400;
|
||||
res.statusCode = HttpStatus.BAD_REQUEST;
|
||||
res.end('Bad Request: Host header is missing');
|
||||
return;
|
||||
}
|
||||
@ -348,6 +352,28 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
// Extract domain (ignoring any port in the Host header)
|
||||
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
|
||||
if (!this.domainCertificates.has(domain)) {
|
||||
try {
|
||||
@ -355,14 +381,15 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
} catch (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');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get domain config, using glob pattern matching if needed
|
||||
const domainMatch = this.getDomainInfoForRequest(domain);
|
||||
if (!domainMatch) {
|
||||
res.statusCode = 404;
|
||||
res.statusCode = HttpStatus.NOT_FOUND;
|
||||
res.end('Domain not configured');
|
||||
return;
|
||||
}
|
||||
@ -370,29 +397,6 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
const { domainInfo, pattern } = domainMatch;
|
||||
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
|
||||
if (options.forward) {
|
||||
this.forwardRequest(req, res, options.forward, 'HTTP');
|
||||
@ -406,7 +410,7 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
const portSuffix = httpsPort === 443 ? '' : `:${httpsPort}`;
|
||||
const redirectUrl = `https://${domain}${portSuffix}${req.url || '/'}`;
|
||||
|
||||
res.statusCode = 301;
|
||||
res.statusCode = HttpStatus.MOVED_PERMANENTLY;
|
||||
res.setHeader('Location', redirectUrl);
|
||||
res.end(`Redirecting to ${redirectUrl}`);
|
||||
return;
|
||||
@ -419,7 +423,7 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
if (!domainInfo.obtainingInProgress) {
|
||||
this.obtainCertificate(domain).catch(err => {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
||||
this.emit(Port80HandlerEvents.CERTIFICATE_FAILED, {
|
||||
this.emit(CertificateEvents.CERTIFICATE_FAILED, {
|
||||
domain,
|
||||
error: errorMessage,
|
||||
isRenewal: false
|
||||
@ -428,14 +432,21 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
res.statusCode = 503;
|
||||
res.statusCode = HttpStatus.SERVICE_UNAVAILABLE;
|
||||
res.end('Certificate issuance in progress, please try again later.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Default response for unhandled request
|
||||
res.statusCode = 404;
|
||||
res.statusCode = HttpStatus.NOT_FOUND;
|
||||
res.end('No handlers configured for this request');
|
||||
|
||||
// Emit request handled event
|
||||
this.emit(HttpEvents.REQUEST_HANDLED, {
|
||||
domain,
|
||||
url: req.url,
|
||||
statusCode: res.statusCode
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -448,7 +459,7 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
private forwardRequest(
|
||||
req: plugins.http.IncomingMessage,
|
||||
res: plugins.http.ServerResponse,
|
||||
target: IForwardConfig,
|
||||
target: ForwardConfig,
|
||||
requestType: string
|
||||
): void {
|
||||
const options = {
|
||||
@ -464,7 +475,7 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
|
||||
const proxyReq = plugins.http.request(options, (proxyRes) => {
|
||||
// Copy status code
|
||||
res.statusCode = proxyRes.statusCode || 500;
|
||||
res.statusCode = proxyRes.statusCode || HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
|
||||
// Copy headers
|
||||
for (const [key, value] of Object.entries(proxyRes.headers)) {
|
||||
@ -474,7 +485,7 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
// Pipe response data
|
||||
proxyRes.pipe(res);
|
||||
|
||||
this.emit(Port80HandlerEvents.REQUEST_FORWARDED, {
|
||||
this.emit(HttpEvents.REQUEST_FORWARDED, {
|
||||
domain,
|
||||
requestType,
|
||||
target: `${target.ip}:${target.port}`,
|
||||
@ -484,8 +495,15 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
|
||||
proxyReq.on('error', (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) {
|
||||
res.statusCode = 502;
|
||||
res.statusCode = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
res.end(`Proxy error: ${error.message}`);
|
||||
} else {
|
||||
res.end();
|
||||
@ -506,59 +524,48 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
* @param domain The domain to obtain a certificate for
|
||||
* @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> {
|
||||
if (this.isGlobPattern(domain)) {
|
||||
throw new CertificateError('Cannot obtain certificates for glob pattern domains', domain, isRenewal);
|
||||
}
|
||||
|
||||
const domainInfo = this.domainCertificates.get(domain)!;
|
||||
|
||||
if (!domainInfo.options.acmeMaintenance) {
|
||||
console.log(`Skipping certificate issuance for ${domain} - acmeMaintenance is disabled`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (domainInfo.obtainingInProgress) {
|
||||
console.log(`Certificate issuance already in progress for ${domain}`);
|
||||
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.lastRenewalAttempt = new Date();
|
||||
|
||||
try {
|
||||
// Request certificate via SmartAcme
|
||||
const certObj = await this.smartAcme.getCertificateForDomain(domain);
|
||||
const certificate = certObj.publicKey;
|
||||
const privateKey = certObj.privateKey;
|
||||
const expiryDate = new Date(certObj.validUntil);
|
||||
domainInfo.certificate = certificate;
|
||||
domainInfo.privateKey = privateKey;
|
||||
// Request certificate via ChallengeResponder
|
||||
const certData = await this.challengeResponder.requestCertificate(domain, isRenewal);
|
||||
|
||||
// Update domain info with certificate data
|
||||
domainInfo.certificate = certData.certificate;
|
||||
domainInfo.privateKey = certData.privateKey;
|
||||
domainInfo.certObtained = true;
|
||||
domainInfo.expiryDate = expiryDate;
|
||||
domainInfo.expiryDate = certData.expiryDate;
|
||||
|
||||
console.log(`Certificate ${isRenewal ? 'renewed' : 'obtained'} for ${domain}`);
|
||||
// Persistence moved to CertProvisioner
|
||||
const eventType = isRenewal
|
||||
? Port80HandlerEvents.CERTIFICATE_RENEWED
|
||||
: Port80HandlerEvents.CERTIFICATE_ISSUED;
|
||||
this.emitCertificateEvent(eventType, {
|
||||
domain,
|
||||
certificate,
|
||||
privateKey,
|
||||
expiryDate: expiryDate || this.getDefaultExpiryDate()
|
||||
});
|
||||
|
||||
// The event will be emitted by the ChallengeResponder, we just store the certificate
|
||||
} 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);
|
||||
this.emit(Port80HandlerEvents.CERTIFICATE_FAILED, {
|
||||
domain,
|
||||
error: errorMsg,
|
||||
isRenewal
|
||||
} as ICertificateFailure);
|
||||
|
||||
// The failure event will be emitted by the ChallengeResponder
|
||||
throw new CertificateError(errorMsg, domain, isRenewal);
|
||||
} finally {
|
||||
domainInfo.obtainingInProgress = false;
|
||||
@ -608,7 +615,7 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
* @param eventType The event type to emit
|
||||
* @param data The certificate data
|
||||
*/
|
||||
private emitCertificateEvent(eventType: Port80HandlerEvents, data: ICertificateData): void {
|
||||
private emitCertificateEvent(eventType: CertificateEvents, data: CertificateData): void {
|
||||
this.emit(eventType, data);
|
||||
}
|
||||
|
||||
@ -670,7 +677,7 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
*/
|
||||
public async renewCertificate(domain: string): Promise<void> {
|
||||
if (!this.domainCertificates.has(domain)) {
|
||||
throw new Port80HandlerError(`Domain not managed: ${domain}`);
|
||||
throw new HttpError(`Domain not managed: ${domain}`);
|
||||
}
|
||||
// Trigger renewal via ACME
|
||||
await this.obtainCertificate(domain, true);
|
||||
|
13
ts/index.ts
13
ts/index.ts
@ -5,7 +5,17 @@
|
||||
// Legacy exports (to maintain backward compatibility)
|
||||
export * from './nfttablesproxy/classes.nftablesproxy.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 './smartproxy/classes.smartproxy.js';
|
||||
// Original: export * from './smartproxy/classes.pp.snihandler.js'
|
||||
@ -20,3 +30,4 @@ export * from './core/models/common-types.js';
|
||||
export * as forwarding from './forwarding/index.js';
|
||||
export * as certificate from './certificate/index.js';
|
||||
export * as tls from './tls/index.js';
|
||||
export * as http from './http/index.js';
|
@ -1,5 +1,6 @@
|
||||
// node native scope
|
||||
import { EventEmitter } from 'events';
|
||||
import * as fs from 'fs';
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
import * as net from 'net';
|
||||
@ -7,7 +8,7 @@ import * as tls from 'tls';
|
||||
import * as url from 'url';
|
||||
import * as http2 from 'http2';
|
||||
|
||||
export { EventEmitter, http, https, net, tls, url, http2 };
|
||||
export { EventEmitter, fs, http, https, net, tls, url, http2 };
|
||||
|
||||
// tsclass scope
|
||||
import * as tsclass from '@tsclass/tsclass';
|
||||
|
24
ts/port80handler/classes.port80handler.ts
Normal file
24
ts/port80handler/classes.port80handler.ts
Normal 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;
|
Loading…
x
Reference in New Issue
Block a user