BREAKING CHANGE(certificates): Remove legacy certificate modules and Port80Handler; update documentation and route configurations to use SmartCertManager for certificate management.

This commit is contained in:
Philipp Kunz 2025-05-18 16:30:23 +00:00
parent db2ac5bae3
commit 455b08b36c
33 changed files with 494 additions and 2904 deletions

View File

@ -1,5 +1,13 @@
# Changelog
## 2025-05-18 - 19.0.0 - BREAKING CHANGE(certificates)
Remove legacy certificate modules and Port80Handler; update documentation and route configurations to use SmartCertManager for certificate management.
- Removed deprecated files under ts/certificate (acme, events, storage, providers) and ts/http/port80.
- Updated readme.md and docs/certificate-management.md to reflect new SmartCertManager integration and removal of Port80Handler.
- Updated route types and models to remove legacy certificate types and references to Port80Handler.
- Bumped major version to reflect breaking changes in certificate management.
## 2025-05-18 - 18.2.0 - feat(smartproxy/certificate)
Integrate HTTP-01 challenge handler into ACME certificate provisioning workflow

View File

@ -0,0 +1,217 @@
# Certificate Management in SmartProxy v18+
## Overview
SmartProxy v18+ introduces a simplified certificate management system using the new `SmartCertManager` class. This replaces the previous `Port80Handler` and multiple certificate-related modules with a unified, route-based approach.
## Key Changes from Previous Versions
- **No backward compatibility**: This is a clean break from the legacy certificate system
- **No separate Port80Handler**: ACME challenges are now handled as regular SmartProxy routes
- **Unified route-based configuration**: Certificates are configured directly in route definitions
- **Direct integration with @push.rocks/smartacme**: Leverages SmartAcme's built-in capabilities
## Configuration
### Route-Level Certificate Configuration
Certificates are now configured at the route level using the `tls` property:
```typescript
const route: IRouteConfig = {
name: 'secure-website',
match: {
ports: 443,
domains: ['example.com', 'www.example.com']
},
action: {
type: 'forward',
target: { host: 'localhost', port: 8080 },
tls: {
mode: 'terminate',
certificate: 'auto', // Use ACME (Let's Encrypt)
acme: {
email: 'admin@example.com',
useProduction: true,
renewBeforeDays: 30
}
}
}
};
```
### Static Certificate Configuration
For manually managed certificates:
```typescript
const route: IRouteConfig = {
name: 'api-endpoint',
match: {
ports: 443,
domains: 'api.example.com'
},
action: {
type: 'forward',
target: { host: 'localhost', port: 9000 },
tls: {
mode: 'terminate',
certificate: {
certFile: './certs/api.crt',
keyFile: './certs/api.key',
ca: '...' // Optional CA chain
}
}
}
};
```
## TLS Modes
SmartProxy supports three TLS modes:
1. **terminate**: Decrypt TLS at the proxy and forward plain HTTP
2. **passthrough**: Pass encrypted TLS traffic directly to the backend
3. **terminate-and-reencrypt**: Decrypt at proxy, then re-encrypt to backend
## Certificate Storage
Certificates are stored in the `./certs` directory by default:
```
./certs/
├── route-name/
│ ├── cert.pem
│ ├── key.pem
│ ├── ca.pem (if available)
│ └── meta.json
```
## ACME Integration
### How It Works
1. SmartProxy creates a high-priority route for ACME challenges
2. When ACME server makes requests to `/.well-known/acme-challenge/*`, SmartProxy handles them automatically
3. Certificates are obtained and stored locally
4. Automatic renewal checks every 12 hours
### Configuration Options
```typescript
export interface IRouteAcme {
email: string; // Contact email for ACME account
useProduction?: boolean; // Use production servers (default: false)
challengePort?: number; // Port for HTTP-01 challenges (default: 80)
renewBeforeDays?: number; // Days before expiry to renew (default: 30)
}
```
## Advanced Usage
### Manual Certificate Operations
```typescript
// Get certificate status
const status = proxy.getCertificateStatus('route-name');
console.log(status);
// {
// domain: 'example.com',
// status: 'valid',
// source: 'acme',
// expiryDate: Date,
// issueDate: Date
// }
// Force certificate renewal
await proxy.renewCertificate('route-name');
// Manually provision a certificate
await proxy.provisionCertificate('route-name');
```
### Events
SmartProxy emits certificate-related events:
```typescript
proxy.on('certificate:issued', (event) => {
console.log(`New certificate for ${event.domain}`);
});
proxy.on('certificate:renewed', (event) => {
console.log(`Certificate renewed for ${event.domain}`);
});
proxy.on('certificate:expiring', (event) => {
console.log(`Certificate expiring soon for ${event.domain}`);
});
```
## Migration from Previous Versions
### Before (v17 and earlier)
```typescript
// Old approach with Port80Handler
const smartproxy = new SmartProxy({
port: 443,
acme: {
enabled: true,
accountEmail: 'admin@example.com',
// ... other ACME options
}
});
// Certificate provisioning was automatic or via certProvisionFunction
```
### After (v18+)
```typescript
// New approach with route-based configuration
const smartproxy = new SmartProxy({
routes: [{
match: { ports: 443, domains: 'example.com' },
action: {
type: 'forward',
target: { host: 'localhost', port: 8080 },
tls: {
mode: 'terminate',
certificate: 'auto',
acme: {
email: 'admin@example.com',
useProduction: true
}
}
}
}]
});
```
## Troubleshooting
### Common Issues
1. **Certificate not provisioning**: Ensure port 80 is accessible for ACME challenges
2. **ACME rate limits**: Use staging environment for testing
3. **Permission errors**: Ensure the certificate directory is writable
### Debug Mode
Enable detailed logging to troubleshoot certificate issues:
```typescript
const proxy = new SmartProxy({
enableDetailedLogging: true,
// ... other options
});
```
## Best Practices
1. **Always test with staging ACME servers first**
2. **Set up monitoring for certificate expiration**
3. **Use meaningful route names for easier certificate management**
4. **Store static certificates securely with appropriate permissions**
5. **Implement certificate status monitoring in production**

View File

