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
|
# 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
|
||||||
|
|
||||||
|
@ -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 | ❌ |
|
||||||
|
@ -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.'
|
||||||
}
|
}
|
||||||
|
@ -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',
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -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 {
|
||||||
|
@ -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';
|
||||||
|
@ -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);
|
||||||
|
15
ts/index.ts
15
ts/index.ts
@ -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';
|
@ -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';
|
||||||
|
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