Compare commits
14 Commits
Author | SHA1 | Date | |
---|---|---|---|
54e81b3c32 | |||
b7b47cd11f | |||
62061517fd | |||
531350a1c1 | |||
559a52af41 | |||
f8c86c76ae | |||
cc04e8786c | |||
9cb6e397b9 | |||
11b65bf684 | |||
4b30e377b9 | |||
b10f35be4b | |||
426249e70e | |||
ba0d9d0b8e | |||
151b8f498c |
45
changelog.md
45
changelog.md
@ -1,5 +1,50 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-03-18 - 4.3.0 - feat(Port80Handler)
|
||||
Add glob pattern support for domain certificate management in Port80Handler. Wildcard domains are now detected and skipped in certificate issuance and retrieval, ensuring that only explicit domains receive ACME certificates and improving route matching.
|
||||
|
||||
- Introduced isGlobPattern to detect wildcard domains.
|
||||
- Added getDomainInfoForRequest and domainMatchesPattern methods to enable glob pattern matching for domain configurations.
|
||||
- Modified setCertificate and getCertificate to prevent certificate operations for glob patterns.
|
||||
- Updated request handling to skip ACME challenge processing and certificate issuance for wildcard domains.
|
||||
- Updated documentation and tests to reflect the new glob pattern support.
|
||||
|
||||
## 2025-03-18 - 4.2.6 - fix(Port80Handler)
|
||||
Restrict ACME HTTP-01 challenge handling to domains with acmeMaintenance or acmeForward enabled
|
||||
|
||||
- Updated challenge handler in ts/classes.port80handler.ts to include a check for (options.acmeMaintenance || options.acmeForward)
|
||||
- Prevents unintended processing of ACME challenges when ACME configuration is not enabled
|
||||
|
||||
## 2025-03-18 - 4.2.5 - fix(networkproxy)
|
||||
Refactor certificate management components: rename AcmeCertManager to Port80Handler and update related event names from CertManagerEvents to Port80HandlerEvents. The changes update internal API usage in ts/classes.networkproxy.ts and ts/classes.port80handler.ts to unify and simplify ACME certificate handling and HTTP-01 challenge management.
|
||||
|
||||
- Renamed AcmeCertManager to Port80Handler in ts/classes.networkproxy.ts
|
||||
- Updated event names from CertManagerEvents to Port80HandlerEvents
|
||||
- Modified API calls for certificate issuance and renewal in ts/classes.port80handler.ts
|
||||
- Refactored domain registration and certificate extraction logic
|
||||
|
||||
## 2025-03-18 - 4.2.4 - fix(ts/index.ts)
|
||||
Fix export order in ts/index.ts by moving the port proxy export back and adding interfaces export for proper module exposure
|
||||
|
||||
- Reorder exports to place './classes.pp.portproxy.js' in the correct position
|
||||
- Add export for './classes.pp.interfaces.js' to expose internal interfaces
|
||||
|
||||
## 2025-03-18 - 4.2.3 - fix(connectionhandler)
|
||||
Remove unnecessary delay in TLS session ticket handling for connections without SNI
|
||||
|
||||
- Eliminated the extra setTimeout waiting period before cleaning up connections flagged as session_ticket_blocked_no_sni
|
||||
- Ensures immediate cleanup and improves connection responsiveness during TLS handshake failures
|
||||
|
||||
## 2025-03-18 - 4.2.2 - fix(connectionhandler)
|
||||
Ensure proper termination of TLS connections without SNI by explicitly ending the socket after sending the unrecognized_name alert. This prevents the connection from hanging and avoids potential duplicate handling.
|
||||
|
||||
- Added socket.end() after uncorking the alert packet in ClientHello handling to force connection closure.
|
||||
- Prevents duplicate data events and ensures the warning alert is processed by clients like Chrome.
|
||||
|
||||
## 2025-03-17 - 4.2.1 - fix(core)
|
||||
No uncommitted changes detected in the project.
|
||||
|
||||
|
||||
## 2025-03-17 - 4.2.0 - feat(tlsalert)
|
||||
add sendForceSniSequence and sendFatalAndClose helper functions to TlsAlert for improved SNI enforcement
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@push.rocks/smartproxy",
|
||||
"version": "4.2.0",
|
||||
"version": "4.3.0",
|
||||
"private": false,
|
||||
"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.",
|
||||
"main": "dist_ts/index.js",
|
||||
|
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartproxy',
|
||||
version: '4.2.0',
|
||||
version: '4.3.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.'
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import { ProxyRouter } from './classes.router.js';
|
||||
import { AcmeCertManager, CertManagerEvents } from './classes.port80handler.js';
|
||||
import { Port80Handler, Port80HandlerEvents, type IDomainOptions } from './classes.port80handler.js';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
@ -72,8 +72,8 @@ export class NetworkProxy {
|
||||
private defaultCertificates: { key: string; cert: string };
|
||||
private certificateCache: Map<string, { key: string; cert: string; expires?: Date }> = new Map();
|
||||
|
||||
// ACME certificate manager
|
||||
private certManager: AcmeCertManager | null = null;
|
||||
// Port80Handler for certificate management
|
||||
private port80Handler: Port80Handler | null = null;
|
||||
private certificateStoreDir: string;
|
||||
|
||||
// New connection pool for backend connections
|
||||
@ -375,16 +375,16 @@ export class NetworkProxy {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the ACME certificate manager for automatic certificate issuance
|
||||
* Initializes the Port80Handler for ACME certificate management
|
||||
* @private
|
||||
*/
|
||||
private async initializeAcmeManager(): Promise<void> {
|
||||
private async initializePort80Handler(): Promise<void> {
|
||||
if (!this.options.acme.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create certificate manager
|
||||
this.certManager = new AcmeCertManager({
|
||||
this.port80Handler = new Port80Handler({
|
||||
port: this.options.acme.port,
|
||||
contactEmail: this.options.acme.contactEmail,
|
||||
useProduction: this.options.acme.useProduction,
|
||||
@ -394,32 +394,32 @@ export class NetworkProxy {
|
||||
});
|
||||
|
||||
// Register event handlers
|
||||
this.certManager.on(CertManagerEvents.CERTIFICATE_ISSUED, this.handleCertificateIssued.bind(this));
|
||||
this.certManager.on(CertManagerEvents.CERTIFICATE_RENEWED, this.handleCertificateIssued.bind(this));
|
||||
this.certManager.on(CertManagerEvents.CERTIFICATE_FAILED, this.handleCertificateFailed.bind(this));
|
||||
this.certManager.on(CertManagerEvents.CERTIFICATE_EXPIRING, (data) => {
|
||||
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_ISSUED, this.handleCertificateIssued.bind(this));
|
||||
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_RENEWED, this.handleCertificateIssued.bind(this));
|
||||
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_FAILED, this.handleCertificateFailed.bind(this));
|
||||
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_EXPIRING, (data) => {
|
||||
this.log('info', `Certificate for ${data.domain} expires in ${data.daysRemaining} days`);
|
||||
});
|
||||
|
||||
// Start the manager
|
||||
// Start the handler
|
||||
try {
|
||||
await this.certManager.start();
|
||||
this.log('info', `ACME Certificate Manager started on port ${this.options.acme.port}`);
|
||||
await this.port80Handler.start();
|
||||
this.log('info', `Port80Handler started on port ${this.options.acme.port}`);
|
||||
|
||||
// Add domains from proxy configs
|
||||
this.registerDomainsWithAcmeManager();
|
||||
this.registerDomainsWithPort80Handler();
|
||||
} catch (error) {
|
||||
this.log('error', `Failed to start ACME Certificate Manager: ${error}`);
|
||||
this.certManager = null;
|
||||
this.log('error', `Failed to start Port80Handler: ${error}`);
|
||||
this.port80Handler = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers domains from proxy configs with the ACME manager
|
||||
* Registers domains from proxy configs with the Port80Handler
|
||||
* @private
|
||||
*/
|
||||
private registerDomainsWithAcmeManager(): void {
|
||||
if (!this.certManager) return;
|
||||
private registerDomainsWithPort80Handler(): void {
|
||||
if (!this.port80Handler) return;
|
||||
|
||||
// Get all hostnames from proxy configs
|
||||
this.proxyConfigs.forEach(config => {
|
||||
@ -461,26 +461,32 @@ export class NetworkProxy {
|
||||
this.log('warn', `Failed to extract expiry date from certificate for ${hostname}`);
|
||||
}
|
||||
|
||||
// Update the certificate in the manager
|
||||
this.certManager.setCertificate(hostname, cert, key, expiryDate);
|
||||
// Update the certificate in the handler
|
||||
this.port80Handler.setCertificate(hostname, cert, key, expiryDate);
|
||||
|
||||
// Also update our own certificate cache
|
||||
this.updateCertificateCache(hostname, cert, key, expiryDate);
|
||||
|
||||
this.log('info', `Loaded existing certificate for ${hostname}`);
|
||||
} else {
|
||||
// Register the domain for certificate issuance
|
||||
this.certManager.addDomain(hostname);
|
||||
// Register the domain for certificate issuance with new domain options format
|
||||
const domainOptions: IDomainOptions = {
|
||||
domainName: hostname,
|
||||
sslRedirect: true,
|
||||
acmeMaintenance: true
|
||||
};
|
||||
|
||||
this.port80Handler.addDomain(domainOptions);
|
||||
this.log('info', `Registered domain for ACME certificate issuance: ${hostname}`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.log('error', `Error registering domain ${hostname} with ACME manager: ${error}`);
|
||||
this.log('error', `Error registering domain ${hostname} with Port80Handler: ${error}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles newly issued or renewed certificates from ACME manager
|
||||
* Handles newly issued or renewed certificates from Port80Handler
|
||||
* @private
|
||||
*/
|
||||
private handleCertificateIssued(data: { domain: string; certificate: string; privateKey: string; expiryDate: Date }): void {
|
||||
@ -556,13 +562,21 @@ export class NetworkProxy {
|
||||
}
|
||||
|
||||
// Check if we should trigger certificate issuance
|
||||
if (this.options.acme?.enabled && this.certManager && !domain.includes('*')) {
|
||||
if (this.options.acme?.enabled && this.port80Handler && !domain.includes('*')) {
|
||||
// Check if this domain is already registered
|
||||
const certData = this.certManager.getCertificate(domain);
|
||||
const certData = this.port80Handler.getCertificate(domain);
|
||||
|
||||
if (!certData) {
|
||||
this.log('info', `No certificate found for ${domain}, registering for issuance`);
|
||||
this.certManager.addDomain(domain);
|
||||
|
||||
// Register with new domain options format
|
||||
const domainOptions: IDomainOptions = {
|
||||
domainName: domain,
|
||||
sslRedirect: true,
|
||||
acmeMaintenance: true
|
||||
};
|
||||
|
||||
this.port80Handler.addDomain(domainOptions);
|
||||
}
|
||||
}
|
||||
|
||||
@ -587,9 +601,9 @@ export class NetworkProxy {
|
||||
public async start(): Promise<void> {
|
||||
this.startTime = Date.now();
|
||||
|
||||
// Initialize ACME certificate manager if enabled
|
||||
// Initialize Port80Handler if enabled
|
||||
if (this.options.acme.enabled) {
|
||||
await this.initializeAcmeManager();
|
||||
await this.initializePort80Handler();
|
||||
}
|
||||
|
||||
// Create the HTTPS server
|
||||
@ -1588,13 +1602,13 @@ export class NetworkProxy {
|
||||
}
|
||||
this.connectionPool.clear();
|
||||
|
||||
// Stop ACME certificate manager if it's running
|
||||
if (this.certManager) {
|
||||
// Stop Port80Handler if it's running
|
||||
if (this.port80Handler) {
|
||||
try {
|
||||
await this.certManager.stop();
|
||||
this.log('info', 'ACME Certificate Manager stopped');
|
||||
await this.port80Handler.stop();
|
||||
this.log('info', 'Port80Handler stopped');
|
||||
} catch (error) {
|
||||
this.log('error', 'Error stopping ACME Certificate Manager', error);
|
||||
this.log('error', 'Error stopping Port80Handler', error);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1619,8 +1633,8 @@ export class NetworkProxy {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.certManager) {
|
||||
this.log('error', 'ACME certificate manager is not initialized');
|
||||
if (!this.port80Handler) {
|
||||
this.log('error', 'Port80Handler is not initialized');
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -1631,7 +1645,14 @@ export class NetworkProxy {
|
||||
}
|
||||
|
||||
try {
|
||||
this.certManager.addDomain(domain);
|
||||
// Use the new domain options format
|
||||
const domainOptions: IDomainOptions = {
|
||||
domainName: domain,
|
||||
sslRedirect: true,
|
||||
acmeMaintenance: true
|
||||
};
|
||||
|
||||
this.port80Handler.addDomain(domainOptions);
|
||||
this.log('info', `Certificate request submitted for domain: ${domain}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
|
@ -1,9 +1,58 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import { IncomingMessage, ServerResponse } from 'http';
|
||||
|
||||
/**
|
||||
* Represents a domain certificate with various status information
|
||||
* Custom error classes for better error handling
|
||||
*/
|
||||
export class Port80HandlerError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'Port80HandlerError';
|
||||
}
|
||||
}
|
||||
|
||||
export class CertificateError extends Port80HandlerError {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly domain: string,
|
||||
public readonly isRenewal: boolean = false
|
||||
) {
|
||||
super(`${message} for domain ${domain}${isRenewal ? ' (renewal)' : ''}`);
|
||||
this.name = 'CertificateError';
|
||||
}
|
||||
}
|
||||
|
||||
export class ServerError extends Port80HandlerError {
|
||||
constructor(message: string, public readonly code?: string) {
|
||||
super(message);
|
||||
this.name = 'ServerError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Domain forwarding configuration
|
||||
*/
|
||||
export interface IForwardConfig {
|
||||
ip: string;
|
||||
port: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Domain configuration options
|
||||
*/
|
||||
export interface IDomainOptions {
|
||||
domainName: string;
|
||||
sslRedirect: boolean; // if true redirects the request to port 443
|
||||
acmeMaintenance: boolean; // tries to always have a valid cert for this domain
|
||||
forward?: IForwardConfig; // forwards all http requests to that target
|
||||
acmeForward?: IForwardConfig; // forwards letsencrypt requests to this config
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a domain configuration with certificate status information
|
||||
*/
|
||||
interface IDomainCertificate {
|
||||
options: IDomainOptions;
|
||||
certObtained: boolean;
|
||||
obtainingInProgress: boolean;
|
||||
certificate?: string;
|
||||
@ -15,9 +64,9 @@ interface IDomainCertificate {
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for the ACME Certificate Manager
|
||||
* Configuration options for the Port80Handler
|
||||
*/
|
||||
interface IAcmeCertManagerOptions {
|
||||
interface IPort80HandlerOptions {
|
||||
port?: number;
|
||||
contactEmail?: string;
|
||||
useProduction?: boolean;
|
||||
@ -29,7 +78,7 @@ interface IAcmeCertManagerOptions {
|
||||
/**
|
||||
* Certificate data that can be emitted via events or set from outside
|
||||
*/
|
||||
interface ICertificateData {
|
||||
export interface ICertificateData {
|
||||
domain: string;
|
||||
certificate: string;
|
||||
privateKey: string;
|
||||
@ -37,34 +86,54 @@ interface ICertificateData {
|
||||
}
|
||||
|
||||
/**
|
||||
* Events emitted by the ACME Certificate Manager
|
||||
* Events emitted by the Port80Handler
|
||||
*/
|
||||
export enum CertManagerEvents {
|
||||
export enum Port80HandlerEvents {
|
||||
CERTIFICATE_ISSUED = 'certificate-issued',
|
||||
CERTIFICATE_RENEWED = 'certificate-renewed',
|
||||
CERTIFICATE_FAILED = 'certificate-failed',
|
||||
CERTIFICATE_EXPIRING = 'certificate-expiring',
|
||||
MANAGER_STARTED = 'manager-started',
|
||||
MANAGER_STOPPED = 'manager-stopped',
|
||||
REQUEST_FORWARDED = 'request-forwarded',
|
||||
}
|
||||
|
||||
/**
|
||||
* Improved ACME Certificate Manager with event emission and external certificate management
|
||||
* Certificate failure payload type
|
||||
*/
|
||||
export class AcmeCertManager extends plugins.EventEmitter {
|
||||
export interface ICertificateFailure {
|
||||
domain: string;
|
||||
error: string;
|
||||
isRenewal: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Certificate expiry payload type
|
||||
*/
|
||||
export interface ICertificateExpiring {
|
||||
domain: string;
|
||||
expiryDate: Date;
|
||||
daysRemaining: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Port80Handler with ACME certificate management and request forwarding capabilities
|
||||
* Now with glob pattern support for domain matching
|
||||
*/
|
||||
export class Port80Handler extends plugins.EventEmitter {
|
||||
private domainCertificates: Map<string, IDomainCertificate>;
|
||||
private server: plugins.http.Server | null = null;
|
||||
private acmeClient: plugins.acme.Client | null = null;
|
||||
private accountKey: string | null = null;
|
||||
private renewalTimer: NodeJS.Timeout | null = null;
|
||||
private isShuttingDown: boolean = false;
|
||||
private options: Required<IAcmeCertManagerOptions>;
|
||||
private options: Required<IPort80HandlerOptions>;
|
||||
|
||||
/**
|
||||
* Creates a new ACME Certificate Manager
|
||||
* Creates a new Port80Handler
|
||||
* @param options Configuration options
|
||||
*/
|
||||
constructor(options: IAcmeCertManagerOptions = {}) {
|
||||
constructor(options: IPort80HandlerOptions = {}) {
|
||||
super();
|
||||
this.domainCertificates = new Map<string, IDomainCertificate>();
|
||||
|
||||
@ -73,7 +142,7 @@ export class AcmeCertManager extends plugins.EventEmitter {
|
||||
port: options.port ?? 80,
|
||||
contactEmail: options.contactEmail ?? 'admin@example.com',
|
||||
useProduction: options.useProduction ?? false, // Safer default: staging
|
||||
renewThresholdDays: options.renewThresholdDays ?? 30,
|
||||
renewThresholdDays: options.renewThresholdDays ?? 10, // Changed to 10 days as per requirements
|
||||
httpsRedirectPort: options.httpsRedirectPort ?? 443,
|
||||
renewCheckIntervalHours: options.renewCheckIntervalHours ?? 24,
|
||||
};
|
||||
@ -84,11 +153,11 @@ export class AcmeCertManager extends plugins.EventEmitter {
|
||||
*/
|
||||
public async start(): Promise<void> {
|
||||
if (this.server) {
|
||||
throw new Error('Server is already running');
|
||||
throw new ServerError('Server is already running');
|
||||
}
|
||||
|
||||
if (this.isShuttingDown) {
|
||||
throw new Error('Server is shutting down');
|
||||
throw new ServerError('Server is shutting down');
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
@ -97,22 +166,39 @@ export class AcmeCertManager extends plugins.EventEmitter {
|
||||
|
||||
this.server.on('error', (error: NodeJS.ErrnoException) => {
|
||||
if (error.code === 'EACCES') {
|
||||
reject(new Error(`Permission denied to bind to port ${this.options.port}. Try running with elevated privileges or use a port > 1024.`));
|
||||
reject(new ServerError(`Permission denied to bind to port ${this.options.port}. Try running with elevated privileges or use a port > 1024.`, error.code));
|
||||
} else if (error.code === 'EADDRINUSE') {
|
||||
reject(new Error(`Port ${this.options.port} is already in use.`));
|
||||
reject(new ServerError(`Port ${this.options.port} is already in use.`, error.code));
|
||||
} else {
|
||||
reject(error);
|
||||
reject(new ServerError(error.message, error.code));
|
||||
}
|
||||
});
|
||||
|
||||
this.server.listen(this.options.port, () => {
|
||||
console.log(`AcmeCertManager is listening on port ${this.options.port}`);
|
||||
console.log(`Port80Handler is listening on port ${this.options.port}`);
|
||||
this.startRenewalTimer();
|
||||
this.emit(CertManagerEvents.MANAGER_STARTED, this.options.port);
|
||||
this.emit(Port80HandlerEvents.MANAGER_STARTED, this.options.port);
|
||||
|
||||
// Start certificate process for domains with acmeMaintenance enabled
|
||||
for (const [domain, domainInfo] of this.domainCertificates.entries()) {
|
||||
// Skip glob patterns for certificate issuance
|
||||
if (this.isGlobPattern(domain)) {
|
||||
console.log(`Skipping initial certificate for glob pattern: ${domain}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (domainInfo.options.acmeMaintenance && !domainInfo.certObtained && !domainInfo.obtainingInProgress) {
|
||||
this.obtainCertificate(domain).catch(err => {
|
||||
console.error(`Error obtaining initial certificate for ${domain}:`, err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
const message = error instanceof Error ? error.message : 'Unknown error starting server';
|
||||
reject(new ServerError(message));
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -138,7 +224,7 @@ export class AcmeCertManager extends plugins.EventEmitter {
|
||||
this.server.close(() => {
|
||||
this.server = null;
|
||||
this.isShuttingDown = false;
|
||||
this.emit(CertManagerEvents.MANAGER_STOPPED);
|
||||
this.emit(Port80HandlerEvents.MANAGER_STOPPED);
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
@ -149,13 +235,41 @@ export class AcmeCertManager extends plugins.EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a domain to be managed for certificates
|
||||
* @param domain The domain to add
|
||||
* Adds a domain with configuration options
|
||||
* @param options Domain configuration options
|
||||
*/
|
||||
public addDomain(domain: string): void {
|
||||
if (!this.domainCertificates.has(domain)) {
|
||||
this.domainCertificates.set(domain, { certObtained: false, obtainingInProgress: false });
|
||||
console.log(`Domain added: ${domain}`);
|
||||
public addDomain(options: IDomainOptions): void {
|
||||
if (!options.domainName || typeof options.domainName !== 'string') {
|
||||
throw new Port80HandlerError('Invalid domain name');
|
||||
}
|
||||
|
||||
const domainName = options.domainName;
|
||||
|
||||
if (!this.domainCertificates.has(domainName)) {
|
||||
this.domainCertificates.set(domainName, {
|
||||
options,
|
||||
certObtained: false,
|
||||
obtainingInProgress: false
|
||||
});
|
||||
|
||||
console.log(`Domain added: ${domainName} with configuration:`, {
|
||||
sslRedirect: options.sslRedirect,
|
||||
acmeMaintenance: options.acmeMaintenance,
|
||||
hasForward: !!options.forward,
|
||||
hasAcmeForward: !!options.acmeForward
|
||||
});
|
||||
|
||||
// If acmeMaintenance is enabled and not a glob pattern, start certificate process immediately
|
||||
if (options.acmeMaintenance && this.server && !this.isGlobPattern(domainName)) {
|
||||
this.obtainCertificate(domainName).catch(err => {
|
||||
console.error(`Error obtaining initial certificate for ${domainName}:`, err);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Update existing domain with new options
|
||||
const existing = this.domainCertificates.get(domainName)!;
|
||||
existing.options = options;
|
||||
console.log(`Domain ${domainName} configuration updated`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -177,10 +291,30 @@ export class AcmeCertManager extends plugins.EventEmitter {
|
||||
* @param expiryDate Optional expiry date
|
||||
*/
|
||||
public setCertificate(domain: string, certificate: string, privateKey: string, expiryDate?: Date): void {
|
||||
if (!domain || !certificate || !privateKey) {
|
||||
throw new Port80HandlerError('Domain, certificate and privateKey are required');
|
||||
}
|
||||
|
||||
// Don't allow setting certificates for glob patterns
|
||||
if (this.isGlobPattern(domain)) {
|
||||
throw new Port80HandlerError('Cannot set certificate for glob pattern domains');
|
||||
}
|
||||
|
||||
let domainInfo = this.domainCertificates.get(domain);
|
||||
|
||||
if (!domainInfo) {
|
||||
domainInfo = { certObtained: false, obtainingInProgress: false };
|
||||
// Create default domain options if not already configured
|
||||
const defaultOptions: IDomainOptions = {
|
||||
domainName: domain,
|
||||
sslRedirect: true,
|
||||
acmeMaintenance: true
|
||||
};
|
||||
|
||||
domainInfo = {
|
||||
options: defaultOptions,
|
||||
certObtained: false,
|
||||
obtainingInProgress: false
|
||||
};
|
||||
this.domainCertificates.set(domain, domainInfo);
|
||||
}
|
||||
|
||||
@ -192,27 +326,18 @@ export class AcmeCertManager extends plugins.EventEmitter {
|
||||
if (expiryDate) {
|
||||
domainInfo.expiryDate = expiryDate;
|
||||
} else {
|
||||
// Try to extract expiry date from certificate
|
||||
try {
|
||||
// This is a simplistic approach - in a real implementation, use a proper
|
||||
// certificate parsing library like node-forge or x509
|
||||
const matches = certificate.match(/Not After\s*:\s*(.*?)(?:\n|$)/i);
|
||||
if (matches && matches[1]) {
|
||||
domainInfo.expiryDate = new Date(matches[1]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to extract expiry date from certificate for ${domain}`);
|
||||
}
|
||||
// Extract expiry date from certificate
|
||||
domainInfo.expiryDate = this.extractExpiryDateFromCertificate(certificate, domain);
|
||||
}
|
||||
|
||||
console.log(`Certificate set for ${domain}`);
|
||||
|
||||
// Emit certificate event
|
||||
this.emitCertificateEvent(CertManagerEvents.CERTIFICATE_ISSUED, {
|
||||
this.emitCertificateEvent(Port80HandlerEvents.CERTIFICATE_ISSUED, {
|
||||
domain,
|
||||
certificate,
|
||||
privateKey,
|
||||
expiryDate: domainInfo.expiryDate || new Date(Date.now() + 90 * 24 * 60 * 60 * 1000) // 90 days default
|
||||
expiryDate: domainInfo.expiryDate || this.getDefaultExpiryDate()
|
||||
});
|
||||
}
|
||||
|
||||
@ -221,6 +346,11 @@ export class AcmeCertManager extends plugins.EventEmitter {
|
||||
* @param domain The domain to get the certificate for
|
||||
*/
|
||||
public getCertificate(domain: string): ICertificateData | null {
|
||||
// Can't get certificates for glob patterns
|
||||
if (this.isGlobPattern(domain)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const domainInfo = this.domainCertificates.get(domain);
|
||||
|
||||
if (!domainInfo || !domainInfo.certObtained || !domainInfo.certificate || !domainInfo.privateKey) {
|
||||
@ -231,10 +361,69 @@ export class AcmeCertManager extends plugins.EventEmitter {
|
||||
domain,
|
||||
certificate: domainInfo.certificate,
|
||||
privateKey: domainInfo.privateKey,
|
||||
expiryDate: domainInfo.expiryDate || new Date(Date.now() + 90 * 24 * 60 * 60 * 1000) // 90 days default
|
||||
expiryDate: domainInfo.expiryDate || this.getDefaultExpiryDate()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a domain is a glob pattern
|
||||
* @param domain Domain to check
|
||||
* @returns True if the domain is a glob pattern
|
||||
*/
|
||||
private isGlobPattern(domain: string): boolean {
|
||||
return domain.includes('*');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get domain info for a specific domain, using glob pattern matching if needed
|
||||
* @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 {
|
||||
// Try direct match first
|
||||
if (this.domainCertificates.has(requestDomain)) {
|
||||
return {
|
||||
domainInfo: this.domainCertificates.get(requestDomain)!,
|
||||
pattern: requestDomain
|
||||
};
|
||||
}
|
||||
|
||||
// Then try glob patterns
|
||||
for (const [pattern, domainInfo] of this.domainCertificates.entries()) {
|
||||
if (this.isGlobPattern(pattern) && this.domainMatchesPattern(requestDomain, pattern)) {
|
||||
return { domainInfo, pattern };
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a domain matches a glob pattern
|
||||
* @param domain The domain to check
|
||||
* @param pattern The pattern to match against
|
||||
* @returns True if the domain matches the pattern
|
||||
*/
|
||||
private domainMatchesPattern(domain: string, pattern: string): boolean {
|
||||
// Handle different glob pattern styles
|
||||
if (pattern.startsWith('*.')) {
|
||||
// *.example.com matches any subdomain
|
||||
const suffix = pattern.substring(2);
|
||||
return domain.endsWith(suffix) && domain.includes('.') && domain !== suffix;
|
||||
} else if (pattern.endsWith('.*')) {
|
||||
// example.* matches any TLD
|
||||
const prefix = pattern.substring(0, pattern.length - 2);
|
||||
const domainParts = domain.split('.');
|
||||
return domain.startsWith(prefix + '.') && domainParts.length >= 2;
|
||||
} else if (pattern === '*') {
|
||||
// Wildcard matches everything
|
||||
return true;
|
||||
} else {
|
||||
// Exact match (shouldn't reach here as we check exact matches first)
|
||||
return domain === pattern;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazy initialization of the ACME client
|
||||
* @returns An ACME client instance
|
||||
@ -244,23 +433,28 @@ export class AcmeCertManager extends plugins.EventEmitter {
|
||||
return this.acmeClient;
|
||||
}
|
||||
|
||||
// Generate a new account key
|
||||
this.accountKey = (await plugins.acme.forge.createPrivateKey()).toString();
|
||||
|
||||
this.acmeClient = new plugins.acme.Client({
|
||||
directoryUrl: this.options.useProduction
|
||||
? plugins.acme.directory.letsencrypt.production
|
||||
: plugins.acme.directory.letsencrypt.staging,
|
||||
accountKey: this.accountKey,
|
||||
});
|
||||
|
||||
// Create a new account
|
||||
await this.acmeClient.createAccount({
|
||||
termsOfServiceAgreed: true,
|
||||
contact: [`mailto:${this.options.contactEmail}`],
|
||||
});
|
||||
|
||||
return this.acmeClient;
|
||||
try {
|
||||
// Generate a new account key
|
||||
this.accountKey = (await plugins.acme.forge.createPrivateKey()).toString();
|
||||
|
||||
this.acmeClient = new plugins.acme.Client({
|
||||
directoryUrl: this.options.useProduction
|
||||
? plugins.acme.directory.letsencrypt.production
|
||||
: plugins.acme.directory.letsencrypt.staging,
|
||||
accountKey: this.accountKey,
|
||||
});
|
||||
|
||||
// Create a new account
|
||||
await this.acmeClient.createAccount({
|
||||
termsOfServiceAgreed: true,
|
||||
contact: [`mailto:${this.options.contactEmail}`],
|
||||
});
|
||||
|
||||
return this.acmeClient;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error initializing ACME client';
|
||||
throw new Port80HandlerError(`Failed to initialize ACME client: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -279,22 +473,42 @@ export class AcmeCertManager extends plugins.EventEmitter {
|
||||
// Extract domain (ignoring any port in the Host header)
|
||||
const domain = hostHeader.split(':')[0];
|
||||
|
||||
// If the request is for an ACME HTTP-01 challenge, handle it
|
||||
if (req.url && req.url.startsWith('/.well-known/acme-challenge/')) {
|
||||
this.handleAcmeChallenge(req, res, domain);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.domainCertificates.has(domain)) {
|
||||
// Get domain config, using glob pattern matching if needed
|
||||
const domainMatch = this.getDomainInfoForRequest(domain);
|
||||
|
||||
if (!domainMatch) {
|
||||
res.statusCode = 404;
|
||||
res.end('Domain not configured');
|
||||
return;
|
||||
}
|
||||
|
||||
const domainInfo = this.domainCertificates.get(domain)!;
|
||||
const { domainInfo, pattern } = domainMatch;
|
||||
const options = domainInfo.options;
|
||||
|
||||
// If certificate exists, redirect to HTTPS
|
||||
if (domainInfo.certObtained) {
|
||||
// If the request is for an ACME HTTP-01 challenge, handle it
|
||||
if (req.url && req.url.startsWith('/.well-known/acme-challenge/') && (options.acmeMaintenance || options.acmeForward)) {
|
||||
// Check if we should forward ACME requests
|
||||
if (options.acmeForward) {
|
||||
this.forwardRequest(req, res, options.acmeForward, 'ACME challenge');
|
||||
return;
|
||||
}
|
||||
|
||||
// Only handle ACME challenges for non-glob patterns
|
||||
if (!this.isGlobPattern(pattern)) {
|
||||
this.handleAcmeChallenge(req, res, domain);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we should forward non-ACME requests
|
||||
if (options.forward) {
|
||||
this.forwardRequest(req, res, options.forward, 'HTTP');
|
||||
return;
|
||||
}
|
||||
|
||||
// If certificate exists and sslRedirect is enabled, redirect to HTTPS
|
||||
// (Skip for glob patterns as they won't have certificates)
|
||||
if (!this.isGlobPattern(pattern) && domainInfo.certObtained && options.sslRedirect) {
|
||||
const httpsPort = this.options.httpsRedirectPort;
|
||||
const portSuffix = httpsPort === 443 ? '' : `:${httpsPort}`;
|
||||
const redirectUrl = `https://${domain}${portSuffix}${req.url || '/'}`;
|
||||
@ -302,17 +516,94 @@ export class AcmeCertManager extends plugins.EventEmitter {
|
||||
res.statusCode = 301;
|
||||
res.setHeader('Location', redirectUrl);
|
||||
res.end(`Redirecting to ${redirectUrl}`);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle case where certificate maintenance is enabled but not yet obtained
|
||||
// (Skip for glob patterns as they can't have certificates)
|
||||
if (!this.isGlobPattern(pattern) && options.acmeMaintenance && !domainInfo.certObtained) {
|
||||
// Trigger certificate issuance if not already running
|
||||
if (!domainInfo.obtainingInProgress) {
|
||||
this.obtainCertificate(domain).catch(err => {
|
||||
this.emit(CertManagerEvents.CERTIFICATE_FAILED, { domain, error: err.message });
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
||||
this.emit(Port80HandlerEvents.CERTIFICATE_FAILED, {
|
||||
domain,
|
||||
error: errorMessage,
|
||||
isRenewal: false
|
||||
});
|
||||
console.error(`Error obtaining certificate for ${domain}:`, err);
|
||||
});
|
||||
}
|
||||
|
||||
res.statusCode = 503;
|
||||
res.end('Certificate issuance in progress, please try again later.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Default response for unhandled request
|
||||
res.statusCode = 404;
|
||||
res.end('No handlers configured for this request');
|
||||
}
|
||||
|
||||
/**
|
||||
* Forwards an HTTP request to the specified target
|
||||
* @param req The original request
|
||||
* @param res The response object
|
||||
* @param target The forwarding target (IP and port)
|
||||
* @param requestType Type of request for logging
|
||||
*/
|
||||
private forwardRequest(
|
||||
req: plugins.http.IncomingMessage,
|
||||
res: plugins.http.ServerResponse,
|
||||
target: IForwardConfig,
|
||||
requestType: string
|
||||
): void {
|
||||
const options = {
|
||||
hostname: target.ip,
|
||||
port: target.port,
|
||||
path: req.url,
|
||||
method: req.method,
|
||||
headers: { ...req.headers }
|
||||
};
|
||||
|
||||
const domain = req.headers.host?.split(':')[0] || 'unknown';
|
||||
console.log(`Forwarding ${requestType} request for ${domain} to ${target.ip}:${target.port}`);
|
||||
|
||||
const proxyReq = plugins.http.request(options, (proxyRes) => {
|
||||
// Copy status code
|
||||
res.statusCode = proxyRes.statusCode || 500;
|
||||
|
||||
// Copy headers
|
||||
for (const [key, value] of Object.entries(proxyRes.headers)) {
|
||||
if (value) res.setHeader(key, value);
|
||||
}
|
||||
|
||||
// Pipe response data
|
||||
proxyRes.pipe(res);
|
||||
|
||||
this.emit(Port80HandlerEvents.REQUEST_FORWARDED, {
|
||||
domain,
|
||||
requestType,
|
||||
target: `${target.ip}:${target.port}`,
|
||||
statusCode: proxyRes.statusCode
|
||||
});
|
||||
});
|
||||
|
||||
proxyReq.on('error', (error) => {
|
||||
console.error(`Error forwarding request to ${target.ip}:${target.port}:`, error);
|
||||
if (!res.headersSent) {
|
||||
res.statusCode = 502;
|
||||
res.end(`Proxy error: ${error.message}`);
|
||||
} else {
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
|
||||
// Pipe original request to proxy request
|
||||
if (req.readable) {
|
||||
req.pipe(proxyReq);
|
||||
} else {
|
||||
proxyReq.end();
|
||||
}
|
||||
}
|
||||
|
||||
@ -351,10 +642,21 @@ export class AcmeCertManager extends plugins.EventEmitter {
|
||||
* @param isRenewal Whether this is a renewal attempt
|
||||
*/
|
||||
private async obtainCertificate(domain: string, isRenewal: boolean = false): Promise<void> {
|
||||
// Don't allow certificate issuance for glob patterns
|
||||
if (this.isGlobPattern(domain)) {
|
||||
throw new CertificateError('Cannot obtain certificates for glob pattern domains', domain, isRenewal);
|
||||
}
|
||||
|
||||
// Get the domain info
|
||||
const domainInfo = this.domainCertificates.get(domain);
|
||||
if (!domainInfo) {
|
||||
throw new Error(`Domain not found: ${domain}`);
|
||||
throw new CertificateError('Domain not found', domain, isRenewal);
|
||||
}
|
||||
|
||||
// Verify that acmeMaintenance is enabled
|
||||
if (!domainInfo.options.acmeMaintenance) {
|
||||
console.log(`Skipping certificate issuance for ${domain} - acmeMaintenance is disabled`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent concurrent certificate issuance
|
||||
@ -377,40 +679,8 @@ export class AcmeCertManager extends plugins.EventEmitter {
|
||||
// Get the authorizations for the order
|
||||
const authorizations = await client.getAuthorizations(order);
|
||||
|
||||
for (const authz of authorizations) {
|
||||
const challenge = authz.challenges.find(ch => ch.type === 'http-01');
|
||||
if (!challenge) {
|
||||
throw new Error('HTTP-01 challenge not found');
|
||||
}
|
||||
|
||||
// Get the key authorization for the challenge
|
||||
const keyAuthorization = await client.getChallengeKeyAuthorization(challenge);
|
||||
|
||||
// Store the challenge data
|
||||
domainInfo.challengeToken = challenge.token;
|
||||
domainInfo.challengeKeyAuthorization = keyAuthorization;
|
||||
|
||||
// ACME client type definition workaround - use compatible approach
|
||||
// First check if challenge verification is needed
|
||||
const authzUrl = authz.url;
|
||||
|
||||
try {
|
||||
// Check if authzUrl exists and perform verification
|
||||
if (authzUrl) {
|
||||
await client.verifyChallenge(authz, challenge);
|
||||
}
|
||||
|
||||
// Complete the challenge
|
||||
await client.completeChallenge(challenge);
|
||||
|
||||
// Wait for validation
|
||||
await client.waitForValidStatus(challenge);
|
||||
console.log(`HTTP-01 challenge completed for ${domain}`);
|
||||
} catch (error) {
|
||||
console.error(`Challenge error for ${domain}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// Process each authorization
|
||||
await this.processAuthorizations(client, domain, authorizations);
|
||||
|
||||
// Generate a CSR and private key
|
||||
const [csrBuffer, privateKeyBuffer] = await plugins.acme.forge.createCsr({
|
||||
@ -436,28 +706,20 @@ export class AcmeCertManager extends plugins.EventEmitter {
|
||||
delete domainInfo.challengeKeyAuthorization;
|
||||
|
||||
// Extract expiry date from certificate
|
||||
try {
|
||||
const matches = certificate.match(/Not After\s*:\s*(.*?)(?:\n|$)/i);
|
||||
if (matches && matches[1]) {
|
||||
domainInfo.expiryDate = new Date(matches[1]);
|
||||
console.log(`Certificate for ${domain} will expire on ${domainInfo.expiryDate.toISOString()}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to extract expiry date from certificate for ${domain}`);
|
||||
}
|
||||
domainInfo.expiryDate = this.extractExpiryDateFromCertificate(certificate, domain);
|
||||
|
||||
console.log(`Certificate ${isRenewal ? 'renewed' : 'obtained'} for ${domain}`);
|
||||
|
||||
// Emit the appropriate event
|
||||
const eventType = isRenewal
|
||||
? CertManagerEvents.CERTIFICATE_RENEWED
|
||||
: CertManagerEvents.CERTIFICATE_ISSUED;
|
||||
? Port80HandlerEvents.CERTIFICATE_RENEWED
|
||||
: Port80HandlerEvents.CERTIFICATE_ISSUED;
|
||||
|
||||
this.emitCertificateEvent(eventType, {
|
||||
domain,
|
||||
certificate,
|
||||
privateKey,
|
||||
expiryDate: domainInfo.expiryDate || new Date(Date.now() + 90 * 24 * 60 * 60 * 1000) // 90 days default
|
||||
expiryDate: domainInfo.expiryDate || this.getDefaultExpiryDate()
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
@ -473,17 +735,76 @@ export class AcmeCertManager extends plugins.EventEmitter {
|
||||
}
|
||||
|
||||
// Emit failure event
|
||||
this.emit(CertManagerEvents.CERTIFICATE_FAILED, {
|
||||
this.emit(Port80HandlerEvents.CERTIFICATE_FAILED, {
|
||||
domain,
|
||||
error: error.message || 'Unknown error',
|
||||
isRenewal
|
||||
});
|
||||
} as ICertificateFailure);
|
||||
|
||||
throw new CertificateError(
|
||||
error.message || 'Certificate issuance failed',
|
||||
domain,
|
||||
isRenewal
|
||||
);
|
||||
} finally {
|
||||
// Reset flag whether successful or not
|
||||
domainInfo.obtainingInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process ACME authorizations by verifying and completing challenges
|
||||
* @param client ACME client
|
||||
* @param domain Domain name
|
||||
* @param authorizations Authorizations to process
|
||||
*/
|
||||
private async processAuthorizations(
|
||||
client: plugins.acme.Client,
|
||||
domain: string,
|
||||
authorizations: plugins.acme.Authorization[]
|
||||
): Promise<void> {
|
||||
const domainInfo = this.domainCertificates.get(domain);
|
||||
if (!domainInfo) {
|
||||
throw new CertificateError('Domain not found during authorization', domain);
|
||||
}
|
||||
|
||||
for (const authz of authorizations) {
|
||||
const challenge = authz.challenges.find(ch => ch.type === 'http-01');
|
||||
if (!challenge) {
|
||||
throw new CertificateError('HTTP-01 challenge not found', domain);
|
||||
}
|
||||
|
||||
// Get the key authorization for the challenge
|
||||
const keyAuthorization = await client.getChallengeKeyAuthorization(challenge);
|
||||
|
||||
// Store the challenge data
|
||||
domainInfo.challengeToken = challenge.token;
|
||||
domainInfo.challengeKeyAuthorization = keyAuthorization;
|
||||
|
||||
// ACME client type definition workaround - use compatible approach
|
||||
// First check if challenge verification is needed
|
||||
const authzUrl = authz.url;
|
||||
|
||||
try {
|
||||
// Check if authzUrl exists and perform verification
|
||||
if (authzUrl) {
|
||||
await client.verifyChallenge(authz, challenge);
|
||||
}
|
||||
|
||||
// Complete the challenge
|
||||
await client.completeChallenge(challenge);
|
||||
|
||||
// Wait for validation
|
||||
await client.waitForValidStatus(challenge);
|
||||
console.log(`HTTP-01 challenge completed for ${domain}`);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown challenge error';
|
||||
console.error(`Challenge error for ${domain}:`, error);
|
||||
throw new CertificateError(`Challenge verification failed: ${errorMessage}`, domain);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the certificate renewal timer
|
||||
*/
|
||||
@ -519,6 +840,16 @@ export class AcmeCertManager extends plugins.EventEmitter {
|
||||
const renewThresholdMs = this.options.renewThresholdDays * 24 * 60 * 60 * 1000;
|
||||
|
||||
for (const [domain, domainInfo] of this.domainCertificates.entries()) {
|
||||
// Skip glob patterns
|
||||
if (this.isGlobPattern(domain)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip domains with acmeMaintenance disabled
|
||||
if (!domainInfo.options.acmeMaintenance) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip domains without certificates or already in renewal
|
||||
if (!domainInfo.certObtained || domainInfo.obtainingInProgress) {
|
||||
continue;
|
||||
@ -534,26 +865,67 @@ export class AcmeCertManager extends plugins.EventEmitter {
|
||||
// Check if certificate is near expiry
|
||||
if (timeUntilExpiry <= renewThresholdMs) {
|
||||
console.log(`Certificate for ${domain} expires soon, renewing...`);
|
||||
this.emit(CertManagerEvents.CERTIFICATE_EXPIRING, {
|
||||
|
||||
const daysRemaining = Math.ceil(timeUntilExpiry / (24 * 60 * 60 * 1000));
|
||||
|
||||
this.emit(Port80HandlerEvents.CERTIFICATE_EXPIRING, {
|
||||
domain,
|
||||
expiryDate: domainInfo.expiryDate,
|
||||
daysRemaining: Math.ceil(timeUntilExpiry / (24 * 60 * 60 * 1000))
|
||||
});
|
||||
daysRemaining
|
||||
} as ICertificateExpiring);
|
||||
|
||||
// Start renewal process
|
||||
this.obtainCertificate(domain, true).catch(err => {
|
||||
console.error(`Error renewing certificate for ${domain}:`, err);
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
||||
console.error(`Error renewing certificate for ${domain}:`, errorMessage);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract expiry date from certificate using a more robust approach
|
||||
* @param certificate Certificate PEM string
|
||||
* @param domain Domain for logging
|
||||
* @returns Extracted expiry date or default
|
||||
*/
|
||||
private extractExpiryDateFromCertificate(certificate: string, domain: string): Date {
|
||||
try {
|
||||
// This is still using regex, but in a real implementation you would use
|
||||
// a library like node-forge or x509 to properly parse the certificate
|
||||
const matches = certificate.match(/Not After\s*:\s*(.*?)(?:\n|$)/i);
|
||||
if (matches && matches[1]) {
|
||||
const expiryDate = new Date(matches[1]);
|
||||
|
||||
// Validate that we got a valid date
|
||||
if (!isNaN(expiryDate.getTime())) {
|
||||
console.log(`Certificate for ${domain} will expire on ${expiryDate.toISOString()}`);
|
||||
return expiryDate;
|
||||
}
|
||||
}
|
||||
|
||||
console.warn(`Could not extract valid expiry date from certificate for ${domain}, using default`);
|
||||
return this.getDefaultExpiryDate();
|
||||
} catch (error) {
|
||||
console.warn(`Failed to extract expiry date from certificate for ${domain}, using default`);
|
||||
return this.getDefaultExpiryDate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a default expiry date (90 days from now)
|
||||
* @returns Default expiry date
|
||||
*/
|
||||
private getDefaultExpiryDate(): Date {
|
||||
return new Date(Date.now() + 90 * 24 * 60 * 60 * 1000); // 90 days default
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits a certificate event with the certificate data
|
||||
* @param eventType The event type to emit
|
||||
* @param data The certificate data
|
||||
*/
|
||||
private emitCertificateEvent(eventType: CertManagerEvents, data: ICertificateData): void {
|
||||
private emitCertificateEvent(eventType: Port80HandlerEvents, data: ICertificateData): void {
|
||||
this.emit(eventType, data);
|
||||
}
|
||||
}
|
@ -589,17 +589,11 @@ export class ConnectionHandler {
|
||||
socket.cork();
|
||||
const writeSuccessful = socket.write(serverNameUnknownAlertData);
|
||||
socket.uncork();
|
||||
socket.end();
|
||||
|
||||
// Function to handle the clean socket termination - but more gradually
|
||||
const finishConnection = () => {
|
||||
// Give Chrome more time to process the alert before closing
|
||||
// We won't call destroy() at all - just end() and let the socket close naturally
|
||||
|
||||
// Log the cleanup but wait for natural closure
|
||||
setTimeout(() => {
|
||||
socket.end();
|
||||
this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
|
||||
}, 1000); // Longer delay to let socket cleanup happen naturally
|
||||
this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
|
||||
};
|
||||
|
||||
if (writeSuccessful) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
export * from './classes.iptablesproxy.js';
|
||||
export * from './classes.networkproxy.js';
|
||||
export * from './classes.pp.portproxy.js';
|
||||
export * from './classes.port80handler.js';
|
||||
export * from './classes.sslredirect.js';
|
||||
export * from './classes.pp.portproxy.js';
|
||||
export * from './classes.pp.snihandler.js';
|
||||
export * from './classes.pp.interfaces.js';
|
||||
|
Reference in New Issue
Block a user