@ -21,10 +21,10 @@ SmartProxy has been restructured using a modern, modular architecture with a uni
│ ├── /models # Data models and interfaces
│ ├── /utils # Shared utilities (IP validation, logging, etc.)
│ └── /events # Common event definitions
├── /certificate # Certificate management
│ ├── /acme # ACME-specific functionality
│ ├── /providers # Certificate providers (static, ACME)
│ └── /storage # Certificate storage mechanisms
├── /certificate # Certificate management (deprecated in v18+)
│ ├── /acme # Moved to SmartCertManager
│ ├── /providers # Now integrated in route configuration
│ └── /storage # Now uses CertStore
├── /forwarding # Forwarding system
│ ├── /handlers # Various forwarding handlers
│ │ ├── base-handler.ts # Abstract base handler
@ -37,6 +37,8 @@ SmartProxy has been restructured using a modern, modular architecture with a uni
│ │ ├── /models # SmartProxy-specific interfaces
│ │ │ ├── route-types.ts # Route-based configuration types
│ │ │ └── interfaces.ts # SmartProxy interfaces
│ │ ├── certificate-manager.ts # SmartCertManager (new in v18+)
│ │ ├── cert-store.ts # Certificate file storage
│ │ ├── route-helpers.ts # Helper functions for creating routes
│ │ ├── route-manager.ts # Route management system
│ │ ├── smart-proxy.ts # Main SmartProxy class
@ -47,7 +49,7 @@ SmartProxy has been restructured using a modern, modular architecture with a uni
│ ├── /sni # SNI handling components
│ └── /alerts # TLS alerts system
└── /http # HTTP-specific functionality
├── /port80 # Port80Handler components
├── /port80 # Port80Handler (removed in v18+)
├── /router # HTTP routing system
└── /redirects # Redirect handlers
```
@ -1411,6 +1413,12 @@ NetworkProxy now supports full route-based configuration including:
- `useIPSets` (boolean, default true)
- `qos`, `netProxyIntegration` (objects)
## Documentation
- [Certificate Management](docs/certificate-management.md) - Detailed guide on certificate provisioning and ACME integration
- [Port Handling](docs/porthandling.md) - Dynamic port management and runtime configuration
- [NFTables Integration](docs/nftables-integration.md) - High-performance kernel-level forwarding
## Troubleshooting
### SmartProxy

View File

@ -1,5 +1,22 @@
# ACME/Certificate Simplification Plan for SmartProxy
## Current Status: Implementation in Progress
### Completed Tasks:
- ✅ SmartCertManager class created
- ✅ CertStore class for file-based certificate storage
- ✅ Route types updated with new TLS/ACME interfaces
- ✅ Static route handler added to route-connection-handler.ts
- ✅ SmartProxy class updated to use SmartCertManager
- ✅ NetworkProxyBridge simplified by removing certificate logic
- ✅ HTTP index.ts updated to remove port80 exports
- ✅ Basic tests created for new certificate functionality
- ✅ SmartAcme integration completed using built-in MemoryCertManager
### Remaining Tasks:
- ❌ Remove old certificate module and port80 directory
- ❌ Update documentation with new configuration format
## Command to reread CLAUDE.md
`reread /home/philkunz/.claude/CLAUDE.md`
@ -71,14 +88,13 @@ ts/proxies/smart-proxy/
### Phase 1: Create SmartCertManager
#### 1.1 Create certificate-manager.ts
#### 1.1 Create certificate-manager.ts ✅ COMPLETED
```typescript
// ts/proxies/smart-proxy/certificate-manager.ts
import * as plugins from '../../plugins.js';
import { NetworkProxy } from '../network-proxy/index.js';
import type { IRouteConfig, IRouteTls } from './models/route-types.js';
import { CertStore } from './cert-store.js';
import { AcmeClient } from './acme-client.js';
export interface ICertStatus {
domain: string;
@ -578,7 +594,7 @@ class InMemoryCertManager implements plugins.smartacme.CertManager {
}
```
#### 1.2 Create cert-store.ts
#### 1.2 Create cert-store.ts ✅ COMPLETED
```typescript
// ts/proxies/smart-proxy/cert-store.ts
import * as plugins from '../../plugins.js';
@ -675,7 +691,7 @@ export class CertStore {
### Phase 2: Update Route Types and Handler
#### 2.1 Update route-types.ts
#### 2.1 Update route-types.ts ✅ COMPLETED
```typescript
// Add to ts/proxies/smart-proxy/models/route-types.ts
@ -742,7 +758,7 @@ export interface IRouteTls {
}
```
#### 2.2 Add Static Route Handler
#### 2.2 Add Static Route Handler ✅ COMPLETED
```typescript
// Add to ts/proxies/smart-proxy/route-connection-handler.ts
@ -839,7 +855,7 @@ function getStatusText(status: number): string {
### Phase 3: SmartProxy Integration
#### 3.1 Update SmartProxy class
#### 3.1 Update SmartProxy class ✅ COMPLETED
```typescript
// Changes to ts/proxies/smart-proxy/smart-proxy.ts
@ -1017,7 +1033,7 @@ export class SmartProxy extends plugins.EventEmitter {
}
```
#### 3.2 Simplify NetworkProxyBridge
#### 3.2 Simplify NetworkProxyBridge ✅ COMPLETED
```typescript
// Simplified ts/proxies/smart-proxy/network-proxy-bridge.ts
@ -1323,7 +1339,7 @@ Certificates are stored in the `./certs` directory by default:
### Phase 5: Update HTTP Module
#### 5.1 Update http/index.ts
#### 5.1 Update http/index.ts ✅ COMPLETED
```typescript
// ts/http/index.ts
/**
@ -1388,25 +1404,26 @@ The simplification leverages SmartProxy's existing capabilities rather than rein
## Implementation Sequence
1. **Day 1: Core Implementation**
1. **Day 1: Core Implementation** ✅ COMPLETED
- Create SmartCertManager class
- Create CertStore and AcmeClient
- Create CertStore
- Update route types
- Integrated with SmartAcme's built-in handlers
2. **Day 2: Integration**
2. **Day 2: Integration** ✅ COMPLETED
- Update SmartProxy to use SmartCertManager
- Simplify NetworkProxyBridge
- Remove old certificate system
- Update HTTP index.ts
3. **Day 3: Testing**
- Create new tests using new format only
- No migration testing needed
- Test all new functionality
3. **Day 3: Testing** ✅ COMPLETED
- Created test.smartacme-integration.ts
- Verified SmartAcme handler access
- Verified certificate manager creation
4. **Day 4: Documentation & Cleanup**
- Update all documentation
- Clean up old files
- Final testing and validation
4. **Day 4: Documentation & Cleanup** 🔄 IN PROGRESS
- Update all documentation
- Clean up old files (certificate/ and port80/)
- Final testing and validation
## Risk Mitigation

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartproxy',
version: '18.2.0',
version: '19.0.0',
description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.'
}

View File

@ -1,48 +0,0 @@
import * as fs from 'fs';
import * as path from 'path';
import type { IAcmeOptions } from '../models/certificate-types.js';
import { ensureCertificateDirectory } from '../utils/certificate-helpers.js';
// We'll need to update this import when we move the Port80Handler
import { Port80Handler } from '../../http/port80/port80-handler.js';
/**
* Factory to create a Port80Handler with common setup.
* Ensures the certificate store directory exists and instantiates the handler.
* @param options Port80Handler configuration options
* @returns A new Port80Handler instance
*/
export function buildPort80Handler(
options: IAcmeOptions
): Port80Handler {
if (options.certificateStore) {
ensureCertificateDirectory(options.certificateStore);
console.log(`Ensured certificate store directory: ${options.certificateStore}`);
}
return new Port80Handler(options);
}
/**
* Creates default ACME options with sensible defaults
* @param email Account email for ACME provider
* @param certificateStore Path to store certificates
* @param useProduction Whether to use production ACME servers
* @returns Configured ACME options
*/
export function createDefaultAcmeOptions(
email: string,
certificateStore: string,
useProduction: boolean = false
): IAcmeOptions {
return {
accountEmail: email,
enabled: true,
port: 80,
useProduction,
httpsRedirectPort: 443,
renewThresholdDays: 30,
renewCheckIntervalHours: 24,
autoRenew: true,
certificateStore,
skipConfiguredCerts: false
};
}

View File

@ -1,110 +0,0 @@
import * as plugins from '../../plugins.js';
import type { IAcmeOptions, ICertificateData } from '../models/certificate-types.js';
import { CertificateEvents } from '../events/certificate-events.js';
/**
* Manages ACME challenges and certificate validation
*/
export class AcmeChallengeHandler extends plugins.EventEmitter {
private options: IAcmeOptions;
private client: any; // ACME client from plugins
private pendingChallenges: Map<string, any>;
/**
* Creates a new ACME challenge handler
* @param options ACME configuration options
*/
constructor(options: IAcmeOptions) {
super();
this.options = options;
this.pendingChallenges = new Map();
// Initialize ACME client if needed
// This is just a placeholder implementation since we don't use the actual
// client directly in this implementation - it's handled by Port80Handler
this.client = null;
console.log('Created challenge handler with options:',
options.accountEmail,
options.useProduction ? 'production' : 'staging'
);
}
/**
* Gets or creates the ACME account key
*/
private getAccountKey(): Buffer {
// Implementation details would depend on plugin requirements
// This is a simplified version
if (!this.options.certificateStore) {
throw new Error('Certificate store is required for ACME challenges');
}
// This is just a placeholder - actual implementation would check for
// existing account key and create one if needed
return Buffer.from('account-key-placeholder');
}
/**
* Validates a domain using HTTP-01 challenge
* @param domain Domain to validate
* @param challengeToken ACME challenge token
* @param keyAuthorization Key authorization for the challenge
*/
public async handleHttpChallenge(
domain: string,
challengeToken: string,
keyAuthorization: string
): Promise<void> {
// Store challenge for response
this.pendingChallenges.set(challengeToken, keyAuthorization);
try {
// Wait for challenge validation - this would normally be handled by the ACME client
await new Promise(resolve => setTimeout(resolve, 1000));
this.emit(CertificateEvents.CERTIFICATE_ISSUED, {
domain,
success: true
});
} catch (error) {
this.emit(CertificateEvents.CERTIFICATE_FAILED, {
domain,
error: error instanceof Error ? error.message : String(error),
isRenewal: false
});
throw error;
} finally {
// Clean up the challenge
this.pendingChallenges.delete(challengeToken);
}
}
/**
* Responds to an HTTP-01 challenge request
* @param token Challenge token from the request path
* @returns The key authorization if found
*/
public getChallengeResponse(token: string): string | null {
return this.pendingChallenges.get(token) || null;
}
/**
* Checks if a request path is an ACME challenge
* @param path Request path
* @returns True if this is an ACME challenge request
*/
public isAcmeChallenge(path: string): boolean {
return path.startsWith('/.well-known/acme-challenge/');
}
/**
* Extracts the challenge token from an ACME challenge path
* @param path Request path
* @returns The challenge token if valid
*/
public extractChallengeToken(path: string): string | null {
if (!this.isAcmeChallenge(path)) return null;
const parts = path.split('/');
return parts[parts.length - 1] || null;
}
}

View File

@ -1,3 +0,0 @@
/**
* ACME certificate provisioning
*/

View File

@ -1,36 +0,0 @@
/**
* Certificate-related events emitted by certificate management components
*/
export enum CertificateEvents {
CERTIFICATE_ISSUED = 'certificate-issued',
CERTIFICATE_RENEWED = 'certificate-renewed',
CERTIFICATE_FAILED = 'certificate-failed',
CERTIFICATE_EXPIRING = 'certificate-expiring',
CERTIFICATE_APPLIED = 'certificate-applied',
// Events moved from Port80Handler for compatibility
MANAGER_STARTED = 'manager-started',
MANAGER_STOPPED = 'manager-stopped',
}
/**
* Port80Handler-specific events including certificate-related ones
* @deprecated Use CertificateEvents and HttpEvents instead
*/
export enum Port80HandlerEvents {
CERTIFICATE_ISSUED = 'certificate-issued',
CERTIFICATE_RENEWED = 'certificate-renewed',
CERTIFICATE_FAILED = 'certificate-failed',
CERTIFICATE_EXPIRING = 'certificate-expiring',
MANAGER_STARTED = 'manager-started',
MANAGER_STOPPED = 'manager-stopped',
REQUEST_FORWARDED = 'request-forwarded',
}
/**
* Certificate provider events
*/
export enum CertProvisionerEvents {
CERTIFICATE_ISSUED = 'certificate',
CERTIFICATE_RENEWED = 'certificate',
CERTIFICATE_FAILED = 'certificate-failed'
}

View File

@ -1,75 +0,0 @@
/**
* Certificate management module for SmartProxy
* Provides certificate provisioning, storage, and management capabilities
*/
// Certificate types and models
export * from './models/certificate-types.js';
// Certificate events
export * from './events/certificate-events.js';
// Certificate providers
export * from './providers/cert-provisioner.js';
// ACME related exports
export * from './acme/acme-factory.js';
export * from './acme/challenge-handler.js';
// Certificate utilities
export * from './utils/certificate-helpers.js';
// Certificate storage
export * from './storage/file-storage.js';
// Convenience function to create a certificate provisioner with common settings
import { CertProvisioner } from './providers/cert-provisioner.js';
import type { TCertProvisionObject } from './providers/cert-provisioner.js';
import { buildPort80Handler } from './acme/acme-factory.js';
import type { IAcmeOptions, IRouteForwardConfig } from './models/certificate-types.js';
import type { IRouteConfig } from '../proxies/smart-proxy/models/route-types.js';
/**
* Interface for NetworkProxyBridge used by CertProvisioner
*/
interface ICertNetworkProxyBridge {
applyExternalCertificate(certData: any): void;
}
/**
* Creates a complete certificate provisioning system with default settings
* @param routeConfigs Route configurations that may need certificates
* @param acmeOptions ACME options for certificate provisioning
* @param networkProxyBridge Bridge to apply certificates to network proxy
* @param certProvider Optional custom certificate provider
* @returns Configured CertProvisioner
*/
export function createCertificateProvisioner(
routeConfigs: IRouteConfig[],
acmeOptions: IAcmeOptions,
networkProxyBridge: ICertNetworkProxyBridge,
certProvider?: (domain: string) => Promise<TCertProvisionObject>
): CertProvisioner {
// Build the Port80Handler for ACME challenges
const port80Handler = buildPort80Handler(acmeOptions);
// Extract ACME-specific configuration
const {
renewThresholdDays = 30,
renewCheckIntervalHours = 24,
autoRenew = true,
routeForwards = []
} = acmeOptions;
// Create and return the certificate provisioner
return new CertProvisioner(
routeConfigs,
port80Handler,
networkProxyBridge,
certProvider,
renewThresholdDays,
renewCheckIntervalHours,
autoRenew,
routeForwards
);
}

View File

@ -1,109 +0,0 @@
import * as plugins from '../../plugins.js';
import type { IRouteConfig } from '../../proxies/smart-proxy/models/route-types.js';
/**
* Certificate data structure containing all necessary information
* about a certificate
*/
export interface ICertificateData {
domain: string;
certificate: string;
privateKey: string;
expiryDate: Date;
// Optional source and renewal information for event emissions
source?: 'static' | 'http01' | 'dns01';
isRenewal?: boolean;
// Reference to the route that requested this certificate (if available)
routeReference?: {
routeId?: string;
routeName?: string;
};
}
/**
* Certificates pair (private and public keys)
*/
export interface ICertificates {
privateKey: string;
publicKey: string;
}
/**
* Certificate failure payload type
*/
export interface ICertificateFailure {
domain: string;
error: string;
isRenewal: boolean;
routeReference?: {
routeId?: string;
routeName?: string;
};
}
/**
* Certificate expiry payload type
*/
export interface ICertificateExpiring {
domain: string;
expiryDate: Date;
daysRemaining: number;
routeReference?: {
routeId?: string;
routeName?: string;
};
}
/**
* Route-specific forwarding configuration for ACME challenges
*/
export interface IRouteForwardConfig {
domain: string;
target: {
host: string;
port: number;
};
sslRedirect?: boolean;
}
/**
* Domain configuration options for Port80Handler
*
* This is used internally by the Port80Handler to manage domains
* but will eventually be replaced with route-based 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?: {
ip: string;
port: number;
}; // forwards all http requests to that target
acmeForward?: {
ip: string;
port: number;
}; // forwards letsencrypt requests to this config
routeReference?: {
routeId?: string;
routeName?: string;
};
}
/**
* Unified ACME configuration options used across proxies and handlers
*/
export interface IAcmeOptions {
accountEmail?: string; // Email for Let's Encrypt account
enabled?: boolean; // Whether ACME is enabled
port?: number; // Port to listen on for ACME challenges (default: 80)
useProduction?: boolean; // Use production environment (default: staging)
httpsRedirectPort?: number; // Port to redirect HTTP requests to HTTPS (default: 443)
renewThresholdDays?: number; // Days before expiry to renew certificates
renewCheckIntervalHours?: number; // How often to check for renewals (in hours)
autoRenew?: boolean; // Whether to automatically renew certificates
certificateStore?: string; // Directory to store certificates
skipConfiguredCerts?: boolean; // Skip domains with existing certificates
routeForwards?: IRouteForwardConfig[]; // Route-specific forwarding configs
}

View File

@ -1,519 +0,0 @@
import * as plugins from '../../plugins.js';
import type { IRouteConfig } from '../../proxies/smart-proxy/models/route-types.js';
import type { ICertificateData, IRouteForwardConfig, IDomainOptions } from '../models/certificate-types.js';
import { Port80HandlerEvents, CertProvisionerEvents } from '../events/certificate-events.js';
import { Port80Handler } from '../../http/port80/port80-handler.js';
// Interface for NetworkProxyBridge
interface INetworkProxyBridge {
applyExternalCertificate(certData: ICertificateData): void;
}
/**
* Type for static certificate provisioning
*/
export type TCertProvisionObject = plugins.tsclass.network.ICert | 'http01' | 'dns01';
/**
* Interface for routes that need certificates
*/
interface ICertRoute {
domain: string;
route: IRouteConfig;
tlsMode: 'terminate' | 'terminate-and-reencrypt';
}
/**
* CertProvisioner manages certificate provisioning and renewal workflows,
* unifying static certificates and HTTP-01 challenges via Port80Handler.
*
* This class directly works with route configurations instead of converting to domain configs.
*/
export class CertProvisioner extends plugins.EventEmitter {
private routeConfigs: IRouteConfig[];
private certRoutes: ICertRoute[] = [];
private port80Handler: Port80Handler;
private networkProxyBridge: INetworkProxyBridge;
private certProvisionFunction?: (domain: string) => Promise<TCertProvisionObject>;
private routeForwards: IRouteForwardConfig[];
private renewThresholdDays: number;
private renewCheckIntervalHours: number;
private autoRenew: boolean;
private renewManager?: plugins.taskbuffer.TaskManager;
// Track provisioning type per domain
private provisionMap: Map<string, { type: 'http01' | 'dns01' | 'static', routeRef?: ICertRoute }>;
/**
* Extract routes that need certificates
* @param routes Route configurations
*/
private extractCertificateRoutesFromRoutes(routes: IRouteConfig[]): ICertRoute[] {
const certRoutes: ICertRoute[] = [];
// Process all HTTPS routes that need certificates
for (const route of routes) {
// Only process routes with TLS termination that need certificates
if (route.action.type === 'forward' &&
route.action.tls &&
(route.action.tls.mode === 'terminate' || route.action.tls.mode === 'terminate-and-reencrypt') &&
route.match.domains) {
// Extract domains from the route
const domains = Array.isArray(route.match.domains)
? route.match.domains
: [route.match.domains];
// For each domain in the route, create a certRoute entry
for (const domain of domains) {
// Skip wildcard domains that can't use ACME unless we have a certProvider
if (domain.includes('*') && (!this.certProvisionFunction || this.certProvisionFunction.length === 0)) {
console.warn(`Skipping wildcard domain that requires a certProvisionFunction: ${domain}`);
continue;
}
certRoutes.push({
domain,
route,
tlsMode: route.action.tls.mode
});
}
}
}
return certRoutes;
}
/**
* Constructor for CertProvisioner
*
* @param routeConfigs Array of route configurations
* @param port80Handler HTTP-01 challenge handler instance
* @param networkProxyBridge Bridge for applying external certificates
* @param certProvider Optional callback returning a static cert or 'http01'
* @param renewThresholdDays Days before expiry to trigger renewals
* @param renewCheckIntervalHours Interval in hours to check for renewals
* @param autoRenew Whether to automatically schedule renewals
* @param routeForwards Route-specific forwarding configs for ACME challenges
*/
constructor(
routeConfigs: IRouteConfig[],
port80Handler: Port80Handler,
networkProxyBridge: INetworkProxyBridge,
certProvider?: (domain: string) => Promise<TCertProvisionObject>,
renewThresholdDays: number = 30,
renewCheckIntervalHours: number = 24,
autoRenew: boolean = true,
routeForwards: IRouteForwardConfig[] = []
) {
super();
this.routeConfigs = routeConfigs;
this.port80Handler = port80Handler;
this.networkProxyBridge = networkProxyBridge;
this.certProvisionFunction = certProvider;
this.renewThresholdDays = renewThresholdDays;
this.renewCheckIntervalHours = renewCheckIntervalHours;
this.autoRenew = autoRenew;
this.provisionMap = new Map();
this.routeForwards = routeForwards;
// Extract certificate routes during instantiation
this.certRoutes = this.extractCertificateRoutesFromRoutes(routeConfigs);
}
/**
* Start initial provisioning and schedule renewals.
*/
public async start(): Promise<void> {
// Subscribe to Port80Handler certificate events
this.setupEventSubscriptions();
// Apply route forwarding for ACME challenges
this.setupForwardingConfigs();
// Initial provisioning for all domains in routes
await this.provisionAllCertificates();
// Schedule renewals if enabled
if (this.autoRenew) {
this.scheduleRenewals();
}
}
/**
* Set up event subscriptions for certificate events
*/
private setupEventSubscriptions(): void {
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_ISSUED, (data: ICertificateData) => {
// Add route reference if we have it
const routeRef = this.findRouteForDomain(data.domain);
const enhancedData: ICertificateData = {
...data,
source: 'http01',
isRenewal: false,
routeReference: routeRef ? {
routeId: routeRef.route.name,
routeName: routeRef.route.name
} : undefined
};
this.emit(CertProvisionerEvents.CERTIFICATE_ISSUED, enhancedData);
});
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_RENEWED, (data: ICertificateData) => {
// Add route reference if we have it
const routeRef = this.findRouteForDomain(data.domain);
const enhancedData: ICertificateData = {
...data,
source: 'http01',
isRenewal: true,
routeReference: routeRef ? {
routeId: routeRef.route.name,
routeName: routeRef.route.name
} : undefined
};
this.emit(CertProvisionerEvents.CERTIFICATE_RENEWED, enhancedData);
});
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_FAILED, (error) => {
this.emit(CertProvisionerEvents.CERTIFICATE_FAILED, error);
});
}
/**
* Find a route for a given domain
*/
private findRouteForDomain(domain: string): ICertRoute | undefined {
return this.certRoutes.find(certRoute => certRoute.domain === domain);
}
/**
* Set up forwarding configurations for the Port80Handler
*/
private setupForwardingConfigs(): void {
for (const config of this.routeForwards) {
const domainOptions: IDomainOptions = {
domainName: config.domain,
sslRedirect: config.sslRedirect || false,
acmeMaintenance: false,
forward: config.target ? {
ip: config.target.host,
port: config.target.port
} : undefined
};
this.port80Handler.addDomain(domainOptions);
}
}
/**
* Provision certificates for all routes that need them
*/
private async provisionAllCertificates(): Promise<void> {
for (const certRoute of this.certRoutes) {
await this.provisionCertificateForRoute(certRoute);
}
}
/**
* Provision a certificate for a route
*/
private async provisionCertificateForRoute(certRoute: ICertRoute): Promise<void> {
const { domain, route } = certRoute;
const isWildcard = domain.includes('*');
let provision: TCertProvisionObject = 'http01';
// Try to get a certificate from the provision function
if (this.certProvisionFunction) {
try {
provision = await this.certProvisionFunction(domain);
} catch (err) {
console.error(`certProvider error for ${domain} on route ${route.name || 'unnamed'}:`, err);
}
} else if (isWildcard) {
// No certProvider: cannot handle wildcard without DNS-01 support
console.warn(`Skipping wildcard domain without certProvisionFunction: ${domain}`);
return;
}
// Store the route reference with the provision type
this.provisionMap.set(domain, {
type: provision === 'http01' || provision === 'dns01' ? provision : 'static',
routeRef: certRoute
});
// Handle different provisioning methods
if (provision === 'http01') {
if (isWildcard) {
console.warn(`Skipping HTTP-01 for wildcard domain: ${domain}`);
return;
}
this.port80Handler.addDomain({
domainName: domain,
sslRedirect: true,
acmeMaintenance: true,
routeReference: {
routeId: route.name || domain,
routeName: route.name
}
});
} else if (provision === 'dns01') {
// DNS-01 challenges would be handled by the certProvisionFunction
// DNS-01 handling would go here if implemented
console.log(`DNS-01 challenge type set for ${domain}`);
} else {
// Static certificate (e.g., DNS-01 provisioned or user-provided)
const certObj = provision as plugins.tsclass.network.ICert;
const certData: ICertificateData = {
domain: certObj.domainName,
certificate: certObj.publicKey,
privateKey: certObj.privateKey,
expiryDate: new Date(certObj.validUntil),
source: 'static',
isRenewal: false,
routeReference: {
routeId: route.name || domain,
routeName: route.name
}
};
this.networkProxyBridge.applyExternalCertificate(certData);
this.emit(CertProvisionerEvents.CERTIFICATE_ISSUED, certData);
}
}
/**
* Schedule certificate renewals using a task manager
*/
private scheduleRenewals(): void {
this.renewManager = new plugins.taskbuffer.TaskManager();
const renewTask = new plugins.taskbuffer.Task({
name: 'CertificateRenewals',
taskFunction: async () => await this.performRenewals()
});
const hours = this.renewCheckIntervalHours;
const cronExpr = `0 0 */${hours} * * *`;
this.renewManager.addAndScheduleTask(renewTask, cronExpr);
this.renewManager.start();
}
/**
* Perform renewals for all domains that need it
*/
private async performRenewals(): Promise<void> {
for (const [domain, info] of this.provisionMap.entries()) {
// Skip wildcard domains for HTTP-01 challenges
if (domain.includes('*') && info.type === 'http01') continue;
try {
await this.renewCertificateForDomain(domain, info.type, info.routeRef);
} catch (err) {
console.error(`Renewal error for ${domain}:`, err);
}
}
}
/**
* Renew a certificate for a specific domain
* @param domain Domain to renew
* @param provisionType Type of provisioning for this domain
* @param certRoute The route reference for this domain
*/
private async renewCertificateForDomain(
domain: string,
provisionType: 'http01' | 'dns01' | 'static',
certRoute?: ICertRoute
): Promise<void> {
if (provisionType === 'http01') {
await this.port80Handler.renewCertificate(domain);
} else if ((provisionType === 'static' || provisionType === 'dns01') && this.certProvisionFunction) {
const provision = await this.certProvisionFunction(domain);
if (provision !== 'http01' && provision !== 'dns01') {
const certObj = provision as plugins.tsclass.network.ICert;
const routeRef = certRoute?.route;
const certData: ICertificateData = {
domain: certObj.domainName,
certificate: certObj.publicKey,
privateKey: certObj.privateKey,
expiryDate: new Date(certObj.validUntil),
source: 'static',
isRenewal: true,
routeReference: routeRef ? {
routeId: routeRef.name || domain,
routeName: routeRef.name
} : undefined
};
this.networkProxyBridge.applyExternalCertificate(certData);
this.emit(CertProvisionerEvents.CERTIFICATE_RENEWED, certData);
}
}
}
/**
* Stop all scheduled renewal tasks.
*/
public async stop(): Promise<void> {
if (this.renewManager) {
this.renewManager.stop();
}
}
/**
* Request a certificate on-demand for the given domain.
* This will look for a matching route configuration and provision accordingly.
*
* @param domain Domain name to provision
*/
public async requestCertificate(domain: string): Promise<void> {
const isWildcard = domain.includes('*');
// Find matching route
const certRoute = this.findRouteForDomain(domain);
// Determine provisioning method
let provision: TCertProvisionObject = 'http01';
if (this.certProvisionFunction) {
provision = await this.certProvisionFunction(domain);
} else if (isWildcard) {
// Cannot perform HTTP-01 on wildcard without certProvider
throw new Error(`Cannot request certificate for wildcard domain without certProvisionFunction: ${domain}`);
}
if (provision === 'http01') {
if (isWildcard) {
throw new Error(`Cannot request HTTP-01 certificate for wildcard domain: ${domain}`);
}
await this.port80Handler.renewCertificate(domain);
} else if (provision === 'dns01') {
// DNS-01 challenges would be handled by external mechanisms
console.log(`DNS-01 challenge requested for ${domain}`);
} else {
// Static certificate (e.g., DNS-01 provisioned) supports wildcards
const certObj = provision as plugins.tsclass.network.ICert;
const certData: ICertificateData = {
domain: certObj.domainName,
certificate: certObj.publicKey,
privateKey: certObj.privateKey,
expiryDate: new Date(certObj.validUntil),
source: 'static',
isRenewal: false,
routeReference: certRoute ? {
routeId: certRoute.route.name || domain,
routeName: certRoute.route.name
} : undefined
};
this.networkProxyBridge.applyExternalCertificate(certData);
this.emit(CertProvisionerEvents.CERTIFICATE_ISSUED, certData);
}
}
/**
* Add a new domain for certificate provisioning
*
* @param domain Domain to add
* @param options Domain configuration options
*/
public async addDomain(domain: string, options?: {
sslRedirect?: boolean;
acmeMaintenance?: boolean;
routeId?: string;
routeName?: string;
}): Promise<void> {
const domainOptions: IDomainOptions = {
domainName: domain,
sslRedirect: options?.sslRedirect ?? true,
acmeMaintenance: options?.acmeMaintenance ?? true,
routeReference: {
routeId: options?.routeId,
routeName: options?.routeName
}
};
this.port80Handler.addDomain(domainOptions);
// Find matching route or create a generic one
const existingRoute = this.findRouteForDomain(domain);
if (existingRoute) {
await this.provisionCertificateForRoute(existingRoute);
} else {
// We don't have a route, just provision the domain
const isWildcard = domain.includes('*');
let provision: TCertProvisionObject = 'http01';
if (this.certProvisionFunction) {
provision = await this.certProvisionFunction(domain);
} else if (isWildcard) {
throw new Error(`Cannot request certificate for wildcard domain without certProvisionFunction: ${domain}`);
}
this.provisionMap.set(domain, {
type: provision === 'http01' || provision === 'dns01' ? provision : 'static'
});
if (provision !== 'http01' && provision !== 'dns01') {
const certObj = provision as plugins.tsclass.network.ICert;
const certData: ICertificateData = {
domain: certObj.domainName,
certificate: certObj.publicKey,
privateKey: certObj.privateKey,
expiryDate: new Date(certObj.validUntil),
source: 'static',
isRenewal: false,
routeReference: {
routeId: options?.routeId,
routeName: options?.routeName
}
};
this.networkProxyBridge.applyExternalCertificate(certData);
this.emit(CertProvisionerEvents.CERTIFICATE_ISSUED, certData);
}
}
}
/**
* Update routes with new configurations
* This replaces all existing routes with new ones and re-provisions certificates as needed
*
* @param newRoutes New route configurations to use
*/
public async updateRoutes(newRoutes: IRouteConfig[]): Promise<void> {
// Store the new route configs
this.routeConfigs = newRoutes;
// Extract new certificate routes
const newCertRoutes = this.extractCertificateRoutesFromRoutes(newRoutes);
// Find domains that no longer need certificates
const oldDomains = new Set(this.certRoutes.map(r => r.domain));
const newDomains = new Set(newCertRoutes.map(r => r.domain));
// Domains to remove
const domainsToRemove = [...oldDomains].filter(d => !newDomains.has(d));
// Remove obsolete domains from provision map
for (const domain of domainsToRemove) {
this.provisionMap.delete(domain);
}
// Update the cert routes
this.certRoutes = newCertRoutes;
// Provision certificates for new routes
for (const certRoute of newCertRoutes) {
if (!oldDomains.has(certRoute.domain)) {
await this.provisionCertificateForRoute(certRoute);
}
}
}
}
// Type alias for backward compatibility
export type TSmartProxyCertProvisionObject = TCertProvisionObject;

View File

@ -1,3 +0,0 @@
/**
* Certificate providers
*/

View File

@ -1,234 +0,0 @@
import * as fs from 'fs';
import * as path from 'path';
import * as plugins from '../../plugins.js';
import type { ICertificateData, ICertificates } from '../models/certificate-types.js';
import { ensureCertificateDirectory } from '../utils/certificate-helpers.js';
/**
* FileStorage provides file system storage for certificates
*/
export class FileStorage {
private storageDir: string;
/**
* Creates a new file storage provider
* @param storageDir Directory to store certificates
*/
constructor(storageDir: string) {
this.storageDir = path.resolve(storageDir);
ensureCertificateDirectory(this.storageDir);
}
/**
* Save a certificate to the file system
* @param domain Domain name
* @param certData Certificate data to save
*/
public async saveCertificate(domain: string, certData: ICertificateData): Promise<void> {
const sanitizedDomain = this.sanitizeDomain(domain);
const certDir = path.join(this.storageDir, sanitizedDomain);
ensureCertificateDirectory(certDir);
const certPath = path.join(certDir, 'fullchain.pem');
const keyPath = path.join(certDir, 'privkey.pem');
const metaPath = path.join(certDir, 'metadata.json');
// Write certificate and private key
await fs.promises.writeFile(certPath, certData.certificate, 'utf8');
await fs.promises.writeFile(keyPath, certData.privateKey, 'utf8');
// Write metadata
const metadata = {
domain: certData.domain,
expiryDate: certData.expiryDate.toISOString(),
source: certData.source || 'unknown',
issuedAt: new Date().toISOString()
};
await fs.promises.writeFile(
metaPath,
JSON.stringify(metadata, null, 2),
'utf8'
);
}
/**
* Load a certificate from the file system
* @param domain Domain name
* @returns Certificate data if found, null otherwise
*/
public async loadCertificate(domain: string): Promise<ICertificateData | null> {
const sanitizedDomain = this.sanitizeDomain(domain);
const certDir = path.join(this.storageDir, sanitizedDomain);
if (!fs.existsSync(certDir)) {
return null;
}
const certPath = path.join(certDir, 'fullchain.pem');
const keyPath = path.join(certDir, 'privkey.pem');
const metaPath = path.join(certDir, 'metadata.json');
try {
// Check if all required files exist
if (!fs.existsSync(certPath) || !fs.existsSync(keyPath)) {
return null;
}
// Read certificate and private key
const certificate = await fs.promises.readFile(certPath, 'utf8');
const privateKey = await fs.promises.readFile(keyPath, 'utf8');
// Try to read metadata if available
let expiryDate = new Date();
let source: 'static' | 'http01' | 'dns01' | undefined;
if (fs.existsSync(metaPath)) {
const metaContent = await fs.promises.readFile(metaPath, 'utf8');
const metadata = JSON.parse(metaContent);
if (metadata.expiryDate) {
expiryDate = new Date(metadata.expiryDate);
}
if (metadata.source) {
source = metadata.source as 'static' | 'http01' | 'dns01';
}
}
return {
domain,
certificate,
privateKey,
expiryDate,
source
};
} catch (error) {
console.error(`Error loading certificate for ${domain}:`, error);
return null;
}
}
/**
* Delete a certificate from the file system
* @param domain Domain name
*/
public async deleteCertificate(domain: string): Promise<boolean> {
const sanitizedDomain = this.sanitizeDomain(domain);
const certDir = path.join(this.storageDir, sanitizedDomain);
if (!fs.existsSync(certDir)) {
return false;
}
try {
// Recursively delete the certificate directory
await this.deleteDirectory(certDir);
return true;
} catch (error) {
console.error(`Error deleting certificate for ${domain}:`, error);
return false;
}
}
/**
* List all domains with stored certificates
* @returns Array of domain names
*/
public async listCertificates(): Promise<string[]> {
try {
const entries = await fs.promises.readdir(this.storageDir, { withFileTypes: true });
return entries
.filter(entry => entry.isDirectory())
.map(entry => entry.name);
} catch (error) {
console.error('Error listing certificates:', error);
return [];
}
}
/**
* Check if a certificate is expiring soon
* @param domain Domain name
* @param thresholdDays Days threshold to consider expiring
* @returns Information about expiring certificate or null
*/
public async isExpiringSoon(
domain: string,
thresholdDays: number = 30
): Promise<{ domain: string; expiryDate: Date; daysRemaining: number } | null> {
const certData = await this.loadCertificate(domain);
if (!certData) {
return null;
}
const now = new Date();
const expiryDate = certData.expiryDate;
const timeRemaining = expiryDate.getTime() - now.getTime();
const daysRemaining = Math.floor(timeRemaining / (1000 * 60 * 60 * 24));
if (daysRemaining <= thresholdDays) {
return {
domain,
expiryDate,
daysRemaining
};
}
return null;
}
/**
* Check all certificates for expiration
* @param thresholdDays Days threshold to consider expiring
* @returns List of expiring certificates
*/
public async getExpiringCertificates(
thresholdDays: number = 30
): Promise<Array<{ domain: string; expiryDate: Date; daysRemaining: number }>> {
const domains = await this.listCertificates();
const expiringCerts = [];
for (const domain of domains) {
const expiring = await this.isExpiringSoon(domain, thresholdDays);
if (expiring) {
expiringCerts.push(expiring);
}
}
return expiringCerts;
}
/**
* Delete a directory recursively
* @param directoryPath Directory to delete
*/
private async deleteDirectory(directoryPath: string): Promise<void> {
if (fs.existsSync(directoryPath)) {
const entries = await fs.promises.readdir(directoryPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(directoryPath, entry.name);
if (entry.isDirectory()) {
await this.deleteDirectory(fullPath);
} else {
await fs.promises.unlink(fullPath);
}
}
await fs.promises.rmdir(directoryPath);
}
}
/**
* Sanitize a domain name for use as a directory name
* @param domain Domain name
* @returns Sanitized domain name
*/
private sanitizeDomain(domain: string): string {
// Replace wildcard and any invalid filesystem characters
return domain.replace(/\*/g, '_wildcard_').replace(/[/\\:*?"<>|]/g, '_');
}
}

View File

@ -1,3 +0,0 @@
/**
* Certificate storage mechanisms
*/

View File

@ -1,50 +0,0 @@
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
import type { ICertificates } from '../models/certificate-types.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
/**
* Loads the default SSL certificates from the assets directory
* @returns The certificate key pair
*/
export function loadDefaultCertificates(): ICertificates {
try {
// Need to adjust path from /ts/certificate/utils to /assets/certs
const certPath = path.join(__dirname, '..', '..', '..', 'assets', 'certs');
const privateKey = fs.readFileSync(path.join(certPath, 'key.pem'), 'utf8');
const publicKey = fs.readFileSync(path.join(certPath, 'cert.pem'), 'utf8');
if (!privateKey || !publicKey) {
throw new Error('Failed to load default certificates');
}
return {
privateKey,
publicKey
};
} catch (error) {
console.error('Error loading default certificates:', error);
throw error;
}
}
/**
* Checks if a certificate file exists at the specified path
* @param certPath Path to check for certificate
* @returns True if the certificate exists, false otherwise
*/
export function certificateExists(certPath: string): boolean {
return fs.existsSync(certPath);
}
/**
* Ensures the certificate directory exists
* @param dirPath Path to the certificate directory
*/
export function ensureCertificateDirectory(dirPath: string): void {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
}

View File

@ -1,4 +1,4 @@
import type { Port80Handler } from '../http/port80/port80-handler.js';
// Port80Handler removed - use SmartCertManager instead
import { Port80HandlerEvents } from './types.js';
import type { ICertificateData, ICertificateFailure, ICertificateExpiring } from './types.js';
@ -16,7 +16,7 @@ export interface Port80HandlerSubscribers {
* Subscribes to Port80Handler events based on provided callbacks
*/
export function subscribeToPort80Handler(
handler: Port80Handler,
handler: any,
subscribers: Port80HandlerSubscribers
): void {
if (subscribers.onCertificateIssued) {

View File

@ -34,7 +34,7 @@ export interface ICertificateData {
}
/**
* Events emitted by the Port80Handler
* @deprecated Events emitted by the Port80Handler - use SmartCertManager instead
*/
export enum Port80HandlerEvents {
CERTIFICATE_ISSUED = 'certificate-issued',

View File

@ -1,34 +1,25 @@
import type { Port80Handler } from '../../http/port80/port80-handler.js';
// Port80Handler has been removed - use SmartCertManager instead
import { Port80HandlerEvents } from '../models/common-types.js';
import type { ICertificateData, ICertificateFailure, ICertificateExpiring } from '../models/common-types.js';
// Re-export for backward compatibility
export { Port80HandlerEvents };
/**
* Subscribers callback definitions for Port80Handler events
* @deprecated Use SmartCertManager instead
*/
export interface IPort80HandlerSubscribers {
onCertificateIssued?: (data: ICertificateData) => void;
onCertificateRenewed?: (data: ICertificateData) => void;
onCertificateFailed?: (data: ICertificateFailure) => void;
onCertificateExpiring?: (data: ICertificateExpiring) => void;
onCertificateIssued?: (data: any) => void;
onCertificateRenewed?: (data: any) => void;
onCertificateFailed?: (data: any) => void;
onCertificateExpiring?: (data: any) => void;
}
/**
* Subscribes to Port80Handler events based on provided callbacks
* @deprecated Use SmartCertManager instead
*/
export function subscribeToPort80Handler(
handler: Port80Handler,
handler: any,
subscribers: IPort80HandlerSubscribers
): void {
if (subscribers.onCertificateIssued) {
handler.on(Port80HandlerEvents.CERTIFICATE_ISSUED, subscribers.onCertificateIssued);
}
if (subscribers.onCertificateRenewed) {
handler.on(Port80HandlerEvents.CERTIFICATE_RENEWED, subscribers.onCertificateRenewed);
}
if (subscribers.onCertificateFailed) {
handler.on(Port80HandlerEvents.CERTIFICATE_FAILED, subscribers.onCertificateFailed);
}
if (subscribers.onCertificateExpiring) {
handler.on(Port80HandlerEvents.CERTIFICATE_EXPIRING, subscribers.onCertificateExpiring);
}
console.warn('subscribeToPort80Handler is deprecated - use SmartCertManager instead');
}

View File

@ -1,8 +1,12 @@
import * as plugins from '../../plugins.js';
import type {
IDomainOptions,
IAcmeOptions
} from '../../certificate/models/certificate-types.js';
// Certificate types have been removed - use SmartCertManager instead
export interface IDomainOptions {
domainName: string;
sslRedirect: boolean;
acmeMaintenance: boolean;
forward?: { ip: string; port: number };
acmeForward?: { ip: string; port: number };
}
/**
* HTTP-specific event types

View File

@ -1,169 +0,0 @@
/**
* Type definitions for SmartAcme interfaces used by ChallengeResponder
* These reflect the actual SmartAcme API based on the documentation
*
* Also includes route-based interfaces for Port80Handler to extract domains
* that need certificate management from route configurations.
*/
import * as plugins from '../../plugins.js';
import type { IRouteConfig } from '../../proxies/smart-proxy/models/route-types.js';
/**
* Structure for SmartAcme certificate result
*/
export interface ISmartAcmeCert {
id?: string;
domainName: string;
created?: number | Date | string;
privateKey: string;
publicKey: string;
csr?: string;
validUntil: number | Date | string;
}
/**
* Structure for SmartAcme options
*/
export interface ISmartAcmeOptions {
accountEmail: string;
certManager: ICertManager;
environment: 'production' | 'integration';
challengeHandlers: IChallengeHandler<any>[];
challengePriority?: string[];
retryOptions?: {
retries?: number;
factor?: number;
minTimeoutMs?: number;
maxTimeoutMs?: number;
};
}
/**
* Interface for certificate manager
*/
export interface ICertManager {
init(): Promise<void>;
get(domainName: string): Promise<ISmartAcmeCert | null>;
put(cert: ISmartAcmeCert): Promise<ISmartAcmeCert>;
delete(domainName: string): Promise<void>;
close?(): Promise<void>;
}
/**
* Interface for challenge handler
*/
export interface IChallengeHandler<T> {
getSupportedTypes(): string[];
prepare(ch: T): Promise<void>;
verify?(ch: T): Promise<void>;
cleanup(ch: T): Promise<void>;
checkWetherDomainIsSupported(domain: string): Promise<boolean>;
}
/**
* HTTP-01 challenge type
*/
export interface IHttp01Challenge {
type: string; // 'http-01'
token: string;
keyAuthorization: string;
webPath: string;
}
/**
* HTTP-01 Memory Handler Interface
*/
export interface IHttp01MemoryHandler extends IChallengeHandler<IHttp01Challenge> {
handleRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse, next?: () => void): void;
}
/**
* SmartAcme main class interface
*/
export interface ISmartAcme {
start(): Promise<void>;
stop(): Promise<void>;
getCertificateForDomain(domain: string): Promise<ISmartAcmeCert>;
on?(event: string, listener: (data: any) => void): void;
eventEmitter?: plugins.EventEmitter;
}
/**
* Port80Handler route options
*/
export interface IPort80RouteOptions {
// The domain for the certificate
domain: string;
// Whether to redirect HTTP to HTTPS
sslRedirect: boolean;
// Whether to enable ACME certificate management
acmeMaintenance: boolean;
// Optional target for forwarding HTTP requests
forward?: {
ip: string;
port: number;
};
// Optional target for forwarding ACME challenge requests
acmeForward?: {
ip: string;
port: number;
};
// Reference to the route that requested this certificate
routeReference?: {
routeId?: string;
routeName?: string;
};
}
/**
* Extract domains that need certificate management from routes
* @param routes Route configurations to extract domains from
* @returns Array of Port80RouteOptions for each domain
*/
export function extractPort80RoutesFromRoutes(routes: IRouteConfig[]): IPort80RouteOptions[] {
const result: IPort80RouteOptions[] = [];
for (const route of routes) {
// Skip routes that don't have domains or TLS configuration
if (!route.match.domains || !route.action.tls) continue;
// Skip routes that don't terminate TLS
if (route.action.tls.mode !== 'terminate' && route.action.tls.mode !== 'terminate-and-reencrypt') continue;
// Only routes with automatic certificates need ACME
if (route.action.tls.certificate !== 'auto') continue;
// Get domains from route
const domains = Array.isArray(route.match.domains)
? route.match.domains
: [route.match.domains];
// Create Port80RouteOptions for each domain
for (const domain of domains) {
// Skip wildcards (we can't get certificates for them)
if (domain.includes('*')) continue;
// Create Port80RouteOptions
const options: IPort80RouteOptions = {
domain,
sslRedirect: true, // Default to true for HTTPS routes
acmeMaintenance: true, // Default to true for auto certificates
// Add route reference
routeReference: {
routeName: route.name
}
};
// Add domain to result
result.push(options);
}
}
return result;
}

View File

@ -1,246 +0,0 @@
import * as plugins from '../../plugins.js';
import { IncomingMessage, ServerResponse } from 'http';
import {
CertificateEvents
} from '../../certificate/events/certificate-events.js';
import type {
ICertificateData,
ICertificateFailure,
ICertificateExpiring
} from '../../certificate/models/certificate-types.js';
import type {
ISmartAcme,
ISmartAcmeCert,
ISmartAcmeOptions,
IHttp01MemoryHandler
} from './acme-interfaces.js';
/**
* ChallengeResponder handles ACME HTTP-01 challenges by leveraging SmartAcme
* It acts as a bridge between the HTTP server and the ACME challenge verification process
*/
export class ChallengeResponder extends plugins.EventEmitter {
private smartAcme: ISmartAcme | null = null;
private http01Handler: IHttp01MemoryHandler | null = null;
/**
* Creates a new challenge responder
* @param useProduction Whether to use production ACME servers
* @param email Account email for ACME
* @param certificateStore Directory to store certificates
*/
constructor(
private readonly useProduction: boolean = false,
private readonly email: string = 'admin@example.com',
private readonly certificateStore: string = './certs'
) {
super();
}
/**
* Initialize the ACME client
*/
public async initialize(): Promise<void> {
try {
// Create the HTTP-01 memory handler from SmartACME
this.http01Handler = new plugins.smartacme.handlers.Http01MemoryHandler();
// Ensure certificate store directory exists
await this.ensureCertificateStore();
// Create a MemoryCertManager for certificate storage
const certManager = new plugins.smartacme.certmanagers.MemoryCertManager();
// Initialize the SmartACME client with appropriate options
this.smartAcme = new plugins.smartacme.SmartAcme({
accountEmail: this.email,
certManager: certManager,
environment: this.useProduction ? 'production' : 'integration',
challengeHandlers: [this.http01Handler],
challengePriority: ['http-01']
});
// Set up event forwarding from SmartAcme
this.setupEventListeners();
// Start the SmartACME client
await this.smartAcme.start();
console.log('ACME client initialized successfully');
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to initialize ACME client: ${errorMessage}`);
}
}
/**
* Ensure the certificate store directory exists
*/
private async ensureCertificateStore(): Promise<void> {
try {
await plugins.fs.promises.mkdir(this.certificateStore, { recursive: true });
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to create certificate store: ${errorMessage}`);
}
}
/**
* Setup event listeners to forward SmartACME events to our own event emitter
*/
private setupEventListeners(): void {
if (!this.smartAcme) return;
const setupEvents = (emitter: { on: (event: string, listener: (data: any) => void) => void }) => {
// Forward certificate events
emitter.on('certificate', (data: any) => {
const isRenewal = !!data.isRenewal;
const certData: ICertificateData = {
domain: data.domainName || data.domain,
certificate: data.publicKey || data.cert,
privateKey: data.privateKey || data.key,
expiryDate: new Date(data.validUntil || data.expiryDate || Date.now()),
source: 'http01',
isRenewal
};
const eventType = isRenewal
? CertificateEvents.CERTIFICATE_RENEWED
: CertificateEvents.CERTIFICATE_ISSUED;
this.emit(eventType, certData);
});
// Forward error events
emitter.on('error', (error: any) => {
const domain = error.domainName || error.domain || 'unknown';
const failureData: ICertificateFailure = {
domain,
error: error.message || String(error),
isRenewal: !!error.isRenewal
};
this.emit(CertificateEvents.CERTIFICATE_FAILED, failureData);
});
};
// Check for direct event methods on SmartAcme
if (typeof this.smartAcme.on === 'function') {
setupEvents(this.smartAcme as any);
}
// Check for eventEmitter property
else if (this.smartAcme.eventEmitter) {
setupEvents(this.smartAcme.eventEmitter);
}
// If no proper event handling, log a warning
else {
console.warn('SmartAcme instance does not support expected event interface - events may not be forwarded');
}
}
/**
* Handle HTTP request by checking if it's an ACME challenge
* @param req HTTP request object
* @param res HTTP response object
* @returns true if the request was handled, false otherwise
*/
public handleRequest(req: IncomingMessage, res: ServerResponse): boolean {
if (!this.http01Handler) return false;
// Check if this is an ACME challenge request (/.well-known/acme-challenge/*)
const url = req.url || '';
if (url.startsWith('/.well-known/acme-challenge/')) {
try {
// Delegate to the HTTP-01 memory handler, which knows how to serve challenges
this.http01Handler.handleRequest(req, res);
return true;
} catch (error) {
console.error('Error handling ACME challenge:', error);
// If there was an error, send a 404 response
res.writeHead(404);
res.end('Not found');
return true;
}
}
return false;
}
/**
* Request a certificate for a domain
* @param domain Domain name to request a certificate for
* @param isRenewal Whether this is a renewal request
*/
public async requestCertificate(domain: string, isRenewal: boolean = false): Promise<ICertificateData> {
if (!this.smartAcme) {
throw new Error('ACME client not initialized');
}
try {
// Request certificate using SmartACME
const certObj = await this.smartAcme.getCertificateForDomain(domain);
// Convert the certificate object to our CertificateData format
const certData: ICertificateData = {
domain,
certificate: certObj.publicKey,
privateKey: certObj.privateKey,
expiryDate: new Date(certObj.validUntil),
source: 'http01',
isRenewal
};
return certData;
} catch (error) {
// Create failure object
const failure: ICertificateFailure = {
domain,
error: error instanceof Error ? error.message : String(error),
isRenewal
};
// Emit failure event
this.emit(CertificateEvents.CERTIFICATE_FAILED, failure);
// Rethrow with more context
throw new Error(`Failed to ${isRenewal ? 'renew' : 'obtain'} certificate for ${domain}: ${
error instanceof Error ? error.message : String(error)
}`);
}
}
/**
* Check if a certificate is expiring soon and trigger renewal if needed
* @param domain Domain name
* @param certificate Certificate data
* @param thresholdDays Days before expiry to trigger renewal
*/
public checkCertificateExpiry(
domain: string,
certificate: ICertificateData,
thresholdDays: number = 30
): void {
if (!certificate.expiryDate) return;
const now = new Date();
const expiryDate = certificate.expiryDate;
const daysDifference = Math.floor((expiryDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
if (daysDifference <= thresholdDays) {
const expiryInfo: ICertificateExpiring = {
domain,
expiryDate,
daysRemaining: daysDifference
};
this.emit(CertificateEvents.CERTIFICATE_EXPIRING, expiryInfo);
// Automatically attempt renewal if expiring
if (this.smartAcme) {
this.requestCertificate(domain, true).catch(error => {
console.error(`Failed to auto-renew certificate for ${domain}:`, error);
});
}
}
}
}

View File

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

View File

@ -1,728 +0,0 @@
import * as plugins from '../../plugins.js';
import { IncomingMessage, ServerResponse } from 'http';
import { CertificateEvents } from '../../certificate/events/certificate-events.js';
import type {
IDomainOptions, // Kept for backward compatibility
ICertificateData,
ICertificateFailure,
ICertificateExpiring,
IAcmeOptions,
IRouteForwardConfig
} from '../../certificate/models/certificate-types.js';
import {
HttpEvents,
HttpStatus,
HttpError,
CertificateError,
ServerError,
} from '../models/http-types.js';
import type { IDomainCertificate } from '../models/http-types.js';
import { ChallengeResponder } from './challenge-responder.js';
import { extractPort80RoutesFromRoutes } from './acme-interfaces.js';
import type { IPort80RouteOptions } from './acme-interfaces.js';
import type { IRouteConfig } from '../../proxies/smart-proxy/models/route-types.js';
// Re-export for backward compatibility
export {
HttpError as Port80HandlerError,
CertificateError,
ServerError
}
// Port80Handler events enum for backward compatibility
export const Port80HandlerEvents = CertificateEvents;
/**
* Configuration options for the Port80Handler
*/
// Port80Handler options moved to common types
/**
* 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 challengeResponder: ChallengeResponder | null = null;
private server: plugins.http.Server | null = null;
// Renewal scheduling is handled externally by SmartProxy
private isShuttingDown: boolean = false;
private options: Required<IAcmeOptions>;
/**
* Creates a new Port80Handler
* @param options Configuration options
*/
constructor(options: IAcmeOptions = {}) {
super();
this.domainCertificates = new Map<string, IDomainCertificate>();
// Default options
this.options = {
port: options.port ?? 80,
accountEmail: options.accountEmail ?? 'admin@example.com',
useProduction: options.useProduction ?? false, // Safer default: staging
httpsRedirectPort: options.httpsRedirectPort ?? 443,
enabled: options.enabled ?? true, // Enable by default
certificateStore: options.certificateStore ?? './certs',
skipConfiguredCerts: options.skipConfiguredCerts ?? false,
renewThresholdDays: options.renewThresholdDays ?? 30,
renewCheckIntervalHours: options.renewCheckIntervalHours ?? 24,
autoRenew: options.autoRenew ?? true,
routeForwards: options.routeForwards ?? []
};
// Initialize challenge responder
if (this.options.enabled) {
this.challengeResponder = new ChallengeResponder(
this.options.useProduction,
this.options.accountEmail,
this.options.certificateStore
);
// Forward certificate events from the challenge responder
this.challengeResponder.on(CertificateEvents.CERTIFICATE_ISSUED, (data: ICertificateData) => {
this.emit(CertificateEvents.CERTIFICATE_ISSUED, data);
});
this.challengeResponder.on(CertificateEvents.CERTIFICATE_RENEWED, (data: ICertificateData) => {
this.emit(CertificateEvents.CERTIFICATE_RENEWED, data);
});
this.challengeResponder.on(CertificateEvents.CERTIFICATE_FAILED, (error: ICertificateFailure) => {
this.emit(CertificateEvents.CERTIFICATE_FAILED, error);
});
this.challengeResponder.on(CertificateEvents.CERTIFICATE_EXPIRING, (expiry: ICertificateExpiring) => {
this.emit(CertificateEvents.CERTIFICATE_EXPIRING, expiry);
});
}
}
/**
* Starts the HTTP server for ACME challenges
*/
public async start(): Promise<void> {
if (this.server) {
throw new ServerError('Server is already running');
}
if (this.isShuttingDown) {
throw new ServerError('Server is shutting down');
}
// Skip if disabled
if (this.options.enabled === false) {
console.log('Port80Handler is disabled, skipping start');
return;
}
// Initialize the challenge responder if enabled
if (this.options.enabled && this.challengeResponder) {
try {
await this.challengeResponder.initialize();
} catch (error) {
throw new ServerError(`Failed to initialize challenge responder: ${
error instanceof Error ? error.message : String(error)
}`);
}
}
return new Promise((resolve, reject) => {
try {
this.server = plugins.http.createServer((req, res) => this.handleRequest(req, res));
this.server.on('error', (error: NodeJS.ErrnoException) => {
if (error.code === 'EACCES') {
reject(new ServerError(`Permission denied to bind to port ${this.options.port}. Try running with elevated privileges or use a port > 1024.`, error.code));
} else if (error.code === 'EADDRINUSE') {
reject(new ServerError(`Port ${this.options.port} is already in use.`, error.code));
} else {
reject(new ServerError(error.message, error.code));
}
});
this.server.listen(this.options.port, () => {
console.log(`Port80Handler is listening on port ${this.options.port}`);
this.emit(CertificateEvents.MANAGER_STARTED, this.options.port);
// Start certificate process for domains with acmeMaintenance enabled
for (const [domain, domainInfo] of this.domainCertificates.entries()) {
// 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) {
const message = error instanceof Error ? error.message : 'Unknown error starting server';
reject(new ServerError(message));
}
});
}
/**
* Stops the HTTP server and cleanup resources
*/
public async stop(): Promise<void> {
if (!this.server) {
return;
}
this.isShuttingDown = true;
return new Promise<void>((resolve) => {
if (this.server) {
this.server.close(() => {
this.server = null;
this.isShuttingDown = false;
this.emit(CertificateEvents.MANAGER_STOPPED);
resolve();
});
} else {
this.isShuttingDown = false;
resolve();
}
});
}
/**
* Adds a domain with configuration options
* @param options Domain configuration options
*/
public addDomain(options: IDomainOptions | IPort80RouteOptions): void {
// Normalize options format (handle both IDomainOptions and IPort80RouteOptions)
const normalizedOptions: IDomainOptions = this.normalizeOptions(options);
if (!normalizedOptions.domainName || typeof normalizedOptions.domainName !== 'string') {
throw new HttpError('Invalid domain name');
}
const domainName = normalizedOptions.domainName;
if (!this.domainCertificates.has(domainName)) {
this.domainCertificates.set(domainName, {
options: normalizedOptions,
certObtained: false,
obtainingInProgress: false
});
console.log(`Domain added: ${domainName} with configuration:`, {
sslRedirect: normalizedOptions.sslRedirect,
acmeMaintenance: normalizedOptions.acmeMaintenance,
hasForward: !!normalizedOptions.forward,
hasAcmeForward: !!normalizedOptions.acmeForward,
routeReference: normalizedOptions.routeReference
});
// If acmeMaintenance is enabled and not a glob pattern, start certificate process immediately
if (normalizedOptions.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 = normalizedOptions;
console.log(`Domain ${domainName} configuration updated`);
}
}
/**
* Add domains from route configurations
* @param routes Array of route configurations
*/
public addDomainsFromRoutes(routes: IRouteConfig[]): void {
// Extract Port80RouteOptions from routes
const routeOptions = extractPort80RoutesFromRoutes(routes);
// Add each domain
for (const options of routeOptions) {
this.addDomain(options);
}
console.log(`Added ${routeOptions.length} domains from routes for certificate management`);
}
/**
* Normalize options from either IDomainOptions or IPort80RouteOptions
* @param options Options to normalize
* @returns Normalized IDomainOptions
* @private
*/
private normalizeOptions(options: IDomainOptions | IPort80RouteOptions): IDomainOptions {
// Handle IPort80RouteOptions format
if ('domain' in options) {
return {
domainName: options.domain,
sslRedirect: options.sslRedirect,
acmeMaintenance: options.acmeMaintenance,
forward: options.forward,
acmeForward: options.acmeForward,
routeReference: options.routeReference
};
}
// Already in IDomainOptions format
return options;
}
/**
* Removes a domain from management
* @param domain The domain to remove
*/
public removeDomain(domain: string): void {
if (this.domainCertificates.delete(domain)) {
console.log(`Domain removed: ${domain}`);
}
}
/**
* Gets the certificate for a domain if it exists
* @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) {
return null;
}
return {
domain,
certificate: domainInfo.certificate,
privateKey: domainInfo.privateKey,
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;
}
}
/**
* Handles incoming HTTP requests
* @param req The HTTP request
* @param res The HTTP response
*/
private handleRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void {
// Emit request received event with basic info
this.emit(HttpEvents.REQUEST_RECEIVED, {
url: req.url,
method: req.method,
headers: req.headers
});
const hostHeader = req.headers.host;
if (!hostHeader) {
res.statusCode = HttpStatus.BAD_REQUEST;
res.end('Bad Request: Host header is missing');
return;
}
// Extract domain (ignoring any port in the Host header)
const domain = hostHeader.split(':')[0];
// Check if this is an ACME challenge request that our ChallengeResponder can handle
if (this.challengeResponder && req.url?.startsWith('/.well-known/acme-challenge/')) {
// Handle ACME HTTP-01 challenge with the challenge responder
const domainMatch = this.getDomainInfoForRequest(domain);
// If there's a specific ACME forwarding config for this domain, use that instead
if (domainMatch?.domainInfo.options.acmeForward) {
this.forwardRequest(req, res, domainMatch.domainInfo.options.acmeForward, 'ACME challenge');
return;
}
// If domain exists and has acmeMaintenance enabled, or we don't have the domain yet
// (for auto-provisioning), try to handle the ACME challenge
if (!domainMatch || domainMatch.domainInfo.options.acmeMaintenance) {
// Let the challenge responder try to handle this request
if (this.challengeResponder.handleRequest(req, res)) {
// Challenge was handled
return;
}
}
}
// Dynamic provisioning: if domain not yet managed, register for ACME and return 503
if (!this.domainCertificates.has(domain)) {
try {
this.addDomain({ domainName: domain, sslRedirect: false, acmeMaintenance: true });
} catch (err) {
console.error(`Error registering domain for on-demand provisioning: ${err}`);
}
res.statusCode = HttpStatus.SERVICE_UNAVAILABLE;
res.end('Certificate issuance in progress');
return;
}
// Get domain config, using glob pattern matching if needed
const domainMatch = this.getDomainInfoForRequest(domain);
if (!domainMatch) {
res.statusCode = HttpStatus.NOT_FOUND;
res.end('Domain not configured');
return;
}
const { domainInfo, pattern } = domainMatch;
const options = domainInfo.options;
// 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 || '/'}`;
res.statusCode = HttpStatus.MOVED_PERMANENTLY;
res.setHeader('Location', redirectUrl);
res.end(`Redirecting to ${redirectUrl}`);
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 => {
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
this.emit(CertificateEvents.CERTIFICATE_FAILED, {
domain,
error: errorMessage,
isRenewal: false
});
console.error(`Error obtaining certificate for ${domain}:`, err);
});
}
res.statusCode = HttpStatus.SERVICE_UNAVAILABLE;
res.end('Certificate issuance in progress, please try again later.');
return;
}
// Default response for unhandled request
res.statusCode = HttpStatus.NOT_FOUND;
res.end('No handlers configured for this request');
// Emit request handled event
this.emit(HttpEvents.REQUEST_HANDLED, {
domain,
url: req.url,
statusCode: res.statusCode
});
}
/**
* 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: { ip: string; port: number },
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 || HttpStatus.INTERNAL_SERVER_ERROR;
// 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(HttpEvents.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);
this.emit(HttpEvents.REQUEST_ERROR, {
domain,
error: error.message,
target: `${target.ip}:${target.port}`
});
if (!res.headersSent) {
res.statusCode = HttpStatus.INTERNAL_SERVER_ERROR;
res.end(`Proxy error: ${error.message}`);
} else {
res.end();
}
});
// Pipe original request to proxy request
if (req.readable) {
req.pipe(proxyReq);
} else {
proxyReq.end();
}
}
/**
* Obtains a certificate for a domain using ACME HTTP-01 challenge
* @param domain The domain to obtain a certificate for
* @param isRenewal Whether this is a renewal attempt
*/
private async obtainCertificate(domain: string, isRenewal: boolean = false): Promise<void> {
if (this.isGlobPattern(domain)) {
throw new CertificateError('Cannot obtain certificates for glob pattern domains', domain, isRenewal);
}
const domainInfo = this.domainCertificates.get(domain)!;
if (!domainInfo.options.acmeMaintenance) {
console.log(`Skipping certificate issuance for ${domain} - acmeMaintenance is disabled`);
return;
}
if (domainInfo.obtainingInProgress) {
console.log(`Certificate issuance already in progress for ${domain}`);
return;
}
if (!this.challengeResponder) {
throw new HttpError('Challenge responder is not initialized');
}
domainInfo.obtainingInProgress = true;
domainInfo.lastRenewalAttempt = new Date();
try {
// Request certificate via ChallengeResponder
// The ChallengeResponder handles all ACME client interactions and will emit events
const certData = await this.challengeResponder.requestCertificate(domain, isRenewal);
// Update domain info with certificate data
domainInfo.certificate = certData.certificate;
domainInfo.privateKey = certData.privateKey;
domainInfo.certObtained = true;
domainInfo.expiryDate = certData.expiryDate;
console.log(`Certificate ${isRenewal ? 'renewed' : 'obtained'} for ${domain}`);
} catch (error: any) {
const errorMsg = error instanceof Error ? error.message : String(error);
console.error(`Error during certificate issuance for ${domain}:`, error);
throw new CertificateError(errorMsg, domain, isRenewal);
} finally {
domainInfo.obtainingInProgress = false;
}
}
/**
* 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: CertificateEvents, data: ICertificateData): void {
this.emit(eventType, data);
}
/**
* Gets all domains and their certificate status
* @returns Map of domains to certificate status
*/
public getDomainCertificateStatus(): Map<string, {
certObtained: boolean;
expiryDate?: Date;
daysRemaining?: number;
obtainingInProgress: boolean;
lastRenewalAttempt?: Date;
}> {
const result = new Map<string, {
certObtained: boolean;
expiryDate?: Date;
daysRemaining?: number;
obtainingInProgress: boolean;
lastRenewalAttempt?: Date;
}>();
const now = new Date();
for (const [domain, domainInfo] of this.domainCertificates.entries()) {
// Skip glob patterns
if (this.isGlobPattern(domain)) continue;
const status: {
certObtained: boolean;
expiryDate?: Date;
daysRemaining?: number;
obtainingInProgress: boolean;
lastRenewalAttempt?: Date;
} = {
certObtained: domainInfo.certObtained,
expiryDate: domainInfo.expiryDate,
obtainingInProgress: domainInfo.obtainingInProgress,
lastRenewalAttempt: domainInfo.lastRenewalAttempt
};
// Calculate days remaining if expiry date is available
if (domainInfo.expiryDate) {
const daysRemaining = Math.ceil(
(domainInfo.expiryDate.getTime() - now.getTime()) / (24 * 60 * 60 * 1000)
);
status.daysRemaining = daysRemaining;
}
result.set(domain, status);
}
return result;
}
/**
* Request a certificate renewal for a specific domain.
* @param domain The domain to renew.
*/
public async renewCertificate(domain: string): Promise<void> {
if (!this.domainCertificates.has(domain)) {
throw new HttpError(`Domain not managed: ${domain}`);
}
// Trigger renewal via ACME
await this.obtainCertificate(domain, true);
}
}

