Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
036d522048 | |||
9c05f71cd6 | |||
a9963f3b8a | |||
05c9156458 | |||
47e3c86487 | |||
1387928938 | |||
19578b061e | |||
e8a539829a | |||
a646f4ad28 | |||
aa70dcc299 | |||
adb85d920f | |||
2e4c6312cd | |||
9b773608c7 | |||
3502807023 | |||
c6dff8b78d | |||
12b18373db | |||
30c25ec70c |
61
changelog.md
61
changelog.md
@ -1,5 +1,66 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-05-05 - 10.1.0 - feat(smartproxy)
|
||||
Implement fallback to NetworkProxy on missing SNI and rename certProvider to certProvisionFunction in CertProvisioner
|
||||
|
||||
- When a TLS ClientHello is received without an SNI extension and allowSessionTicket is false, the code now attempts to forward the connection to NetworkProxy instead of immediately closing the connection with a TLS alert.
|
||||
- An error callback has been added to handle proxy forwarding failures; if forwarding fails or no NetworkProxy is available, the TLS unrecognized_name alert is sent and the connection is terminated.
|
||||
- Renamed all instances of 'certProvider' to 'certProvisionFunction' in the CertProvisioner implementation, updating the associated types and call sites.
|
||||
- Updated unit tests to simulate a ClientHello without SNI and to verify that with NetworkProxy enabled the connection is correctly forwarded.
|
||||
|
||||
## 2025-05-05 - 10.0.12 - fix(port80handler)
|
||||
refactor ACME challenge handling to use dedicated Http01MemoryHandler, remove obsolete readme.plan.md, and update version to 10.0.12
|
||||
|
||||
- Removed readme.plan.md planning document
|
||||
- Eliminated internal acmeHttp01Storage from Port80Handler
|
||||
- Instantiated and integrated Http01MemoryHandler as a class property for managing HTTP-01 challenges
|
||||
- Delegated ACME HTTP-01 challenge responses to smartAcmeHttp01Handler
|
||||
- Updated ts/00_commitinfo_data.ts version from 10.0.11 to 10.0.12
|
||||
- Adjusted certificate provisioning logic to properly handle wildcard domains and on-demand requests
|
||||
|
||||
## 2025-05-05 - 10.0.12 - fix(port80handler)
|
||||
Remove obsolete readme.plan.md and refactor Port80Handler's ACME challenge handling to use a dedicated Http01MemoryHandler
|
||||
|
||||
- Deleted readme.plan.md planning document which was no longer needed
|
||||
- Removed internal acmeHttp01Storage map from Port80Handler
|
||||
- Instantiated Http01MemoryHandler as a class property and provided it to SmartAcme for challenge handling
|
||||
- Delegated ACME HTTP-01 challenge responses to the new smartAcmeHttp01Handler instead of in-memory storage
|
||||
|
||||
## 2025-05-05 - 10.0.11 - fix(dependencies)
|
||||
Bump @push.rocks/smartacme to ^7.2.5 and @tsclass/tsclass to ^9.2.0; update MemoryCertManager import to use plugins.smartacme.certmanagers.MemoryCertManager()
|
||||
|
||||
- Updated @push.rocks/smartacme from ^7.2.4 to ^7.2.5
|
||||
- Updated @tsclass/tsclass from ^9.1.0 to ^9.2.0
|
||||
- Refactored MemoryCertManager instantiation to use the new import path
|
||||
|
||||
## 2025-05-05 - 10.0.10 - fix(docs)
|
||||
Update README: rename certProviderFunction to certProvisionFunction in configuration options for consistency.
|
||||
|
||||
- Replaced 'certProviderFunction' with 'certProvisionFunction' in the docs to reflect the updated API.
|
||||
- Ensured all references in the readme are consistent with the new naming convention.
|
||||
|
||||
## 2025-05-05 - 10.0.9 - fix(documentation)
|
||||
Update documentation to use 'certProviderFunction' instead of 'certProvider' in SmartProxy settings.
|
||||
|
||||
- Renamed 'certProvider' to 'certProviderFunction' in README examples and configuration options.
|
||||
- Ensured consistency in the configuration section of the documentation.
|
||||
|
||||
## 2025-05-05 - 10.0.8 - fix(smartproxy)
|
||||
rename certProvider to certProvisionFunction in certificate provisioning interfaces and SmartProxy
|
||||
|
||||
- In ts/smartproxy/classes.pp.interfaces.ts, renamed the optional property 'certProvider' to 'certProvisionFunction'.
|
||||
- In ts/smartproxy/classes.smartproxy.ts, updated references from this.settings.certProvider to this.settings.certProvisionFunction.
|
||||
|
||||
## 2025-05-04 - 10.0.7 - fix(core)
|
||||
refactor: Rename IPortProxySettings to ISmartProxyOptions in internal modules
|
||||
|
||||
- Replaced IPortProxySettings with ISmartProxyOptions in connection handler, connection manager, domain config manager, security manager, timeout manager, TLS manager, and network proxy bridge.
|
||||
- Updated type imports and constructors accordingly while preserving backward compatibility via export alias.
|
||||
|
||||
## 2025-05-04 - 10.0.6 - fix(smartproxy)
|
||||
No changes detected in project files. This commit updates commit info without modifying any functionality.
|
||||
|
||||
|
||||
## 2025-05-04 - 10.0.5 - fix(exports/types)
|
||||
Refactor exports and remove duplicate IReverseProxyConfig interface
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@push.rocks/smartproxy",
|
||||
"version": "10.0.5",
|
||||
"version": "10.1.0",
|
||||
"private": false,
|
||||
"description": "A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.",
|
||||
"main": "dist_ts/index.js",
|
||||
@ -24,14 +24,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@push.rocks/lik": "^6.2.2",
|
||||
"@push.rocks/smartacme": "^7.2.4",
|
||||
"@push.rocks/smartacme": "^7.3.2",
|
||||
"@push.rocks/smartdelay": "^3.0.5",
|
||||
"@push.rocks/smartnetwork": "^4.0.1",
|
||||
"@push.rocks/smartpromise": "^4.2.3",
|
||||
"@push.rocks/smartrequest": "^2.1.0",
|
||||
"@push.rocks/smartstring": "^4.0.15",
|
||||
"@push.rocks/taskbuffer": "^3.1.7",
|
||||
"@tsclass/tsclass": "^9.1.0",
|
||||
"@tsclass/tsclass": "^9.2.0",
|
||||
"@types/minimatch": "^5.1.2",
|
||||
"@types/ws": "^8.18.1",
|
||||
"minimatch": "^10.0.1",
|
||||
|
34
pnpm-lock.yaml
generated
34
pnpm-lock.yaml
generated
@ -12,8 +12,8 @@ importers:
|
||||
specifier: ^6.2.2
|
||||
version: 6.2.2
|
||||
'@push.rocks/smartacme':
|
||||
specifier: ^7.2.4
|
||||
version: 7.2.4(@aws-sdk/credential-providers@3.798.0)(socks@2.8.4)
|
||||
specifier: ^7.3.2
|
||||
version: 7.3.2(@aws-sdk/credential-providers@3.798.0)(socks@2.8.4)
|
||||
'@push.rocks/smartdelay':
|
||||
specifier: ^3.0.5
|
||||
version: 3.0.5
|
||||
@ -33,8 +33,8 @@ importers:
|
||||
specifier: ^3.1.7
|
||||
version: 3.1.7
|
||||
'@tsclass/tsclass':
|
||||
specifier: ^9.1.0
|
||||
version: 9.1.0
|
||||
specifier: ^9.2.0
|
||||
version: 9.2.0
|
||||
'@types/minimatch':
|
||||
specifier: ^5.1.2
|
||||
version: 5.1.2
|
||||
@ -355,8 +355,8 @@ packages:
|
||||
'@cloudflare/workers-types@4.20250303.0':
|
||||
resolution: {integrity: sha512-O7F7nRT4bbmwHf3gkRBLfJ7R6vHIJ/oZzWdby6obOiw2yavUfp/AIwS7aO2POu5Cv8+h3TXS3oHs3kKCZLraUA==}
|
||||
|
||||
'@cloudflare/workers-types@4.20250504.0':
|
||||
resolution: {integrity: sha512-/70Kb5vrqj+O0krOuS8LVLiCeDuCGzQy4X+wGGs4/rHv0gZJulv7Uj5YlUjIaRemK/Dyrzlk7WNJwTy8yv0cIw==}
|
||||
'@cloudflare/workers-types@4.20250505.0':
|
||||
resolution: {integrity: sha512-pLQ/UaCupEy3fTTfy7yCR7FuAbawvCohYAdadGHPUfzssksA9MhkqBLlzYWRwIoC34R8grVn4XOCknEg+NMr0Q==}
|
||||
|
||||
'@colors/colors@1.6.0':
|
||||
resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==}
|
||||
@ -872,8 +872,8 @@ packages:
|
||||
'@push.rocks/qenv@6.1.0':
|
||||
resolution: {integrity: sha512-1FUFMlSVwFSFg8LbqfkzJ2LLP4lMGApUtgOpsvrde6+AxBmB4gjoNgCUH7z3xXfDAtYqcrtSELXBNE0xVL1MqQ==}
|
||||
|
||||
'@push.rocks/smartacme@7.2.4':
|
||||
resolution: {integrity: sha512-0ciewRheDAwv0ER0ZyLQVLAn0ZoG1++ibSZ14HoXn8GOTOLyuRLWLNDQL2fI4LtLxeaNYmQUS+f7tt4KaZb/UA==}
|
||||
'@push.rocks/smartacme@7.3.2':
|
||||
resolution: {integrity: sha512-pfNd31wqvEn/2Bi9qZGCzvpV6/5V1jB9xOuWlsUTp4RihDVwQq2/se69pUeXDd1smWOM1yF4zq+45VO5DMDsCg==}
|
||||
|
||||
'@push.rocks/smartarchive@3.0.8':
|
||||
resolution: {integrity: sha512-1jPmR0b7hXmjYQoRiTlRXrIbZcdcFmSdGOfznufjcDpGPe86Km0d8TBnzqghTx4dTihzKC67IxAaz/DM3lvxpA==}
|
||||
@ -1567,8 +1567,8 @@ packages:
|
||||
'@tsclass/tsclass@8.2.1':
|
||||
resolution: {integrity: sha512-bRDCfJTipsTcK6eEokWdsOR1mGCQFeM7zTg6PRHzbxTWQcWQD9AhEr2q3CrPcmAbvIS7fvkO6/pU/mPm1MZxhQ==}
|
||||
|
||||
'@tsclass/tsclass@9.1.0':
|
||||
resolution: {integrity: sha512-PkG1bXK/bqVtxaRHje+iJHjtcdRHLHrNTOkzqh+jv2A7mgiyNo2YBJIl4eEJLkw1X3FwEFU4vCAtsegSmJgRug==}
|
||||
'@tsclass/tsclass@9.2.0':
|
||||
resolution: {integrity: sha512-A6ULEkQfYgOnCKQVQRt26O7PRzFo4PE2EoD25RAtnuFuVrNwGynYC20Vee2c8KAOyI7nQ/LaREki9KAX4AHOHQ==}
|
||||
|
||||
'@types/accepts@1.3.7':
|
||||
resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==}
|
||||
@ -4760,7 +4760,7 @@ snapshots:
|
||||
'@api.global/typedrequest': 3.1.10
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
'@api.global/typedsocket': 3.0.1
|
||||
'@cloudflare/workers-types': 4.20250504.0
|
||||
'@cloudflare/workers-types': 4.20250505.0
|
||||
'@design.estate/dees-comms': 1.0.27
|
||||
'@push.rocks/lik': 6.2.2
|
||||
'@push.rocks/smartchok': 1.0.34
|
||||
@ -4827,7 +4827,7 @@ snapshots:
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartrequest': 2.1.0
|
||||
'@push.rocks/smartstring': 4.0.15
|
||||
'@tsclass/tsclass': 9.1.0
|
||||
'@tsclass/tsclass': 9.2.0
|
||||
cloudflare: 4.2.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
@ -5671,7 +5671,7 @@ snapshots:
|
||||
|
||||
'@cloudflare/workers-types@4.20250303.0': {}
|
||||
|
||||
'@cloudflare/workers-types@4.20250504.0': {}
|
||||
'@cloudflare/workers-types@4.20250505.0': {}
|
||||
|
||||
'@colors/colors@1.6.0': {}
|
||||
|
||||
@ -6284,7 +6284,7 @@ snapshots:
|
||||
'@push.rocks/smartlog': 3.0.7
|
||||
'@push.rocks/smartpath': 5.0.18
|
||||
|
||||
'@push.rocks/smartacme@7.2.4(@aws-sdk/credential-providers@3.798.0)(socks@2.8.4)':
|
||||
'@push.rocks/smartacme@7.3.2(@aws-sdk/credential-providers@3.798.0)(socks@2.8.4)':
|
||||
dependencies:
|
||||
'@api.global/typedserver': 3.0.74
|
||||
'@apiclient.xyz/cloudflare': 6.4.1
|
||||
@ -6292,13 +6292,15 @@ snapshots:
|
||||
'@push.rocks/smartdata': 5.15.1(@aws-sdk/credential-providers@3.798.0)(socks@2.8.4)
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
'@push.rocks/smartdns': 6.2.2
|
||||
'@push.rocks/smartfile': 11.2.0
|
||||
'@push.rocks/smartlog': 3.0.7
|
||||
'@push.rocks/smartnetwork': 4.0.1
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartrequest': 2.1.0
|
||||
'@push.rocks/smartstring': 4.0.15
|
||||
'@push.rocks/smarttime': 4.1.1
|
||||
'@push.rocks/smartunique': 3.0.9
|
||||
'@tsclass/tsclass': 9.1.0
|
||||
'@tsclass/tsclass': 9.2.0
|
||||
acme-client: 5.4.0
|
||||
transitivePeerDependencies:
|
||||
- '@aws-sdk/credential-providers'
|
||||
@ -7736,7 +7738,7 @@ snapshots:
|
||||
dependencies:
|
||||
type-fest: 4.40.1
|
||||
|
||||
'@tsclass/tsclass@9.1.0':
|
||||
'@tsclass/tsclass@9.2.0':
|
||||
dependencies:
|
||||
type-fest: 4.40.1
|
||||
|
||||
|
@ -384,7 +384,7 @@ Listen for certificate events via EventEmitter:
|
||||
- **SmartProxy**:
|
||||
- `certificate` (domain, publicKey, privateKey, expiryDate, source, isRenewal)
|
||||
|
||||
Provide a `certProvider(domain)` in SmartProxy settings to supply static certs or return `'http01'`.
|
||||
Provide a `certProvisionFunction(domain)` in SmartProxy settings to supply static certs or return `'http01'`.
|
||||
|
||||
## Configuration Options
|
||||
|
||||
@ -429,7 +429,7 @@ Provide a `certProvider(domain)` in SmartProxy settings to supply static certs o
|
||||
- `sniEnabled`, `defaultAllowedIPs`, `preserveSourceIP` (booleans)
|
||||
- Timeouts: `initialDataTimeout`, `socketTimeout`, `inactivityTimeout`, etc.
|
||||
- Socket opts: `noDelay`, `keepAlive`, `enableKeepAliveProbes`
|
||||
- `acme` (IAcmeOptions), `certProvider` (callback)
|
||||
- `acme` (IAcmeOptions), `certProvisionFunction` (callback)
|
||||
- `useNetworkProxy` (number[]), `networkProxyPort` (number)
|
||||
|
||||
## Troubleshooting
|
||||
|
@ -1,29 +1,18 @@
|
||||
# Project Simplification Plan
|
||||
# Plan: Fallback to NetworkProxy on Missing SNI
|
||||
|
||||
This document outlines a roadmap to simplify and refactor the SmartProxy & NetworkProxy codebase for better maintainability, reduced duplication, and clearer configuration.
|
||||
When a TLS ClientHello arrives without an SNI extension, we currently send a TLS alert and close the connection. Instead, if NetworkProxy is in use, we want to forward the connection to the network proxy first, and only issue the TLS error if proxying is not possible.
|
||||
|
||||
## Goals
|
||||
- Eliminate duplicate code and shared types
|
||||
- Unify certificate management flow across components
|
||||
- Simplify configuration schemas and option handling
|
||||
- Centralize plugin imports and module interfaces
|
||||
- Strengthen type safety and linting
|
||||
- Improve test coverage and CI integration
|
||||
- Allow TLS connections with no SNI to be forwarded to NetworkProxy when configured for any domain.
|
||||
- Only send a TLS unrecognized_name alert if proxy forwarding is unavailable or fails.
|
||||
|
||||
## Plan
|
||||
- [x] Extract all shared interfaces and types (e.g., certificate, proxy, domain configs) into a common `ts/common` module
|
||||
- [x] Consolidate ACME/Port80Handler logic:
|
||||
- [x] Merge standalone Port80Handler into a single certificate service
|
||||
- [x] Remove duplicate ACME setup in SmartProxy and NetworkProxy
|
||||
- [x] Unify configuration options:
|
||||
- [x] Merge `INetworkProxyOptions.acme`, `IPort80HandlerOptions`, and `port80HandlerConfig` into one schema
|
||||
- [x] Deprecate old option names and provide clear upgrade path
|
||||
- [x] Centralize plugin imports in `ts/plugins.ts` and update all modules to use it
|
||||
- [x] Remove legacy or unused code paths (e.g., old HTTP/2 fallback logic if obsolete)
|
||||
- [ ] Enhance and expand test coverage:
|
||||
- Add unit tests for certificate issuance, renewal, and error handling
|
||||
- Add integration tests for HTTP challenge routing and request forwarding
|
||||
- [ ] Update main README.md with architecture overview and configuration guide
|
||||
- [ ] Review and prune external dependencies no longer needed
|
||||
|
||||
Once these steps are complete, the project will be cleaner, easier to understand, and simpler to extend.
|
||||
- [ ] In `ts/smartproxy/classes.pp.connectionhandler.ts`, locate the SNI-block branch in `handleStandardConnection` that checks `allowSessionTicket === false && isClientHello && !serverName`.
|
||||
- [ ] Replace the direct TLS alert/error logic with:
|
||||
- If NetworkProxy is enabled (global `useNetworkProxy` setting or an active NetworkProxy instance), call `this.handleNetworkProxyConnection(socket, record)` (passing the buffered ClientHello) before issuing a TLS alert.
|
||||
- Supply an error callback to `forwardToNetworkProxy`; if proxying fails or no NetworkProxy is available, fall back to the original TLS alert sequence.
|
||||
- [ ] Ensure existing metrics and cleanup (`record.incomingTerminationReason`, termination stats) are correctly tracked in the forwarding error path.
|
||||
- [ ] Add or update unit tests to simulate a TLS ClientHello without SNI on port 443 and verify:
|
||||
- When NetworkProxy is enabled, the connection is forwarded to the proxy.
|
||||
- If proxy forwarding fails or the domain is not configured, a TLS alert is sent and the socket is closed.
|
||||
- [ ] Run `pnpm test` to confirm no regressions and that the new behavior is correctly covered.
|
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartproxy',
|
||||
version: '10.0.5',
|
||||
version: '10.1.0',
|
||||
description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.'
|
||||
}
|
||||
|
@ -77,9 +77,9 @@ export interface IDomainForwardConfig {
|
||||
* 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)
|
||||
contactEmail?: string; // Email for Let's Encrypt account
|
||||
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
|
||||
|
@ -357,7 +357,7 @@ export class CertificateManager {
|
||||
// Build and configure Port80Handler
|
||||
this.port80Handler = buildPort80Handler({
|
||||
port: this.options.acme.port,
|
||||
contactEmail: this.options.acme.contactEmail,
|
||||
accountEmail: this.options.acme.accountEmail,
|
||||
useProduction: this.options.acme.useProduction,
|
||||
httpsRedirectPort: this.options.port, // Redirect to our HTTPS port
|
||||
enabled: this.options.acme.enabled,
|
||||
|
@ -76,7 +76,7 @@ export class NetworkProxy implements IMetricsTracker {
|
||||
acme: {
|
||||
enabled: optionsArg.acme?.enabled || false,
|
||||
port: optionsArg.acme?.port || 80,
|
||||
contactEmail: optionsArg.acme?.contactEmail || 'admin@example.com',
|
||||
accountEmail: optionsArg.acme?.accountEmail || 'admin@example.com',
|
||||
useProduction: optionsArg.acme?.useProduction || false, // Default to staging for safety
|
||||
renewThresholdDays: optionsArg.acme?.renewThresholdDays || 30,
|
||||
autoRenew: optionsArg.acme?.autoRenew !== false, // Default to true
|
||||
|
@ -10,21 +10,6 @@ import type {
|
||||
IAcmeOptions
|
||||
} from '../common/types.js';
|
||||
// (fs and path I/O moved to CertProvisioner)
|
||||
// ACME HTTP-01 challenge handler storing tokens in memory (diskless)
|
||||
class DisklessHttp01Handler {
|
||||
private storage: Map<string, string>;
|
||||
constructor(storage: Map<string, string>) { this.storage = storage; }
|
||||
public getSupportedTypes(): string[] { return ['http-01']; }
|
||||
public async prepare(ch: any): Promise<void> {
|
||||
this.storage.set(ch.token, ch.keyAuthorization);
|
||||
}
|
||||
public async verify(ch: any): Promise<void> {
|
||||
return;
|
||||
}
|
||||
public async cleanup(ch: any): Promise<void> {
|
||||
this.storage.delete(ch.token);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom error classes for better error handling
|
||||
@ -80,11 +65,11 @@ interface IDomainCertificate {
|
||||
*/
|
||||
export class Port80Handler extends plugins.EventEmitter {
|
||||
private domainCertificates: Map<string, IDomainCertificate>;
|
||||
// In-memory storage for ACME HTTP-01 challenge tokens
|
||||
private acmeHttp01Storage: Map<string, string> = new Map();
|
||||
// SmartAcme instance for certificate management
|
||||
private smartAcme: plugins.smartacme.SmartAcme | null = null;
|
||||
private smartAcmeHttp01Handler!: plugins.smartacme.handlers.Http01MemoryHandler;
|
||||
private server: plugins.http.Server | null = null;
|
||||
|
||||
// Renewal scheduling is handled externally by SmartProxy
|
||||
// (Removed internal renewal timer)
|
||||
private isShuttingDown: boolean = false;
|
||||
@ -101,7 +86,7 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
// Default options
|
||||
this.options = {
|
||||
port: options.port ?? 80,
|
||||
contactEmail: options.contactEmail ?? 'admin@example.com',
|
||||
accountEmail: options.accountEmail ?? 'admin@example.com',
|
||||
useProduction: options.useProduction ?? false, // Safer default: staging
|
||||
httpsRedirectPort: options.httpsRedirectPort ?? 443,
|
||||
enabled: options.enabled ?? true, // Enable by default
|
||||
@ -131,13 +116,14 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
console.log('Port80Handler is disabled, skipping start');
|
||||
return;
|
||||
}
|
||||
// Initialize SmartAcme for ACME challenge management (diskless HTTP handler)
|
||||
// Initialize SmartAcme with in-memory HTTP-01 challenge handler
|
||||
if (this.options.enabled) {
|
||||
this.smartAcmeHttp01Handler = new plugins.smartacme.handlers.Http01MemoryHandler();
|
||||
this.smartAcme = new plugins.smartacme.SmartAcme({
|
||||
accountEmail: this.options.contactEmail,
|
||||
certManager: new plugins.smartacme.MemoryCertManager(),
|
||||
accountEmail: this.options.accountEmail,
|
||||
certManager: new plugins.smartacme.certmanagers.MemoryCertManager(),
|
||||
environment: this.options.useProduction ? 'production' : 'integration',
|
||||
challengeHandlers: [ new DisklessHttp01Handler(this.acmeHttp01Storage) ],
|
||||
challengeHandlers: [ this.smartAcmeHttp01Handler ],
|
||||
challengePriority: ['http-01'],
|
||||
});
|
||||
await this.smartAcme.start();
|
||||
@ -448,17 +434,12 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
res.end('Not found');
|
||||
return;
|
||||
}
|
||||
// Serve challenge response from in-memory storage
|
||||
const token = req.url.split('/').pop() || '';
|
||||
const keyAuth = this.acmeHttp01Storage.get(token);
|
||||
if (keyAuth) {
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', 'text/plain');
|
||||
res.end(keyAuth);
|
||||
console.log(`Served ACME challenge response for ${domain}`);
|
||||
// Delegate to Http01MemoryHandler
|
||||
if (this.smartAcmeHttp01Handler) {
|
||||
this.smartAcmeHttp01Handler.handleRequest(req, res);
|
||||
} else {
|
||||
res.statusCode = 404;
|
||||
res.end('Challenge token not found');
|
||||
res.statusCode = 500;
|
||||
res.end('ACME HTTP-01 handler not initialized');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ export class CertProvisioner extends plugins.EventEmitter {
|
||||
private domainConfigs: IDomainConfig[];
|
||||
private port80Handler: Port80Handler;
|
||||
private networkProxyBridge: NetworkProxyBridge;
|
||||
private certProvider?: (domain: string) => Promise<ISmartProxyCertProvisionObject>;
|
||||
private certProvisionFunction?: (domain: string) => Promise<ISmartProxyCertProvisionObject>;
|
||||
private forwardConfigs: Array<{ domain: string; forwardConfig?: { ip: string; port: number }; acmeForwardConfig?: { ip: string; port: number }; sslRedirect: boolean }>;
|
||||
private renewThresholdDays: number;
|
||||
private renewCheckIntervalHours: number;
|
||||
@ -46,7 +46,7 @@ export class CertProvisioner extends plugins.EventEmitter {
|
||||
this.domainConfigs = domainConfigs;
|
||||
this.port80Handler = port80Handler;
|
||||
this.networkProxyBridge = networkProxyBridge;
|
||||
this.certProvider = certProvider;
|
||||
this.certProvisionFunction = certProvider;
|
||||
this.renewThresholdDays = renewThresholdDays;
|
||||
this.renewCheckIntervalHours = renewCheckIntervalHours;
|
||||
this.autoRenew = autoRenew;
|
||||
@ -81,20 +81,28 @@ export class CertProvisioner extends plugins.EventEmitter {
|
||||
// Initial provisioning for all domains
|
||||
const domains = this.domainConfigs.flatMap(cfg => cfg.domains);
|
||||
for (const domain of domains) {
|
||||
// Skip wildcard domains
|
||||
if (domain.includes('*')) continue;
|
||||
const isWildcard = domain.includes('*');
|
||||
let provision: ISmartProxyCertProvisionObject | 'http01' = 'http01';
|
||||
if (this.certProvider) {
|
||||
if (this.certProvisionFunction) {
|
||||
try {
|
||||
provision = await this.certProvider(domain);
|
||||
provision = await this.certProvisionFunction(domain);
|
||||
} catch (err) {
|
||||
console.error(`certProvider error for ${domain}:`, err);
|
||||
}
|
||||
} else if (isWildcard) {
|
||||
// No certProvider: cannot handle wildcard without DNS-01 support
|
||||
console.warn(`Skipping wildcard domain without certProvisionFunction: ${domain}`);
|
||||
continue;
|
||||
}
|
||||
if (provision === 'http01') {
|
||||
if (isWildcard) {
|
||||
console.warn(`Skipping HTTP-01 for wildcard domain: ${domain}`);
|
||||
continue;
|
||||
}
|
||||
this.provisionMap.set(domain, 'http01');
|
||||
this.port80Handler.addDomain({ domainName: domain, sslRedirect: true, acmeMaintenance: true });
|
||||
} else {
|
||||
// Static certificate (e.g., DNS-01 provisioned or user-provided) supports wildcard domains
|
||||
this.provisionMap.set(domain, 'static');
|
||||
const certObj = provision as plugins.tsclass.network.ICert;
|
||||
const certData: ICertificateData = {
|
||||
@ -120,8 +128,8 @@ export class CertProvisioner extends plugins.EventEmitter {
|
||||
try {
|
||||
if (type === 'http01') {
|
||||
await this.port80Handler.renewCertificate(domain);
|
||||
} else if (type === 'static' && this.certProvider) {
|
||||
const provision2 = await this.certProvider(domain);
|
||||
} else if (type === 'static' && this.certProvisionFunction) {
|
||||
const provision2 = await this.certProvisionFunction(domain);
|
||||
if (provision2 !== 'http01') {
|
||||
const certObj = provision2 as plugins.tsclass.network.ICert;
|
||||
const certData: ICertificateData = {
|
||||
@ -162,18 +170,22 @@ export class CertProvisioner extends plugins.EventEmitter {
|
||||
* @param domain Domain name to provision
|
||||
*/
|
||||
public async requestCertificate(domain: string): Promise<void> {
|
||||
// Skip wildcard domains
|
||||
if (domain.includes('*')) {
|
||||
throw new Error(`Cannot request certificate for wildcard domain: ${domain}`);
|
||||
}
|
||||
const isWildcard = domain.includes('*');
|
||||
// Determine provisioning method
|
||||
let provision: ISmartProxyCertProvisionObject | 'http01' = 'http01';
|
||||
if (this.certProvider) {
|
||||
provision = await this.certProvider(domain);
|
||||
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 {
|
||||
// Static certificate (e.g., DNS-01 provisioned) supports wildcards
|
||||
const certObj = provision as plugins.tsclass.network.ICert;
|
||||
const certData: ICertificateData = {
|
||||
domain: certObj.domainName,
|
||||
|
@ -2,7 +2,7 @@ import * as plugins from '../plugins.js';
|
||||
import type {
|
||||
IConnectionRecord,
|
||||
IDomainConfig,
|
||||
IPortProxySettings,
|
||||
ISmartProxyOptions,
|
||||
} from './classes.pp.interfaces.js';
|
||||
import { ConnectionManager } from './classes.pp.connectionmanager.js';
|
||||
import { SecurityManager } from './classes.pp.securitymanager.js';
|
||||
@ -17,7 +17,7 @@ import { PortRangeManager } from './classes.pp.portrangemanager.js';
|
||||
*/
|
||||
export class ConnectionHandler {
|
||||
constructor(
|
||||
private settings: IPortProxySettings,
|
||||
private settings: ISmartProxyOptions,
|
||||
private connectionManager: ConnectionManager,
|
||||
private securityManager: SecurityManager,
|
||||
private domainConfigManager: DomainConfigManager,
|
||||
@ -557,13 +557,41 @@ export class ConnectionHandler {
|
||||
this.tlsManager.isClientHello(chunk) &&
|
||||
!serverName
|
||||
) {
|
||||
// Block ClientHello without SNI when allowSessionTicket is false
|
||||
console.log(
|
||||
`[${connectionId}] No SNI detected in ClientHello and allowSessionTicket=false. ` +
|
||||
`Sending warning unrecognized_name alert to encourage immediate retry with SNI.`
|
||||
// Missing SNI: forward to NetworkProxy if available
|
||||
const proxyInstance = this.networkProxyBridge.getNetworkProxy();
|
||||
if (proxyInstance) {
|
||||
if (this.settings.enableDetailedLogging) {
|
||||
console.log(
|
||||
`[${connectionId}] No SNI in ClientHello; forwarding to NetworkProxy.`
|
||||
);
|
||||
}
|
||||
this.networkProxyBridge.forwardToNetworkProxy(
|
||||
connectionId,
|
||||
socket,
|
||||
record,
|
||||
chunk,
|
||||
undefined,
|
||||
(_reason) => {
|
||||
// On proxy failure, send TLS unrecognized_name alert and cleanup
|
||||
if (record.incomingTerminationReason === null) {
|
||||
record.incomingTerminationReason = 'session_ticket_blocked_no_sni';
|
||||
this.connectionManager.incrementTerminationStat(
|
||||
'incoming',
|
||||
'session_ticket_blocked_no_sni'
|
||||
);
|
||||
}
|
||||
const alert = Buffer.from([0x15, 0x03, 0x03, 0x00, 0x02, 0x01, 0x70]);
|
||||
try { socket.cork(); socket.write(alert); socket.uncork(); socket.end(); }
|
||||
catch { socket.end(); }
|
||||
this.connectionManager.initiateCleanupOnce(record, 'session_ticket_blocked_no_sni');
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Fallback: send TLS unrecognized_name alert and terminate
|
||||
console.log(
|
||||
`[${connectionId}] No SNI detected and proxy unavailable; sending TLS alert.`
|
||||
);
|
||||
|
||||
// Set the termination reason first
|
||||
if (record.incomingTerminationReason === null) {
|
||||
record.incomingTerminationReason = 'session_ticket_blocked_no_sni';
|
||||
this.connectionManager.incrementTerminationStat(
|
||||
@ -571,54 +599,10 @@ export class ConnectionHandler {
|
||||
'session_ticket_blocked_no_sni'
|
||||
);
|
||||
}
|
||||
|
||||
// Create a warning-level alert for unrecognized_name
|
||||
// This encourages Chrome to retry immediately with SNI
|
||||
const serverNameUnknownAlertData = Buffer.from([
|
||||
0x15, // Alert record type
|
||||
0x03,
|
||||
0x03, // TLS 1.2 version
|
||||
0x00,
|
||||
0x02, // Length
|
||||
0x01, // Warning alert level (not fatal)
|
||||
0x70, // unrecognized_name alert (code 112)
|
||||
]);
|
||||
|
||||
try {
|
||||
// Use cork/uncork to ensure the alert is sent as a single packet
|
||||
socket.cork();
|
||||
const writeSuccessful = socket.write(serverNameUnknownAlertData);
|
||||
socket.uncork();
|
||||
socket.end();
|
||||
|
||||
// Function to handle the clean socket termination - but more gradually
|
||||
const finishConnection = () => {
|
||||
this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
|
||||
};
|
||||
|
||||
if (writeSuccessful) {
|
||||
// Wait longer before ending connection to ensure alert is processed by client
|
||||
setTimeout(finishConnection, 200); // Increased from 50ms to 200ms
|
||||
} else {
|
||||
// If the kernel buffer was full, wait for the drain event
|
||||
socket.once('drain', () => {
|
||||
// Wait longer after drain as well
|
||||
setTimeout(finishConnection, 200);
|
||||
});
|
||||
|
||||
// Safety timeout is increased too
|
||||
setTimeout(() => {
|
||||
socket.removeAllListeners('drain');
|
||||
finishConnection();
|
||||
}, 400); // Increased from 250ms to 400ms
|
||||
}
|
||||
} catch (err) {
|
||||
// If we can't send the alert, fall back to immediate termination
|
||||
console.log(`[${connectionId}] Error sending TLS alert: ${err.message}`);
|
||||
socket.end();
|
||||
this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
|
||||
}
|
||||
|
||||
const alert = Buffer.from([0x15, 0x03, 0x03, 0x00, 0x02, 0x01, 0x70]);
|
||||
try { socket.cork(); socket.write(alert); socket.uncork(); socket.end(); }
|
||||
catch { socket.end(); }
|
||||
this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import type { IConnectionRecord, IPortProxySettings } from './classes.pp.interfaces.js';
|
||||
import type { IConnectionRecord, ISmartProxyOptions } from './classes.pp.interfaces.js';
|
||||
import { SecurityManager } from './classes.pp.securitymanager.js';
|
||||
import { TimeoutManager } from './classes.pp.timeoutmanager.js';
|
||||
|
||||
@ -14,7 +14,7 @@ export class ConnectionManager {
|
||||
} = { incoming: {}, outgoing: {} };
|
||||
|
||||
constructor(
|
||||
private settings: IPortProxySettings,
|
||||
private settings: ISmartProxyOptions,
|
||||
private securityManager: SecurityManager,
|
||||
private timeoutManager: TimeoutManager
|
||||
) {}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import type { IDomainConfig, IPortProxySettings } from './classes.pp.interfaces.js';
|
||||
import type { IDomainConfig, ISmartProxyOptions } from './classes.pp.interfaces.js';
|
||||
|
||||
/**
|
||||
* Manages domain configurations and target selection
|
||||
@ -8,7 +8,7 @@ export class DomainConfigManager {
|
||||
// Track round-robin indices for domain configs
|
||||
private domainTargetIndices: Map<IDomainConfig, number> = new Map();
|
||||
|
||||
constructor(private settings: IPortProxySettings) {}
|
||||
constructor(private settings: ISmartProxyOptions) {}
|
||||
|
||||
/**
|
||||
* Updates the domain configurations
|
||||
|
@ -22,7 +22,7 @@ export interface IDomainConfig {
|
||||
|
||||
/** Port proxy settings including global allowed port ranges */
|
||||
import type { IAcmeOptions } from '../common/types.js';
|
||||
export interface IPortProxySettings {
|
||||
export interface ISmartProxyOptions {
|
||||
fromPort: number;
|
||||
toPort: number;
|
||||
targetIP?: string; // Global target host to proxy to, defaults to 'localhost'
|
||||
@ -91,7 +91,7 @@ export interface IPortProxySettings {
|
||||
* Optional certificate provider callback. Return 'http01' to use HTTP-01 challenges,
|
||||
* or a static certificate object for immediate provisioning.
|
||||
*/
|
||||
certProvider?: (domain: string) => Promise<ISmartProxyCertProvisionObject>;
|
||||
certProvisionFunction?: (domain: string) => Promise<ISmartProxyCertProvisionObject>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -4,7 +4,7 @@ import { Port80Handler } from '../port80handler/classes.port80handler.js';
|
||||
import { Port80HandlerEvents } from '../common/types.js';
|
||||
import { subscribeToPort80Handler } from '../common/eventUtils.js';
|
||||
import type { ICertificateData } from '../common/types.js';
|
||||
import type { IConnectionRecord, IPortProxySettings, IDomainConfig } from './classes.pp.interfaces.js';
|
||||
import type { IConnectionRecord, ISmartProxyOptions, IDomainConfig } from './classes.pp.interfaces.js';
|
||||
|
||||
/**
|
||||
* Manages NetworkProxy integration for TLS termination
|
||||
@ -13,7 +13,7 @@ export class NetworkProxyBridge {
|
||||
private networkProxy: NetworkProxy | null = null;
|
||||
private port80Handler: Port80Handler | null = null;
|
||||
|
||||
constructor(private settings: IPortProxySettings) {}
|
||||
constructor(private settings: ISmartProxyOptions) {}
|
||||
|
||||
/**
|
||||
* Set the Port80Handler to use for certificate management
|
||||
|
@ -1,10 +1,10 @@
|
||||
import type{ IPortProxySettings } from './classes.pp.interfaces.js';
|
||||
import type{ ISmartProxyOptions } from './classes.pp.interfaces.js';
|
||||
|
||||
/**
|
||||
* Manages port ranges and port-based configuration
|
||||
*/
|
||||
export class PortRangeManager {
|
||||
constructor(private settings: IPortProxySettings) {}
|
||||
constructor(private settings: ISmartProxyOptions) {}
|
||||
|
||||
/**
|
||||
* Get all ports that should be listened on
|
||||
|
@ -1,5 +1,5 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import type { IPortProxySettings } from './classes.pp.interfaces.js';
|
||||
import type { ISmartProxyOptions } from './classes.pp.interfaces.js';
|
||||
|
||||
/**
|
||||
* Handles security aspects like IP tracking, rate limiting, and authorization
|
||||
@ -8,7 +8,7 @@ export class SecurityManager {
|
||||
private connectionsByIP: Map<string, Set<string>> = new Map();
|
||||
private connectionRateByIP: Map<string, number[]> = new Map();
|
||||
|
||||
constructor(private settings: IPortProxySettings) {}
|
||||
constructor(private settings: ISmartProxyOptions) {}
|
||||
|
||||
/**
|
||||
* Get connections count by IP
|
||||
|
@ -1,10 +1,10 @@
|
||||
import type { IConnectionRecord, IPortProxySettings } from './classes.pp.interfaces.js';
|
||||
import type { IConnectionRecord, ISmartProxyOptions } from './classes.pp.interfaces.js';
|
||||
|
||||
/**
|
||||
* Manages timeouts and inactivity tracking for connections
|
||||
*/
|
||||
export class TimeoutManager {
|
||||
constructor(private settings: IPortProxySettings) {}
|
||||
constructor(private settings: ISmartProxyOptions) {}
|
||||
|
||||
/**
|
||||
* Ensure timeout values don't exceed Node.js max safe integer
|
||||
|
@ -1,5 +1,5 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import type { IPortProxySettings } from './classes.pp.interfaces.js';
|
||||
import type { ISmartProxyOptions } from './classes.pp.interfaces.js';
|
||||
import { SniHandler } from './classes.pp.snihandler.js';
|
||||
|
||||
/**
|
||||
@ -16,7 +16,7 @@ interface IConnectionInfo {
|
||||
* Manages TLS-related operations including SNI extraction and validation
|
||||
*/
|
||||
export class TlsManager {
|
||||
constructor(private settings: IPortProxySettings) {}
|
||||
constructor(private settings: ISmartProxyOptions) {}
|
||||
|
||||
/**
|
||||
* Check if a data chunk appears to be a TLS handshake
|
||||
|
@ -13,8 +13,8 @@ import { CertProvisioner } from './classes.pp.certprovisioner.js';
|
||||
import type { ICertificateData } from '../common/types.js';
|
||||
import { buildPort80Handler } from '../common/acmeFactory.js';
|
||||
|
||||
import type { IPortProxySettings, IDomainConfig } from './classes.pp.interfaces.js';
|
||||
export type { IPortProxySettings, IDomainConfig };
|
||||
import type { ISmartProxyOptions, IDomainConfig } from './classes.pp.interfaces.js';
|
||||
export type { ISmartProxyOptions as IPortProxySettings, IDomainConfig };
|
||||
|
||||
/**
|
||||
* SmartProxy - Main class that coordinates all components
|
||||
@ -39,7 +39,7 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
// CertProvisioner for unified certificate workflows
|
||||
private certProvisioner?: CertProvisioner;
|
||||
|
||||
constructor(settingsArg: IPortProxySettings) {
|
||||
constructor(settingsArg: ISmartProxyOptions) {
|
||||
super();
|
||||
// Set reasonable defaults for all settings
|
||||
this.settings = {
|
||||
@ -78,7 +78,7 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
this.settings.acme = {
|
||||
enabled: false,
|
||||
port: 80,
|
||||
contactEmail: 'admin@example.com',
|
||||
accountEmail: 'admin@example.com',
|
||||
useProduction: false,
|
||||
renewThresholdDays: 30,
|
||||
autoRenew: true,
|
||||
@ -119,7 +119,7 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
/**
|
||||
* The settings for the port proxy
|
||||
*/
|
||||
public settings: IPortProxySettings;
|
||||
public settings: ISmartProxyOptions;
|
||||
|
||||
/**
|
||||
* Initialize the Port80Handler for ACME certificate management
|
||||
@ -165,7 +165,7 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
this.settings.domainConfigs,
|
||||
this.port80Handler,
|
||||
this.networkProxyBridge,
|
||||
this.settings.certProvider,
|
||||
this.settings.certProvisionFunction,
|
||||
acme.renewThresholdDays!,
|
||||
acme.renewCheckIntervalHours!,
|
||||
acme.autoRenew!,
|
||||
@ -391,16 +391,23 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
if (this.port80Handler && this.settings.acme?.enabled) {
|
||||
for (const domainConfig of newDomainConfigs) {
|
||||
for (const domain of domainConfig.domains) {
|
||||
if (domain.includes('*')) continue;
|
||||
let provision = 'http01' as string | plugins.tsclass.network.ICert;
|
||||
if (this.settings.certProvider) {
|
||||
const isWildcard = domain.includes('*');
|
||||
let provision: string | plugins.tsclass.network.ICert = 'http01';
|
||||
if (this.settings.certProvisionFunction) {
|
||||
try {
|
||||
provision = await this.settings.certProvider(domain);
|
||||
provision = await this.settings.certProvisionFunction(domain);
|
||||
} catch (err) {
|
||||
console.log(`certProvider error for ${domain}: ${err}`);
|
||||
}
|
||||
} else if (isWildcard) {
|
||||
console.warn(`Skipping wildcard domain without certProvisionFunction: ${domain}`);
|
||||
continue;
|
||||
}
|
||||
if (provision === 'http01') {
|
||||
if (isWildcard) {
|
||||
console.warn(`Skipping HTTP-01 for wildcard domain: ${domain}`);
|
||||
continue;
|
||||
}
|
||||
this.port80Handler.addDomain({
|
||||
domainName: domain,
|
||||
sslRedirect: true,
|
||||
@ -408,6 +415,7 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
});
|
||||
console.log(`Registered domain ${domain} with Port80Handler for HTTP-01`);
|
||||
} else {
|
||||
// Static certificate (e.g., DNS-01 provisioned) supports wildcards
|
||||
const certObj = provision as plugins.tsclass.network.ICert;
|
||||
const certData: ICertificateData = {
|
||||
domain: certObj.domainName,
|
||||
|
Reference in New Issue
Block a user