Compare commits
36 Commits
Author | SHA1 | Date | |
---|---|---|---|
54e81b3c32 | |||
b7b47cd11f | |||
62061517fd | |||
531350a1c1 | |||
559a52af41 | |||
f8c86c76ae | |||
cc04e8786c | |||
9cb6e397b9 | |||
11b65bf684 | |||
4b30e377b9 | |||
b10f35be4b | |||
426249e70e | |||
ba0d9d0b8e | |||
151b8f498c | |||
0db4b07b22 | |||
b55e2da23e | |||
3593e411cf | |||
ca6f6de798 | |||
80d2f30804 | |||
22f46700f1 | |||
1611f65455 | |||
c6350e271a | |||
0fb5e5ea50 | |||
35f6739b3c | |||
4634c68ea6 | |||
e126032b61 | |||
7797c799dd | |||
e8639e1b01 | |||
60a0ad106d | |||
a70c123007 | |||
46aa7620b0 | |||
f72db86e37 | |||
d612df107e | |||
1c34578c36 | |||
1f9943b5a7 | |||
67ddf97547 |
117
changelog.md
117
changelog.md
@ -1,5 +1,122 @@
|
||||
# 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
|
||||
|
||||
- Introduce sendForceSniSequence to combine multiple alerts and force clients to provide SNI
|
||||
- Add sendFatalAndClose to immediately send a fatal alert and close the connection
|
||||
- Enhance TLS alert handling for better browser compatibility and error management
|
||||
|
||||
## 2025-03-17 - 4.1.16 - fix(tls)
|
||||
Improve TLS alert handling in connection handler: use the new TlsAlert class to send proper unrecognized_name alerts when a ClientHello is missing SNI and wait for a retry on the same connection before closing. Also, add alertFallbackTimeout tracking to connection records for better timeout management.
|
||||
|
||||
- Replaced hardcoded alert buffers in ConnectionHandler with calls to the TlsAlert class.
|
||||
- Removed old warnings and implemented a mechanism to remove existing 'data' listeners and await a new ClientHello.
|
||||
- Introduced alertFallbackTimeout property in connection records to track fallback timeout and ensure proper cleanup.
|
||||
- Extended the delay before closing the connection after sending an alert, providing the client more time to retry.
|
||||
|
||||
## 2025-03-17 - 4.1.15 - fix(connectionhandler)
|
||||
Delay socket termination in TLS session resumption handling to allow proper alert processing
|
||||
|
||||
- Removed the immediate socket.end() call in finishConnection and moved it inside the setTimeout, ensuring that clients (especially Chrome) have additional time to process the TLS alert before connection termination
|
||||
- This prevents premature socket closure on ClientHello without SNI when session tickets are disallowed
|
||||
|
||||
## 2025-03-17 - 4.1.14 - fix(ConnectionHandler)
|
||||
Use the correct TLS alert data and increase the delay before socket termination when session resumption without SNI is detected.
|
||||
|
||||
- Replaced certificateExpiredAlert with serverNameUnknownAlertData for sending the appropriate alert.
|
||||
- Increased the cleanup delay from 1000ms to 5000ms to allow a more graceful termination.
|
||||
|
||||
## 2025-03-17 - 4.1.13 - fix(tls-handshake)
|
||||
Set certificate_expired TLS alert level to warning instead of fatal to allow graceful termination.
|
||||
|
||||
- In the TLS handshake alert for certificate_expired (0x2F), changed the alert level from 0x02 (fatal) to 0x01 (warning).
|
||||
- This change avoids abrupt connection termination, enabling a smoother handling of certificate expiration alerts.
|
||||
|
||||
## 2025-03-17 - 4.1.12 - fix(classes.pp.connectionhandler)
|
||||
Replace unrecognized_name alert data with certificate_expired alert in TLS handshake handling for session resumption without SNI
|
||||
|
||||
- Switched the alert payload from serverNameUnknownAlertData to a new certificateExpiredAlert buffer
|
||||
- Now sends a fatal certificate_expired alert (code 47) instead of a warning unrecognized_name alert
|
||||
- Improves TLS error reporting and encourages immediate disconnection when a ClientHello lacks SNI and session tickets are disallowed
|
||||
|
||||
## 2025-03-17 - 4.1.11 - fix(connectionhandler)
|
||||
Increase delay before cleaning up connections when session resumption is blocked due to missing SNI, allowing more natural socket termination.
|
||||
|
||||
- Changed cleanup delay in ts/classes.pp.connectionhandler.ts from 300ms to 1000ms.
|
||||
- This fix ensures that sockets get sufficient time to terminate gracefully.
|
||||
|
||||
## 2025-03-16 - 4.1.10 - fix(connectionhandler)
|
||||
Increase delay timings for TLS alert transmission in session ticket blocking to allow graceful socket termination
|
||||
|
||||
- Updated finishConnection: replaced immediate socket.destroy with a graceful end call
|
||||
- Increased delay after successful write from 50ms to 200ms to allow alert processing
|
||||
- Raised safety timeout from 250ms to 400ms when waiting for 'drain' event
|
||||
|
||||
## 2025-03-16 - 4.1.9 - fix(ConnectionHandler)
|
||||
Replace closeNotify alert with handshake failure alert in TLS ClientHello handling to properly signal missing SNI and enforce session ticket restrictions.
|
||||
|
||||
- Switched alert data sent on missing SNI from closeNotifyAlert to sslHandshakeFailureAlertData.
|
||||
- Ensures consistent TLS alert behavior during handshake failure.
|
||||
|
||||
## 2025-03-16 - 4.1.8 - fix(ConnectionHandler/tls)
|
||||
Change the TLS alert sent when a ClientHello lacks SNI: use the close_notify alert instead of handshake_failure to prompt immediate retry with SNI.
|
||||
|
||||
- Replaced the previously sent handshake_failure alert (code 0x28) with a close_notify alert (code 0x00) in the TLS session resumption handling in ConnectionHandler.
|
||||
- This change encourages clients to immediately retry and include SNI when allowSessionTicket is false.
|
||||
|
||||
## 2025-03-16 - 4.1.7 - fix(classes.pp.connectionhandler)
|
||||
Improve TLS alert handling in ClientHello when SNI is missing and session tickets are disallowed
|
||||
|
||||
- Replace the unrecognized_name alert with a handshake_failure alert to ensure better client behavior.
|
||||
- Refactor the alert sending mechanism using cork/uncork and add a safety timeout for the drain event.
|
||||
- Enhance logging for debugging TLS handshake failures when SNI is absent.
|
||||
|
||||
## 2025-03-16 - 4.1.6 - fix(tls)
|
||||
Refine TLS ClientHello handling when allowSessionTicket is false by replacing extensive alert timeout logic with a concise warning alert and short delay, encouraging immediate client retry with proper SNI
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@push.rocks/smartproxy",
|
||||
"version": "4.1.6",
|
||||
"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.1.6',
|
||||
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);
|
||||
}
|
||||
}
|
@ -560,9 +560,9 @@ export class ConnectionHandler {
|
||||
// Block ClientHello without SNI when allowSessionTicket is false
|
||||
console.log(
|
||||
`[${connectionId}] No SNI detected in ClientHello and allowSessionTicket=false. ` +
|
||||
`Sending warning unrecognized_name alert to encourage immediate retry with SNI.`
|
||||
`Sending warning unrecognized_name alert to encourage immediate retry with SNI.`
|
||||
);
|
||||
|
||||
|
||||
// Set the termination reason first
|
||||
if (record.incomingTerminationReason === null) {
|
||||
record.incomingTerminationReason = 'session_ticket_blocked_no_sni';
|
||||
@ -571,10 +571,10 @@ export class ConnectionHandler {
|
||||
'session_ticket_blocked_no_sni'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Create a warning-level alert for unrecognized_name
|
||||
// This encourages Chrome to retry immediately with SNI
|
||||
const alertData = Buffer.from([
|
||||
const serverNameUnknownAlertData = Buffer.from([
|
||||
0x15, // Alert record type
|
||||
0x03,
|
||||
0x03, // TLS 1.2 version
|
||||
@ -583,43 +583,34 @@ export class ConnectionHandler {
|
||||
0x01, // Warning alert level (not fatal)
|
||||
0x70, // unrecognized_name alert (code 112)
|
||||
]);
|
||||
|
||||
|
||||
try {
|
||||
// Use cork/uncork to ensure the alert is sent as a single packet
|
||||
socket.cork();
|
||||
const writeSuccessful = socket.write(alertData);
|
||||
const writeSuccessful = socket.write(serverNameUnknownAlertData);
|
||||
socket.uncork();
|
||||
socket.end();
|
||||
|
||||
// Function to handle the clean socket termination
|
||||
// Function to handle the clean socket termination - but more gradually
|
||||
const finishConnection = () => {
|
||||
// First call end() to initiate a graceful close (sends FIN)
|
||||
socket.end();
|
||||
|
||||
// Allow a short delay for the alert and FIN to be transmitted
|
||||
// before we fully close the socket
|
||||
setTimeout(() => {
|
||||
if (!socket.destroyed) {
|
||||
socket.destroy();
|
||||
}
|
||||
this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
|
||||
}, 150); // Short delay, but longer than the standard TCP ACK timeout
|
||||
this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
|
||||
};
|
||||
|
||||
if (writeSuccessful) {
|
||||
// If the data was successfully written to the kernel buffer,
|
||||
// we can finish the connection after a short delay to ensure transmission
|
||||
setTimeout(finishConnection, 50);
|
||||
// Wait longer before ending connection to ensure alert is processed by client
|
||||
setTimeout(finishConnection, 200); // Increased from 50ms to 200ms
|
||||
} else {
|
||||
// If the kernel buffer was full, wait for the drain event
|
||||
socket.once('drain', () => {
|
||||
setTimeout(finishConnection, 50);
|
||||
// Wait longer after drain as well
|
||||
setTimeout(finishConnection, 200);
|
||||
});
|
||||
|
||||
// Set a safety timeout in case drain never happens
|
||||
// Safety timeout is increased too
|
||||
setTimeout(() => {
|
||||
socket.removeAllListeners('drain');
|
||||
finishConnection();
|
||||
}, 250);
|
||||
}, 400); // Increased from 250ms to 400ms
|
||||
}
|
||||
} catch (err) {
|
||||
// If we can't send the alert, fall back to immediate termination
|
||||
@ -627,7 +618,7 @@ export class ConnectionHandler {
|
||||
socket.end();
|
||||
this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
|
||||
}
|
||||
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -1075,4 +1066,4 @@ export class ConnectionHandler {
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -104,6 +104,7 @@ export interface IConnectionRecord {
|
||||
lockedDomain?: string; // Used to lock this connection to the initial SNI
|
||||
connectionClosed: boolean; // Flag to prevent multiple cleanup attempts
|
||||
cleanupTimer?: NodeJS.Timeout; // Timer for max lifetime/inactivity
|
||||
alertFallbackTimeout?: NodeJS.Timeout; // Timer for fallback after alert
|
||||
lastActivity: number; // Last activity timestamp for inactivity detection
|
||||
pendingData: Buffer[]; // Buffer to hold data during connection setup
|
||||
pendingDataSize: number; // Track total size of pending data
|
||||
|
258
ts/classes.pp.tlsalert.ts
Normal file
258
ts/classes.pp.tlsalert.ts
Normal file
@ -0,0 +1,258 @@
|
||||
import * as net from 'net';
|
||||
|
||||
/**
|
||||
* TlsAlert class for managing TLS alert messages
|
||||
*/
|
||||
export class TlsAlert {
|
||||
// TLS Alert Levels
|
||||
static readonly LEVEL_WARNING = 0x01;
|
||||
static readonly LEVEL_FATAL = 0x02;
|
||||
|
||||
// TLS Alert Description Codes - RFC 8446 (TLS 1.3) / RFC 5246 (TLS 1.2)
|
||||
static readonly CLOSE_NOTIFY = 0x00;
|
||||
static readonly UNEXPECTED_MESSAGE = 0x0A;
|
||||
static readonly BAD_RECORD_MAC = 0x14;
|
||||
static readonly DECRYPTION_FAILED = 0x15; // TLS 1.0 only
|
||||
static readonly RECORD_OVERFLOW = 0x16;
|
||||
static readonly DECOMPRESSION_FAILURE = 0x1E; // TLS 1.2 and below
|
||||
static readonly HANDSHAKE_FAILURE = 0x28;
|
||||
static readonly NO_CERTIFICATE = 0x29; // SSLv3 only
|
||||
static readonly BAD_CERTIFICATE = 0x2A;
|
||||
static readonly UNSUPPORTED_CERTIFICATE = 0x2B;
|
||||
static readonly CERTIFICATE_REVOKED = 0x2C;
|
||||
static readonly CERTIFICATE_EXPIRED = 0x2F;
|
||||
static readonly CERTIFICATE_UNKNOWN = 0x30;
|
||||
static readonly ILLEGAL_PARAMETER = 0x2F;
|
||||
static readonly UNKNOWN_CA = 0x30;
|
||||
static readonly ACCESS_DENIED = 0x31;
|
||||
static readonly DECODE_ERROR = 0x32;
|
||||
static readonly DECRYPT_ERROR = 0x33;
|
||||
static readonly EXPORT_RESTRICTION = 0x3C; // TLS 1.0 only
|
||||
static readonly PROTOCOL_VERSION = 0x46;
|
||||
static readonly INSUFFICIENT_SECURITY = 0x47;
|
||||
static readonly INTERNAL_ERROR = 0x50;
|
||||
static readonly INAPPROPRIATE_FALLBACK = 0x56;
|
||||
static readonly USER_CANCELED = 0x5A;
|
||||
static readonly NO_RENEGOTIATION = 0x64; // TLS 1.2 and below
|
||||
static readonly MISSING_EXTENSION = 0x6D; // TLS 1.3
|
||||
static readonly UNSUPPORTED_EXTENSION = 0x6E; // TLS 1.3
|
||||
static readonly CERTIFICATE_REQUIRED = 0x6F; // TLS 1.3
|
||||
static readonly UNRECOGNIZED_NAME = 0x70;
|
||||
static readonly BAD_CERTIFICATE_STATUS_RESPONSE = 0x71;
|
||||
static readonly BAD_CERTIFICATE_HASH_VALUE = 0x72; // TLS 1.2 and below
|
||||
static readonly UNKNOWN_PSK_IDENTITY = 0x73;
|
||||
static readonly CERTIFICATE_REQUIRED_1_3 = 0x74; // TLS 1.3
|
||||
static readonly NO_APPLICATION_PROTOCOL = 0x78;
|
||||
|
||||
/**
|
||||
* Create a TLS alert buffer with the specified level and description code
|
||||
*
|
||||
* @param level Alert level (warning or fatal)
|
||||
* @param description Alert description code
|
||||
* @param tlsVersion TLS version bytes (default is TLS 1.2: 0x0303)
|
||||
* @returns Buffer containing the TLS alert message
|
||||
*/
|
||||
static create(
|
||||
level: number,
|
||||
description: number,
|
||||
tlsVersion: [number, number] = [0x03, 0x03]
|
||||
): Buffer {
|
||||
return Buffer.from([
|
||||
0x15, // Alert record type
|
||||
tlsVersion[0],
|
||||
tlsVersion[1], // TLS version (default to TLS 1.2: 0x0303)
|
||||
0x00,
|
||||
0x02, // Length
|
||||
level, // Alert level
|
||||
description, // Alert description
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a warning-level TLS alert
|
||||
*
|
||||
* @param description Alert description code
|
||||
* @returns Buffer containing the warning-level TLS alert message
|
||||
*/
|
||||
static createWarning(description: number): Buffer {
|
||||
return this.create(this.LEVEL_WARNING, description);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a fatal-level TLS alert
|
||||
*
|
||||
* @param description Alert description code
|
||||
* @returns Buffer containing the fatal-level TLS alert message
|
||||
*/
|
||||
static createFatal(description: number): Buffer {
|
||||
return this.create(this.LEVEL_FATAL, description);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a TLS alert to a socket and optionally close the connection
|
||||
*
|
||||
* @param socket The socket to send the alert to
|
||||
* @param level Alert level (warning or fatal)
|
||||
* @param description Alert description code
|
||||
* @param closeAfterSend Whether to close the connection after sending the alert
|
||||
* @param closeDelay Milliseconds to wait before closing the connection (default: 200ms)
|
||||
* @returns Promise that resolves when the alert has been sent
|
||||
*/
|
||||
static async send(
|
||||
socket: net.Socket,
|
||||
level: number,
|
||||
description: number,
|
||||
closeAfterSend: boolean = false,
|
||||
closeDelay: number = 200
|
||||
): Promise<void> {
|
||||
const alert = this.create(level, description);
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
try {
|
||||
// Ensure the alert is written as a single packet
|
||||
socket.cork();
|
||||
const writeSuccessful = socket.write(alert, (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (closeAfterSend) {
|
||||
setTimeout(() => {
|
||||
socket.end();
|
||||
resolve();
|
||||
}, closeDelay);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
socket.uncork();
|
||||
|
||||
// If write wasn't successful immediately, wait for drain
|
||||
if (!writeSuccessful && !closeAfterSend) {
|
||||
socket.once('drain', () => {
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-defined TLS alert messages
|
||||
*/
|
||||
static readonly alerts = {
|
||||
// Warning level alerts
|
||||
closeNotify: TlsAlert.createWarning(TlsAlert.CLOSE_NOTIFY),
|
||||
unsupportedExtension: TlsAlert.createWarning(TlsAlert.UNSUPPORTED_EXTENSION),
|
||||
certificateRequired: TlsAlert.createWarning(TlsAlert.CERTIFICATE_REQUIRED),
|
||||
unrecognizedName: TlsAlert.createWarning(TlsAlert.UNRECOGNIZED_NAME),
|
||||
noRenegotiation: TlsAlert.createWarning(TlsAlert.NO_RENEGOTIATION),
|
||||
userCanceled: TlsAlert.createWarning(TlsAlert.USER_CANCELED),
|
||||
|
||||
// Warning level alerts for session resumption
|
||||
certificateExpiredWarning: TlsAlert.createWarning(TlsAlert.CERTIFICATE_EXPIRED),
|
||||
handshakeFailureWarning: TlsAlert.createWarning(TlsAlert.HANDSHAKE_FAILURE),
|
||||
insufficientSecurityWarning: TlsAlert.createWarning(TlsAlert.INSUFFICIENT_SECURITY),
|
||||
|
||||
// Fatal level alerts
|
||||
unexpectedMessage: TlsAlert.createFatal(TlsAlert.UNEXPECTED_MESSAGE),
|
||||
badRecordMac: TlsAlert.createFatal(TlsAlert.BAD_RECORD_MAC),
|
||||
recordOverflow: TlsAlert.createFatal(TlsAlert.RECORD_OVERFLOW),
|
||||
handshakeFailure: TlsAlert.createFatal(TlsAlert.HANDSHAKE_FAILURE),
|
||||
badCertificate: TlsAlert.createFatal(TlsAlert.BAD_CERTIFICATE),
|
||||
certificateExpired: TlsAlert.createFatal(TlsAlert.CERTIFICATE_EXPIRED),
|
||||
certificateUnknown: TlsAlert.createFatal(TlsAlert.CERTIFICATE_UNKNOWN),
|
||||
illegalParameter: TlsAlert.createFatal(TlsAlert.ILLEGAL_PARAMETER),
|
||||
unknownCA: TlsAlert.createFatal(TlsAlert.UNKNOWN_CA),
|
||||
accessDenied: TlsAlert.createFatal(TlsAlert.ACCESS_DENIED),
|
||||
decodeError: TlsAlert.createFatal(TlsAlert.DECODE_ERROR),
|
||||
decryptError: TlsAlert.createFatal(TlsAlert.DECRYPT_ERROR),
|
||||
protocolVersion: TlsAlert.createFatal(TlsAlert.PROTOCOL_VERSION),
|
||||
insufficientSecurity: TlsAlert.createFatal(TlsAlert.INSUFFICIENT_SECURITY),
|
||||
internalError: TlsAlert.createFatal(TlsAlert.INTERNAL_ERROR),
|
||||
unrecognizedNameFatal: TlsAlert.createFatal(TlsAlert.UNRECOGNIZED_NAME),
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility method to send a warning-level unrecognized_name alert
|
||||
* Specifically designed for SNI issues to encourage the client to retry with SNI
|
||||
*
|
||||
* @param socket The socket to send the alert to
|
||||
* @returns Promise that resolves when the alert has been sent
|
||||
*/
|
||||
static async sendSniRequired(socket: net.Socket): Promise<void> {
|
||||
return this.send(socket, this.LEVEL_WARNING, this.UNRECOGNIZED_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to send a close_notify alert and close the connection
|
||||
*
|
||||
* @param socket The socket to send the alert to
|
||||
* @param closeDelay Milliseconds to wait before closing the connection (default: 200ms)
|
||||
* @returns Promise that resolves when the alert has been sent and the connection closed
|
||||
*/
|
||||
static async sendCloseNotify(socket: net.Socket, closeDelay: number = 200): Promise<void> {
|
||||
return this.send(socket, this.LEVEL_WARNING, this.CLOSE_NOTIFY, true, closeDelay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to send a certificate_expired alert to force new TLS session
|
||||
*
|
||||
* @param socket The socket to send the alert to
|
||||
* @param fatal Whether to send as a fatal alert (default: false)
|
||||
* @param closeAfterSend Whether to close the connection after sending the alert (default: true)
|
||||
* @param closeDelay Milliseconds to wait before closing the connection (default: 200ms)
|
||||
* @returns Promise that resolves when the alert has been sent
|
||||
*/
|
||||
static async sendCertificateExpired(
|
||||
socket: net.Socket,
|
||||
fatal: boolean = false,
|
||||
closeAfterSend: boolean = true,
|
||||
closeDelay: number = 200
|
||||
): Promise<void> {
|
||||
const level = fatal ? this.LEVEL_FATAL : this.LEVEL_WARNING;
|
||||
return this.send(socket, level, this.CERTIFICATE_EXPIRED, closeAfterSend, closeDelay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a sequence of alerts to force SNI from clients
|
||||
* This combines multiple alerts to ensure maximum browser compatibility
|
||||
*
|
||||
* @param socket The socket to send the alerts to
|
||||
* @returns Promise that resolves when all alerts have been sent
|
||||
*/
|
||||
static async sendForceSniSequence(socket: net.Socket): Promise<void> {
|
||||
try {
|
||||
// Send unrecognized_name (warning)
|
||||
socket.cork();
|
||||
socket.write(this.alerts.unrecognizedName);
|
||||
socket.uncork();
|
||||
|
||||
// Give the socket time to send the alert
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, 50);
|
||||
});
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a fatal level alert that immediately terminates the connection
|
||||
*
|
||||
* @param socket The socket to send the alert to
|
||||
* @param description Alert description code
|
||||
* @param closeDelay Milliseconds to wait before closing the connection (default: 100ms)
|
||||
* @returns Promise that resolves when the alert has been sent and the connection closed
|
||||
*/
|
||||
static async sendFatalAndClose(
|
||||
socket: net.Socket,
|
||||
description: number,
|
||||
closeDelay: number = 100
|
||||
): Promise<void> {
|
||||
return this.send(socket, this.LEVEL_FATAL, description, true, closeDelay);
|
||||
}
|
||||
}
|
@ -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