View File

@ -9,39 +9,36 @@ export * from './proxies/nftables-proxy/index.js';
// Export NetworkProxy elements selectively to avoid RouteManager ambiguity
export { NetworkProxy, CertificateManager, ConnectionPool, RequestHandler, WebSocketHandler } from './proxies/network-proxy/index.js';
export type { IMetricsTracker, MetricsTracker } from './proxies/network-proxy/index.js';
export * from './proxies/network-proxy/models/index.js';
// Export models except IAcmeOptions to avoid conflict
export type { INetworkProxyOptions, ICertificateEntry, ILogger } from './proxies/network-proxy/models/types.js';
export { RouteManager as NetworkProxyRouteManager } from './proxies/network-proxy/models/types.js';
// Export port80handler elements selectively to avoid conflicts
export {
Port80Handler,
Port80HandlerError as HttpError,
ServerError,
CertificateError
} from './http/port80/port80-handler.js';
// Use re-export to control the names
export { Port80HandlerEvents } from './certificate/events/certificate-events.js';
// Certificate and Port80 modules have been removed - use SmartCertManager instead
export * from './redirect/classes.redirect.js';
// Export SmartProxy elements selectively to avoid RouteManager ambiguity
export { SmartProxy, ConnectionManager, SecurityManager, TimeoutManager, TlsManager, NetworkProxyBridge, RouteConnectionHandler } from './proxies/smart-proxy/index.js';
export { RouteManager } from './proxies/smart-proxy/route-manager.js';
export * from './proxies/smart-proxy/models/index.js';
// Export smart-proxy models
export type { ISmartProxyOptions, IConnectionRecord, IRouteConfig, IRouteMatch, IRouteAction, IRouteTls, IRouteContext } from './proxies/smart-proxy/models/index.js';
export type { TSmartProxyCertProvisionObject } from './proxies/smart-proxy/models/interfaces.js';
export * from './proxies/smart-proxy/utils/index.js';
// Original: export * from './smartproxy/classes.pp.snihandler.js'
// Now we export from the new module
export { SniHandler } from './tls/sni/sni-handler.js';
// Original: export * from './smartproxy/classes.pp.interfaces.js'
// Now we export from the new module
export * from './proxies/smart-proxy/models/interfaces.js';
// Now we export from the new module (selectively to avoid conflicts)
// Core types and utilities
export * from './core/models/common-types.js';
// Export IAcmeOptions from one place only
export type { IAcmeOptions } from './proxies/smart-proxy/models/interfaces.js';
// Modular exports for new architecture
export * as forwarding from './forwarding/index.js';
export * as certificate from './certificate/index.js';
// Certificate module has been removed - use SmartCertManager instead
export * as tls from './tls/index.js';
export * as http from './http/index.js';

View File

@ -2,16 +2,19 @@
* Proxy implementations module
*/
// Export NetworkProxy with selective imports to avoid RouteManager ambiguity
// Export NetworkProxy with selective imports to avoid conflicts
export { NetworkProxy, CertificateManager, ConnectionPool, RequestHandler, WebSocketHandler } from './network-proxy/index.js';
export type { IMetricsTracker, MetricsTracker } from './network-proxy/index.js';
export * from './network-proxy/models/index.js';
// Export network-proxy models except IAcmeOptions
export type { INetworkProxyOptions, ICertificateEntry, ILogger } from './network-proxy/models/types.js';
export { RouteManager as NetworkProxyRouteManager } from './network-proxy/models/types.js';
// Export SmartProxy with selective imports to avoid RouteManager ambiguity
// Export SmartProxy with selective imports to avoid conflicts
export { SmartProxy, ConnectionManager, SecurityManager, TimeoutManager, TlsManager, NetworkProxyBridge, RouteConnectionHandler } from './smart-proxy/index.js';
export { RouteManager as SmartProxyRouteManager } from './smart-proxy/route-manager.js';
export * from './smart-proxy/utils/index.js';
export * from './smart-proxy/models/index.js';
// Export smart-proxy models except IAcmeOptions
export type { ISmartProxyOptions, IConnectionRecord, IRouteConfig, IRouteMatch, IRouteAction, IRouteTls, IRouteContext } from './smart-proxy/models/index.js';
// Export NFTables proxy (no conflicts)
export * from './nftables-proxy/index.js';

View File

@ -3,21 +3,17 @@ import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
import { type INetworkProxyOptions, type ICertificateEntry, type ILogger, createLogger } from './models/types.js';
import { Port80Handler } from '../../http/port80/port80-handler.js';
import { CertificateEvents } from '../../certificate/events/certificate-events.js';
import { buildPort80Handler } from '../../certificate/acme/acme-factory.js';
import { subscribeToPort80Handler } from '../../core/utils/event-utils.js';
import type { IDomainOptions } from '../../certificate/models/certificate-types.js';
import type { IRouteConfig } from '../smart-proxy/models/route-types.js';
/**
* Manages SSL certificates for NetworkProxy including ACME integration
* @deprecated This class is deprecated. Use SmartCertManager instead.
*
* This is a stub implementation that maintains backward compatibility
* while the functionality has been moved to SmartCertManager.
*/
export class CertificateManager {
private defaultCertificates: { key: string; cert: string };
private certificateCache: Map<string, ICertificateEntry> = new Map();
private port80Handler: Port80Handler | null = null;
private externalPort80Handler: boolean = false;
private certificateStoreDir: string;
private logger: ILogger;
private httpsServer: plugins.https.Server | null = null;
@ -26,6 +22,8 @@ export class CertificateManager {
this.certificateStoreDir = path.resolve(options.acme?.certificateStore || './certs');
this.logger = createLogger(options.logLevel || 'info');
this.logger.warn('CertificateManager is deprecated - use SmartCertManager instead');
// Ensure certificate store directory exists
try {
if (!fs.existsSync(this.certificateStoreDir)) {
@ -44,7 +42,6 @@ export class CertificateManager {
*/
public loadDefaultCertificates(): void {
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Fix the path to look for certificates at the project root instead of inside ts directory
const certPath = path.join(__dirname, '..', '..', '..', 'assets', 'certs');
try {
@ -52,467 +49,145 @@ export class CertificateManager {
key: fs.readFileSync(path.join(certPath, 'key.pem'), 'utf8'),
cert: fs.readFileSync(path.join(certPath, 'cert.pem'), 'utf8')
};
this.logger.info('Default certificates loaded successfully');
this.logger.info('Loaded default certificates from filesystem');
} catch (error) {
this.logger.error('Error loading default certificates', error);
// Generate self-signed fallback certificates
try {
// This is a placeholder for actual certificate generation code
// In a real implementation, you would use a library like selfsigned to generate certs
this.defaultCertificates = {
key: "FALLBACK_KEY_CONTENT",
cert: "FALLBACK_CERT_CONTENT"
};
this.logger.warn('Using fallback self-signed certificates');
} catch (fallbackError) {
this.logger.error('Failed to generate fallback certificates', fallbackError);
throw new Error('Could not load or generate SSL certificates');
}
this.logger.error(`Failed to load default certificates: ${error}`);
this.generateSelfSignedCertificate();
}
}
/**
* Set the HTTPS server reference for context updates
* Generates self-signed certificates as fallback
*/
private generateSelfSignedCertificate(): void {
// Generate a self-signed certificate using forge or similar
// For now, just use a placeholder
const selfSignedCert = `-----BEGIN CERTIFICATE-----
MIIBkTCB+wIJAKHHIgIIA0/cMA0GCSqGSIb3DQEBBQUAMA0xCzAJBgNVBAYTAlVT
MB4XDTE0MDEwMTAwMDAwMFoXDTI0MDEwMTAwMDAwMFowDTELMAkGA1UEBhMCVVMw
gZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMRiH0VwnOH3jCV7c6JFZWYrvuqy
-----END CERTIFICATE-----`;
const selfSignedKey = `-----BEGIN PRIVATE KEY-----
MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAMRiH0VwnOH3jCV7
c6JFZWYrvuqyALCLXj0pcr1iqNdHjegNXnkl5zjdaUjq4edNOKl7M1AlFiYjG2xk
-----END PRIVATE KEY-----`;
this.defaultCertificates = {
key: selfSignedKey,
cert: selfSignedCert
};
this.logger.warn('Using self-signed certificate as fallback');
}
/**
* Gets the default certificates
*/
public getDefaultCertificates(): { key: string; cert: string } {
return this.defaultCertificates;
}
/**
* @deprecated Use SmartCertManager instead
*/
public setExternalPort80Handler(handler: any): void {
this.logger.warn('setExternalPort80Handler is deprecated - use SmartCertManager instead');
}
/**
* @deprecated Use SmartCertManager instead
*/
public async updateRoutes(routes: IRouteConfig[]): Promise<void> {
this.logger.warn('updateRoutes is deprecated - use SmartCertManager instead');
}
/**
* Handles SNI callback to provide appropriate certificate
*/
public handleSNI(domain: string, cb: (err: Error | null, ctx: plugins.tls.SecureContext) => void): void {
const certificate = this.getCachedCertificate(domain);
if (certificate) {
const context = plugins.tls.createSecureContext({
key: certificate.key,
cert: certificate.cert
});
cb(null, context);
return;
}
// Use default certificate if no domain-specific certificate found
const defaultContext = plugins.tls.createSecureContext({
key: this.defaultCertificates.key,
cert: this.defaultCertificates.cert
});
cb(null, defaultContext);
}
/**
* Updates a certificate in the cache
*/
public updateCertificate(domain: string, cert: string, key: string): void {
this.certificateCache.set(domain, {
cert,
key,
expires: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000) // 90 days
});
this.logger.info(`Certificate updated for ${domain}`);
}
/**
* Gets a cached certificate
*/
private getCachedCertificate(domain: string): ICertificateEntry | null {
return this.certificateCache.get(domain) || null;
}
/**
* @deprecated Use SmartCertManager instead
*/
public async initializePort80Handler(): Promise<any> {
this.logger.warn('initializePort80Handler is deprecated - use SmartCertManager instead');
return null;
}
/**
* @deprecated Use SmartCertManager instead
*/
public async stopPort80Handler(): Promise<void> {
this.logger.warn('stopPort80Handler is deprecated - use SmartCertManager instead');
}
/**
* @deprecated Use SmartCertManager instead
*/
public registerDomainsWithPort80Handler(domains: string[]): void {
this.logger.warn('registerDomainsWithPort80Handler is deprecated - use SmartCertManager instead');
}
/**
* @deprecated Use SmartCertManager instead
*/
public registerRoutesWithPort80Handler(routes: IRouteConfig[]): void {
this.logger.warn('registerRoutesWithPort80Handler is deprecated - use SmartCertManager instead');
}
/**
* Sets the HTTPS server for certificate updates
*/
public setHttpsServer(server: plugins.https.Server): void {
this.httpsServer = server;
}
/**
* Get default certificates
*/
public getDefaultCertificates(): { key: string; cert: string } {
return { ...this.defaultCertificates };
}
/**
* Sets an external Port80Handler for certificate management
*/
public setExternalPort80Handler(handler: Port80Handler): void {
if (this.port80Handler && !this.externalPort80Handler) {
this.logger.warn('Replacing existing internal Port80Handler with external handler');
// Clean up existing handler if needed
if (this.port80Handler !== handler) {
// Unregister event handlers to avoid memory leaks
this.port80Handler.removeAllListeners(CertificateEvents.CERTIFICATE_ISSUED);
this.port80Handler.removeAllListeners(CertificateEvents.CERTIFICATE_RENEWED);
this.port80Handler.removeAllListeners(CertificateEvents.CERTIFICATE_FAILED);
this.port80Handler.removeAllListeners(CertificateEvents.CERTIFICATE_EXPIRING);
}
}
// Set the external handler
this.port80Handler = handler;
this.externalPort80Handler = true;
// Subscribe to Port80Handler events
subscribeToPort80Handler(this.port80Handler, {
onCertificateIssued: this.handleCertificateIssued.bind(this),
onCertificateRenewed: this.handleCertificateIssued.bind(this),
onCertificateFailed: this.handleCertificateFailed.bind(this),
onCertificateExpiring: (data) => {
this.logger.info(`Certificate for ${data.domain} expires in ${data.daysRemaining} days`);
}
});
this.logger.info('External Port80Handler connected to CertificateManager');
// Register domains with Port80Handler if we have any certificates cached
if (this.certificateCache.size > 0) {
const domains = Array.from(this.certificateCache.keys())
.filter(domain => !domain.includes('*')); // Skip wildcard domains
this.registerDomainsWithPort80Handler(domains);
}
}
/**
* Update route configurations managed by this certificate manager
* This method is called when route configurations change
*
* @param routes Array of route configurations
* Gets statistics for metrics
*/
public updateRouteConfigs(routes: IRouteConfig[]): void {
if (!this.port80Handler) {
this.logger.warn('Cannot update routes - Port80Handler is not initialized');
return;
}
// Register domains from routes with Port80Handler
this.registerRoutesWithPort80Handler(routes);
// Process individual routes for certificate requirements
for (const route of routes) {
this.processRouteForCertificates(route);
}
this.logger.info(`Updated certificate management for ${routes.length} routes`);
}
/**
* Handle newly issued or renewed certificates from Port80Handler
*/
private handleCertificateIssued(data: { domain: string; certificate: string; privateKey: string; expiryDate: Date }): void {
const { domain, certificate, privateKey, expiryDate } = data;
this.logger.info(`Certificate ${this.certificateCache.has(domain) ? 'renewed' : 'issued'} for ${domain}, valid until ${expiryDate.toISOString()}`);
// Update certificate in HTTPS server
this.updateCertificateCache(domain, certificate, privateKey, expiryDate);
// Save the certificate to the filesystem if not using external handler
if (!this.externalPort80Handler && this.options.acme?.certificateStore) {
this.saveCertificateToStore(domain, certificate, privateKey);
}
}
/**
* Handle certificate issuance failures
*/
private handleCertificateFailed(data: { domain: string; error: string }): void {
this.logger.error(`Certificate issuance failed for ${data.domain}: ${data.error}`);
}
/**
* Saves certificate and private key to the filesystem
*/
private saveCertificateToStore(domain: string, certificate: string, privateKey: string): void {
try {
const certPath = path.join(this.certificateStoreDir, `${domain}.cert.pem`);
const keyPath = path.join(this.certificateStoreDir, `${domain}.key.pem`);
fs.writeFileSync(certPath, certificate);
fs.writeFileSync(keyPath, privateKey);
// Ensure private key has restricted permissions
try {
fs.chmodSync(keyPath, 0o600);
} catch (error) {
this.logger.warn(`Failed to set permissions on private key for ${domain}: ${error}`);
}
this.logger.info(`Saved certificate for ${domain} to ${certPath}`);
} catch (error) {
this.logger.error(`Failed to save certificate for ${domain}: ${error}`);
}
}
/**
* Handles SNI (Server Name Indication) for TLS connections
* Used by the HTTPS server to select the correct certificate for each domain
*/
public handleSNI(domain: string, cb: (err: Error | null, ctx: plugins.tls.SecureContext) => void): void {
this.logger.debug(`SNI request for domain: ${domain}`);
// Check if we have a certificate for this domain
const certs = this.certificateCache.get(domain);
if (certs) {
try {
// Create TLS context with the cached certificate
const context = plugins.tls.createSecureContext({
key: certs.key,
cert: certs.cert
});
this.logger.debug(`Using cached certificate for ${domain}`);
cb(null, context);
return;
} catch (err) {
this.logger.error(`Error creating secure context for ${domain}:`, err);
}
}
// No existing certificate: trigger dynamic provisioning via Port80Handler
if (this.port80Handler) {
try {
this.logger.info(`Triggering on-demand certificate retrieval for ${domain}`);
this.port80Handler.addDomain({
domainName: domain,
sslRedirect: false,
acmeMaintenance: true
});
} catch (err) {
this.logger.error(`Error registering domain for on-demand certificate: ${domain}`, err);
}
}
// Check if we should trigger certificate issuance
if (this.options.acme?.enabled && this.port80Handler && !domain.includes('*')) {
// Check if this domain is already registered
const certData = this.port80Handler.getCertificate(domain);
if (!certData) {
this.logger.info(`No certificate found for ${domain}, registering for issuance`);
// Register with new domain options format
const domainOptions: IDomainOptions = {
domainName: domain,
sslRedirect: true,
acmeMaintenance: true
};
this.port80Handler.addDomain(domainOptions);
}
}
// Fall back to default certificate
try {
const context = plugins.tls.createSecureContext({
key: this.defaultCertificates.key,
cert: this.defaultCertificates.cert
});
this.logger.debug(`Using default certificate for ${domain}`);
cb(null, context);
} catch (err) {
this.logger.error(`Error creating default secure context:`, err);
cb(new Error('Cannot create secure context'), null);
}
}
/**
* Updates certificate in cache
*/
public updateCertificateCache(domain: string, certificate: string, privateKey: string, expiryDate?: Date): void {
// Update certificate context in HTTPS server if it's running
if (this.httpsServer) {
try {
this.httpsServer.addContext(domain, {
key: privateKey,
cert: certificate
});
this.logger.debug(`Updated SSL context for domain: ${domain}`);
} catch (error) {
this.logger.error(`Error updating SSL context for domain ${domain}:`, error);
}
}
// Update certificate in cache
this.certificateCache.set(domain, {
key: privateKey,
cert: certificate,
expires: expiryDate
});
}
/**
* Gets a certificate for a domain
*/
public getCertificate(domain: string): ICertificateEntry | undefined {
return this.certificateCache.get(domain);
}
/**
* Requests a new certificate for a domain
*/
public async requestCertificate(domain: string): Promise<boolean> {
if (!this.options.acme?.enabled && !this.externalPort80Handler) {
this.logger.warn('ACME certificate management is not enabled');
return false;
}
if (!this.port80Handler) {
this.logger.error('Port80Handler is not initialized');
return false;
}
// Skip wildcard domains - can't get certs for these with HTTP-01 validation
if (domain.includes('*')) {
this.logger.error(`Cannot request certificate for wildcard domain: ${domain}`);
return false;
}
try {
// Use the new domain options format
const domainOptions: IDomainOptions = {
domainName: domain,
sslRedirect: true,
acmeMaintenance: true
};
this.port80Handler.addDomain(domainOptions);
this.logger.info(`Certificate request submitted for domain: ${domain}`);
return true;
} catch (error) {
this.logger.error(`Error requesting certificate for domain ${domain}:`, error);
return false;
}
}
/**
* Registers domains with Port80Handler for ACME certificate management
* @param domains String array of domains to register
*/
public registerDomainsWithPort80Handler(domains: string[]): void {
if (!this.port80Handler) {
this.logger.warn('Port80Handler is not initialized');
return;
}
for (const domain of domains) {
// Skip wildcard domains - can't get certs for these with HTTP-01 validation
if (domain.includes('*')) {
this.logger.info(`Skipping wildcard domain for ACME: ${domain}`);
continue;
}
// Skip domains already with certificates if configured to do so
if (this.options.acme?.skipConfiguredCerts) {
const cachedCert = this.certificateCache.get(domain);
if (cachedCert) {
this.logger.info(`Skipping domain with existing certificate: ${domain}`);
continue;
}
}
// Register the domain for certificate issuance with new domain options format
const domainOptions: IDomainOptions = {
domainName: domain,
sslRedirect: true,
acmeMaintenance: true
};
this.port80Handler.addDomain(domainOptions);
this.logger.info(`Registered domain for ACME certificate issuance: ${domain}`);
}
}
/**
* Extract domains from route configurations and register with Port80Handler
* This method enables direct integration with route-based configuration
*
* @param routes Array of route configurations
*/
public registerRoutesWithPort80Handler(routes: IRouteConfig[]): void {
if (!this.port80Handler) {
this.logger.warn('Port80Handler is not initialized');
return;
}
// Extract domains from route configurations
const domains: Set<string> = new Set();
for (const route of routes) {
// Skip disabled routes
if (route.enabled === false) {
continue;
}
// Skip routes without HTTPS termination
if (route.action.type !== 'forward' || route.action.tls?.mode !== 'terminate') {
continue;
}
// Extract domains from match criteria
if (route.match.domains) {
if (typeof route.match.domains === 'string') {
domains.add(route.match.domains);
} else if (Array.isArray(route.match.domains)) {
for (const domain of route.match.domains) {
domains.add(domain);
}
}
}
}
// Register extracted domains
this.registerDomainsWithPort80Handler(Array.from(domains));
}
/**
* Process a route config to determine if it requires automatic certificate provisioning
* @param route Route configuration to process
*/
public processRouteForCertificates(route: IRouteConfig): void {
// Skip disabled routes
if (route.enabled === false) {
return;
}
// Skip routes without HTTPS termination or auto certificate
if (route.action.type !== 'forward' ||
route.action.tls?.mode !== 'terminate' ||
route.action.tls?.certificate !== 'auto') {
return;
}
// Extract domains from match criteria
const domains: string[] = [];
if (route.match.domains) {
if (typeof route.match.domains === 'string') {
domains.push(route.match.domains);
} else if (Array.isArray(route.match.domains)) {
domains.push(...route.match.domains);
}
}
// Request certificates for the domains
for (const domain of domains) {
if (!domain.includes('*')) { // Skip wildcard domains
this.requestCertificate(domain).catch(err => {
this.logger.error(`Error requesting certificate for domain ${domain}:`, err);
});
}
}
}
/**
* Initialize internal Port80Handler
*/
public async initializePort80Handler(): Promise<Port80Handler | null> {
// Skip if using external handler
if (this.externalPort80Handler) {
this.logger.info('Using external Port80Handler, skipping initialization');
return this.port80Handler;
}
if (!this.options.acme?.enabled) {
return null;
}
// Build and configure Port80Handler
this.port80Handler = buildPort80Handler({
port: this.options.acme.port,
accountEmail: this.options.acme.accountEmail,
useProduction: this.options.acme.useProduction,
httpsRedirectPort: this.options.port, // Redirect to our HTTPS port
enabled: this.options.acme.enabled,
certificateStore: this.options.acme.certificateStore,
skipConfiguredCerts: this.options.acme.skipConfiguredCerts
});
// Subscribe to Port80Handler events
subscribeToPort80Handler(this.port80Handler, {
onCertificateIssued: this.handleCertificateIssued.bind(this),
onCertificateRenewed: this.handleCertificateIssued.bind(this),
onCertificateFailed: this.handleCertificateFailed.bind(this),
onCertificateExpiring: (data) => {
this.logger.info(`Certificate for ${data.domain} expires in ${data.daysRemaining} days`);
}
});
// Start the handler
try {
await this.port80Handler.start();
this.logger.info(`Port80Handler started on port ${this.options.acme.port}`);
return this.port80Handler;
} catch (error) {
this.logger.error(`Failed to start Port80Handler: ${error}`);
this.port80Handler = null;
return null;
}
}
/**
* Stop the Port80Handler if it was internally created
*/
public async stopPort80Handler(): Promise<void> {
if (this.port80Handler && !this.externalPort80Handler) {
try {
await this.port80Handler.stop();
this.logger.info('Port80Handler stopped');
} catch (error) {
this.logger.error('Error stopping Port80Handler', error);
}
}
public getStats() {
return {
cachedCertificates: this.certificateCache.size,
defaultCertEnabled: true
};
}
}

View File

@ -1,5 +1,17 @@
import * as plugins from '../../../plugins.js';
import type { IAcmeOptions } from '../../../certificate/models/certificate-types.js';
// Certificate types removed - define IAcmeOptions locally
export interface IAcmeOptions {
enabled: boolean;
email?: string;
accountEmail?: string;
port?: number;
certificateStore?: string;
environment?: 'production' | 'staging';
useProduction?: boolean;
renewThresholdDays?: number;
autoRenew?: boolean;
skipConfiguredCerts?: boolean;
}
import type { IRouteConfig } from '../../smart-proxy/models/route-types.js';
import type { IRouteContext } from '../../../core/models/route-context.js';
@ -22,7 +34,7 @@ export interface INetworkProxyOptions {
// Settings for SmartProxy integration
connectionPoolSize?: number; // Maximum connections to maintain in the pool to each backend
portProxyIntegration?: boolean; // Flag to indicate this proxy is used by SmartProxy
useExternalPort80Handler?: boolean; // Flag to indicate using external Port80Handler
useExternalPort80Handler?: boolean; // @deprecated - use SmartCertManager instead
// Protocol to use when proxying to backends: HTTP/1.x or HTTP/2
backendProtocol?: 'http1' | 'http2';

View File

@ -18,7 +18,6 @@ import { RequestHandler, type IMetricsTracker } from './request-handler.js';
import { WebSocketHandler } from './websocket-handler.js';
import { ProxyRouter } from '../../http/router/index.js';
import { RouteRouter } from '../../http/router/route-router.js';
import { Port80Handler } from '../../http/port80/port80-handler.js';
import { FunctionCache } from './function-cache.js';
/**
@ -221,15 +220,10 @@ export class NetworkProxy implements IMetricsTracker {
}
/**
* Sets an external Port80Handler for certificate management
* This allows the NetworkProxy to use a centrally managed Port80Handler
* instead of creating its own
*
* @param handler The Port80Handler instance to use
* @deprecated Use SmartCertManager instead
*/
public setExternalPort80Handler(handler: Port80Handler): void {
// Connect it to the certificate manager
this.certificateManager.setExternalPort80Handler(handler);
public setExternalPort80Handler(handler: any): void {
this.logger.warn('Port80Handler is deprecated - use SmartCertManager instead');
}
/**
@ -238,10 +232,7 @@ export class NetworkProxy implements IMetricsTracker {
public async start(): Promise<void> {
this.startTime = Date.now();
// Initialize Port80Handler if enabled and not using external handler
if (this.options.acme?.enabled && !this.options.useExternalPort80Handler) {
await this.certificateManager.initializePort80Handler();
}
// Certificate management is now handled by SmartCertManager
// Create HTTP/2 server with HTTP/1 fallback
this.httpsServer = plugins.http2.createSecureServer(
@ -385,7 +376,7 @@ export class NetworkProxy implements IMetricsTracker {
// Directly update the certificate manager with the new routes
// This will extract domains and handle certificate provisioning
this.certificateManager.updateRouteConfigs(routes);
this.certificateManager.updateRoutes(routes);
// Collect all domains and certificates for configuration
const currentHostnames = new Set<string>();
@ -425,7 +416,7 @@ export class NetworkProxy implements IMetricsTracker {
// Update certificate cache with any static certificates
for (const [domain, certData] of certificateUpdates.entries()) {
try {
this.certificateManager.updateCertificateCache(
this.certificateManager.updateCertificate(
domain,
certData.cert,
certData.key
@ -547,8 +538,7 @@ export class NetworkProxy implements IMetricsTracker {
// Close all connection pool connections
this.connectionPool.closeAllConnections();
// Stop Port80Handler if internally managed
await this.certificateManager.stopPort80Handler();
// Certificate management cleanup is handled by SmartCertManager
// Close the HTTPS server
return new Promise((resolve) => {
@ -566,7 +556,8 @@ export class NetworkProxy implements IMetricsTracker {
* @returns A promise that resolves when the request is submitted (not when the certificate is issued)
*/
public async requestCertificate(domain: string): Promise<boolean> {
return this.certificateManager.requestCertificate(domain);
this.logger.warn('requestCertificate is deprecated - use SmartCertManager instead');
return false;
}
/**
@ -587,7 +578,7 @@ export class NetworkProxy implements IMetricsTracker {
expiryDate?: Date
): void {
this.logger.info(`Updating certificate for ${domain}`);
this.certificateManager.updateCertificateCache(domain, certificate, privateKey, expiryDate);
this.certificateManager.updateCertificate(domain, certificate, privateKey);
}
/**

View File

@ -1,5 +1,6 @@
/**
* SmartProxy models
*/
export * from './interfaces.js';
// Export everything except IAcmeOptions from interfaces
export type { ISmartProxyOptions, IConnectionRecord, TSmartProxyCertProvisionObject } from './interfaces.js';
export * from './route-types.js';

View File

@ -1,5 +1,18 @@
import * as plugins from '../../../plugins.js';
import type { IAcmeOptions } from '../../../certificate/models/certificate-types.js';
// Certificate types removed - define IAcmeOptions locally
export interface IAcmeOptions {
enabled?: boolean;
email?: string;
environment?: 'production' | 'staging';
port?: number;
useProduction?: boolean;
renewThresholdDays?: number;
autoRenew?: boolean;
certificateStore?: string;
skipConfiguredCerts?: boolean;
renewCheckIntervalHours?: number;
routeForwards?: any[];
}
import type { IRouteConfig } from './route-types.js';
import type { TForwardingType } from '../../../forwarding/config/forwarding-types.js';

View File

@ -1,5 +1,5 @@
import * as plugins from '../../../plugins.js';
import type { IAcmeOptions } from '../../../certificate/models/certificate-types.js';
// Certificate types removed - use local definition
import type { TForwardingType } from '../../../forwarding/config/forwarding-types.js';
import type { PortRange } from '../../../proxies/nftables-proxy/models/interfaces.js';

View File

@ -121,13 +121,12 @@ export class SmartProxy extends plugins.EventEmitter {
this.settings.acme = {
enabled: false,
port: 80,
accountEmail: 'admin@example.com',
email: 'admin@example.com',
useProduction: false,
renewThresholdDays: 30,
autoRenew: true,
certificateStore: './certs',
skipConfiguredCerts: false,
httpsRedirectPort: 443,
renewCheckIntervalHours: 24,
routeForwards: []
};