Compare commits

...

5 Commits

Author SHA1 Message Date
d81cf94876 3.37.0
Some checks failed
Default (tags) / security (push) Failing after 10m56s
Default (tags) / test (push) Has been cancelled
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-03-11 12:56:04 +00:00
8d06f1533e feat(portproxy): Add ACME certificate management options to PortProxy, update ACME settings handling, and bump dependency versions 2025-03-11 12:56:03 +00:00
223be61c8d 3.35.0 2025-03-11 12:45:55 +00:00
6a693f4d86 feat(NetworkProxy): Integrate Port80Handler for automatic ACME certificate management
- Add ACME certificate management capabilities to NetworkProxy
- Implement automatic certificate issuance and renewal
- Add SNI support for serving the correct certificates
- Create certificate storage and caching system
- Enable dynamic certificate issuance for new domains
- Support automatic HTTP-to-HTTPS redirects for secured domains

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-11 12:45:22 +00:00
27a2bcb556 feat(NetworkProxy): Add support for array-based destinations and integration with PortProxy
- Update NetworkProxy to support new IReverseProxyConfig interface with destinationIps[] and destinationPorts[]
- Add load balancing with round-robin selection of destination endpoints
- Create automatic conversion of PortProxy domain configs to NetworkProxy configs
- Implement backward compatibility to ensure tests continue to work

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-11 12:34:24 +00:00
6 changed files with 758 additions and 74 deletions

View File

@ -1,5 +1,13 @@
# Changelog
## 2025-03-11 - 3.37.0 - feat(portproxy)
Add ACME certificate management options to PortProxy, update ACME settings handling, and bump dependency versions
- Bumped version in package.json from 3.34.0 to 3.36.0 and updated commitinfo accordingly
- Updated dependencies: @push.rocks/tapbundle to ^5.5.10, @types/node to ^22.13.10, and @tsclass/tsclass to ^5.0.0
- Added ACME certificate management configuration to PortProxy settings (acme options, updateAcmeSettings, requestCertificate)
- Enhanced sync of domain configs to NetworkProxy with fallback for missing default certificates
## 2025-03-11 - 3.34.0 - feat(core)
Improve wildcard domain matching and enhance NetworkProxy integration in PortProxy. Added support for TLD wildcards and complex wildcard patterns in the router, and refactored TLS renegotiation handling for stricter SNI enforcement.

View File

@ -1,8 +1,8 @@
{
"name": "@push.rocks/smartproxy",
"version": "3.34.0",
"version": "3.37.0",
"private": false,
"description": "A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, and dynamic routing with authentication options.",
"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",
"typings": "dist_ts/index.d.ts",
"type": "module",
@ -18,8 +18,8 @@
"@git.zone/tsbuild": "^2.2.6",
"@git.zone/tsrun": "^1.2.44",
"@git.zone/tstest": "^1.0.77",
"@push.rocks/tapbundle": "^5.5.6",
"@types/node": "^22.13.9",
"@push.rocks/tapbundle": "^5.5.10",
"@types/node": "^22.13.10",
"typescript": "^5.8.2"
},
"dependencies": {
@ -28,7 +28,7 @@
"@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartrequest": "^2.0.23",
"@push.rocks/smartstring": "^4.0.15",
"@tsclass/tsclass": "^4.4.3",
"@tsclass/tsclass": "^5.0.0",
"@types/minimatch": "^5.1.2",
"@types/ws": "^8.18.0",
"acme-client": "^5.4.0",

119
pnpm-lock.yaml generated
View File

@ -24,8 +24,8 @@ importers:
specifier: ^4.0.15
version: 4.0.15
'@tsclass/tsclass':
specifier: ^4.4.3
version: 4.4.3
specifier: ^5.0.0
version: 5.0.0
'@types/minimatch':
specifier: ^5.1.2
version: 5.1.2
@ -55,11 +55,11 @@ importers:
specifier: ^1.0.77
version: 1.0.96(@aws-sdk/credential-providers@3.758.0)(socks@2.8.4)(typescript@5.8.2)
'@push.rocks/tapbundle':
specifier: ^5.5.6
version: 5.5.6(@aws-sdk/credential-providers@3.758.0)(socks@2.8.4)
specifier: ^5.5.10
version: 5.5.10(@aws-sdk/credential-providers@3.758.0)(socks@2.8.4)
'@types/node':
specifier: ^22.13.9
version: 22.13.9
specifier: ^22.13.10
version: 22.13.10
typescript:
specifier: ^5.8.2
version: 5.8.2
@ -941,8 +941,8 @@ packages:
'@push.rocks/smartyaml@2.0.5':
resolution: {integrity: sha512-tBcf+HaOIfeEsTMwgUZDtZERCxXQyRsWO8Ar5DjBdiSRchbhVGZQEBzXswMS0W5ZoRenjgPK+4tPW3JQGRTfbg==}
'@push.rocks/tapbundle@5.5.6':
resolution: {integrity: sha512-V6u+nZwt4fNccxbm3ztZgHr/QAj/uKhaaOUFgtaae0jzYdds4jNEI+mXLpfXuNMgm7Nx93Lk5XUxWKTI8drjNw==}
'@push.rocks/tapbundle@5.5.10':
resolution: {integrity: sha512-vTGzd3/kzKp8s6jrREGIKGG+87fy7grcTZIelVDpyZMtBdIi9Fe7g8EIw/reVx8oTAFSuUEAEaAT8B5NDIFStg==}
'@push.rocks/taskbuffer@3.1.7':
resolution: {integrity: sha512-QktGVJPucqQmW/QNGnscf4FAigT1H7JWKFGFdRuDEaOHKFh9qN+PXG3QY7DtZ4jfXdGLxPN4yAufDuPSAJYFnw==}
@ -1315,8 +1315,11 @@ packages:
'@tsclass/tsclass@3.0.48':
resolution: {integrity: sha512-hC65UvDlp9qvsl6OcIZXz0JNiWZ0gyzsTzbXpg215sGxopgbkOLCr6E0s4qCTnweYm95gt2AdY95uP7M7kExaQ==}
'@tsclass/tsclass@4.4.3':
resolution: {integrity: sha512-Vhp+B1UsYlwXLhIeds++CXEeCwFgRzpput4YNM7Qyhr+UQgIMFRFAs2HSI3jEE5r9c1hR9G6MkSxi2U/CLyiaA==}
'@tsclass/tsclass@4.4.4':
resolution: {integrity: sha512-YZOAF+u+r4u5rCev2uUd1KBTBdfyFdtDmcv4wuN+864lMccbdfRICR3SlJwCfYS1lbeV3QNLYGD30wjRXgvCJA==}
'@tsclass/tsclass@5.0.0':
resolution: {integrity: sha512-2X66VCk0Oe1L01j6GQHC6F9Gj7lpZPPSUTDNax7e29lm4OqBTyAzTR3ePR8coSbWBwsmRV8awLRSrSI+swlqWA==}
'@types/accepts@1.3.7':
resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==}
@ -1472,8 +1475,8 @@ packages:
'@types/node-forge@1.3.11':
resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==}
'@types/node@22.13.9':
resolution: {integrity: sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==}
'@types/node@22.13.10':
resolution: {integrity: sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==}
'@types/parse5@6.0.3':
resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==}
@ -4373,7 +4376,7 @@ snapshots:
'@push.rocks/taskbuffer': 3.1.7
'@push.rocks/webrequest': 3.0.37
'@push.rocks/webstore': 2.0.20
'@tsclass/tsclass': 4.4.3
'@tsclass/tsclass': 4.4.4
'@types/express': 4.17.21
body-parser: 1.20.3
cors: 2.8.5
@ -5231,7 +5234,7 @@ snapshots:
'@push.rocks/smartlog': 3.0.7
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartshell': 3.2.3
'@push.rocks/tapbundle': 5.5.6(@aws-sdk/credential-providers@3.758.0)(socks@2.8.4)
'@push.rocks/tapbundle': 5.5.10(@aws-sdk/credential-providers@3.758.0)(socks@2.8.4)
'@types/ws': 8.18.0
figures: 6.1.0
ws: 8.18.1
@ -5281,7 +5284,7 @@ snapshots:
'@jest/schemas': 29.6.3
'@types/istanbul-lib-coverage': 2.0.6
'@types/istanbul-reports': 3.0.4
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/yargs': 17.0.33
chalk: 4.1.2
@ -5529,7 +5532,7 @@ snapshots:
'@push.rocks/smartstring': 4.0.15
'@push.rocks/smartunique': 3.0.9
'@push.rocks/taskbuffer': 3.1.7
'@tsclass/tsclass': 4.4.3
'@tsclass/tsclass': 4.4.4
transitivePeerDependencies:
- aws-crt
@ -5551,7 +5554,7 @@ snapshots:
'@pushrocks/smartjson': 4.0.6
'@pushrocks/smartpath': 5.0.5
'@pushrocks/smartpromise': 3.1.10
'@tsclass/tsclass': 4.4.3
'@tsclass/tsclass': 4.4.4
mongodb: 4.17.2
transitivePeerDependencies:
- aws-crt
@ -5602,7 +5605,7 @@ snapshots:
'@push.rocks/smartstream': 3.2.5
'@push.rocks/smartstring': 4.0.15
'@push.rocks/smartunique': 3.0.9
'@tsclass/tsclass': 4.4.3
'@tsclass/tsclass': 4.4.4
transitivePeerDependencies:
- aws-crt
@ -5652,7 +5655,7 @@ snapshots:
'@push.rocks/smarttime': 4.1.1
'@push.rocks/smartunique': 3.0.9
'@push.rocks/taskbuffer': 3.1.7
'@tsclass/tsclass': 4.4.3
'@tsclass/tsclass': 4.4.4
mongodb: 6.14.2(@aws-sdk/credential-providers@3.758.0)(socks@2.8.4)
transitivePeerDependencies:
- '@aws-sdk/credential-providers'
@ -5764,7 +5767,7 @@ snapshots:
'@push.rocks/smartlog-interfaces@3.0.2':
dependencies:
'@api.global/typedrequest-interfaces': 2.0.2
'@tsclass/tsclass': 4.4.3
'@tsclass/tsclass': 4.4.4
'@push.rocks/smartlog@3.0.7':
dependencies:
@ -5879,7 +5882,7 @@ snapshots:
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartpuppeteer': 2.0.5(typescript@5.8.2)
'@push.rocks/smartunique': 3.0.9
'@tsclass/tsclass': 4.4.3
'@tsclass/tsclass': 4.4.4
'@types/express': 5.0.0
express: 4.21.2
pdf-lib: 1.17.1
@ -5929,7 +5932,7 @@ snapshots:
'@push.rocks/smartbucket': 3.3.7
'@push.rocks/smartfile': 11.2.0
'@push.rocks/smartpath': 5.0.18
'@tsclass/tsclass': 4.4.3
'@tsclass/tsclass': 4.4.4
'@types/s3rver': 3.7.4
s3rver: 3.7.1
transitivePeerDependencies:
@ -5952,7 +5955,7 @@ snapshots:
'@push.rocks/smartxml': 1.1.1
'@push.rocks/smartyaml': 2.0.5
'@push.rocks/webrequest': 3.0.37
'@tsclass/tsclass': 4.4.3
'@tsclass/tsclass': 4.4.4
'@push.rocks/smartsocket@2.0.27':
dependencies:
@ -6058,7 +6061,7 @@ snapshots:
'@types/js-yaml': 3.12.10
js-yaml: 3.14.1
'@push.rocks/tapbundle@5.5.6(@aws-sdk/credential-providers@3.758.0)(socks@2.8.4)':
'@push.rocks/tapbundle@5.5.10(@aws-sdk/credential-providers@3.758.0)(socks@2.8.4)':
dependencies:
'@open-wc/testing': 4.0.0
'@push.rocks/consolecolor': 2.0.2
@ -6112,7 +6115,7 @@ snapshots:
dependencies:
'@pushrocks/smartdelay': 3.0.1
'@pushrocks/smartpromise': 4.0.2
'@tsclass/tsclass': 4.4.3
'@tsclass/tsclass': 4.4.4
'@push.rocks/webstore@2.0.20':
dependencies:
@ -6654,20 +6657,24 @@ snapshots:
dependencies:
type-fest: 2.19.0
'@tsclass/tsclass@4.4.3':
'@tsclass/tsclass@4.4.4':
dependencies:
type-fest: 4.37.0
'@tsclass/tsclass@5.0.0':
dependencies:
type-fest: 4.37.0
'@types/accepts@1.3.7':
dependencies:
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/babel__code-frame@7.0.6': {}
'@types/body-parser@1.19.5':
dependencies:
'@types/connect': 3.4.38
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/buffer-json@2.0.3': {}
@ -6683,17 +6690,17 @@ snapshots:
'@types/clean-css@4.2.11':
dependencies:
'@types/node': 22.13.9
'@types/node': 22.13.10
source-map: 0.6.1
'@types/co-body@6.1.3':
dependencies:
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/qs': 6.9.18
'@types/connect@3.4.38':
dependencies:
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/content-disposition@0.5.8': {}
@ -6706,11 +6713,11 @@ snapshots:
'@types/connect': 3.4.38
'@types/express': 5.0.0
'@types/keygrip': 1.0.6
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/cors@2.8.17':
dependencies:
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/debounce@1.2.4': {}
@ -6724,14 +6731,14 @@ snapshots:
'@types/express-serve-static-core@4.19.6':
dependencies:
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/qs': 6.9.18
'@types/range-parser': 1.2.7
'@types/send': 0.17.4
'@types/express-serve-static-core@5.0.6':
dependencies:
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/qs': 6.9.18
'@types/range-parser': 1.2.7
'@types/send': 0.17.4
@ -6756,30 +6763,30 @@ snapshots:
'@types/from2@2.3.5':
dependencies:
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/fs-extra@11.0.4':
dependencies:
'@types/jsonfile': 6.1.4
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/fs-extra@9.0.13':
dependencies:
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/glob@7.2.0':
dependencies:
'@types/minimatch': 5.1.2
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/glob@8.1.0':
dependencies:
'@types/minimatch': 5.1.2
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/gunzip-maybe@1.4.2':
dependencies:
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/hast@3.0.4':
dependencies:
@ -6813,7 +6820,7 @@ snapshots:
'@types/jsonfile@6.1.4':
dependencies:
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/keygrip@1.0.6': {}
@ -6830,7 +6837,7 @@ snapshots:
'@types/http-errors': 2.0.4
'@types/keygrip': 1.0.6
'@types/koa-compose': 3.2.8
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/mdast@4.0.4':
dependencies:
@ -6848,9 +6855,9 @@ snapshots:
'@types/node-forge@1.3.11':
dependencies:
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/node@22.13.9':
'@types/node@22.13.10':
dependencies:
undici-types: 6.20.0
@ -6868,19 +6875,19 @@ snapshots:
'@types/s3rver@3.7.4':
dependencies:
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/semver@7.5.8': {}
'@types/send@0.17.4':
dependencies:
'@types/mime': 1.3.5
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/serve-static@1.15.7':
dependencies:
'@types/http-errors': 2.0.4
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/send': 0.17.4
'@types/sinon-chai@3.2.12':
@ -6900,11 +6907,11 @@ snapshots:
'@types/tar-stream@2.2.3':
dependencies:
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/through2@2.0.41':
dependencies:
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/triple-beam@1.3.5': {}
@ -6928,18 +6935,18 @@ snapshots:
'@types/whatwg-url@8.2.2':
dependencies:
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/webidl-conversions': 7.0.3
'@types/which@3.0.4': {}
'@types/ws@7.4.7':
dependencies:
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/ws@8.18.0':
dependencies:
'@types/node': 22.13.9
'@types/node': 22.13.10
'@types/yargs-parser@21.0.3': {}
@ -6949,7 +6956,7 @@ snapshots:
'@types/yauzl@2.10.3':
dependencies:
'@types/node': 22.13.9
'@types/node': 22.13.10
optional: true
'@ungap/structured-clone@1.3.0': {}
@ -7598,7 +7605,7 @@ snapshots:
dependencies:
'@types/cookie': 0.4.1
'@types/cors': 2.8.17
'@types/node': 22.13.9
'@types/node': 22.13.10
accepts: 1.3.8
base64id: 2.0.0
cookie: 0.4.2
@ -8370,7 +8377,7 @@ snapshots:
jest-util@29.7.0:
dependencies:
'@jest/types': 29.6.3
'@types/node': 22.13.9
'@types/node': 22.13.10
chalk: 4.1.2
ci-info: 3.9.0
graceful-fs: 4.2.11

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartproxy',
version: '3.34.0',
description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, and dynamic routing with authentication options.'
version: '3.37.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.'
}

View File

@ -1,5 +1,6 @@
import * as plugins from './plugins.js';
import { ProxyRouter } from './classes.router.js';
import { AcmeCertManager, CertManagerEvents } from './classes.port80handler.js';
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
@ -20,6 +21,18 @@ export interface INetworkProxyOptions {
// New settings for PortProxy integration
connectionPoolSize?: number; // Maximum connections to maintain in the pool to each backend
portProxyIntegration?: boolean; // Flag to indicate this proxy is used by PortProxy
// ACME certificate management options
acme?: {
enabled?: boolean; // Whether to enable automatic certificate management
port?: number; // Port to listen on for ACME challenges (default: 80)
contactEmail?: string; // Email for Let's Encrypt account
useProduction?: boolean; // Whether to use Let's Encrypt production (default: false for staging)
renewThresholdDays?: number; // Days before expiry to renew certificates (default: 30)
autoRenew?: boolean; // Whether to automatically renew certificates (default: true)
certificateStore?: string; // Directory to store certificates (default: ./certs)
skipConfiguredCerts?: boolean; // Skip domains that already have certificates configured
};
}
interface IWebSocketWithHeartbeat extends plugins.wsDefault {
@ -59,12 +72,19 @@ export class NetworkProxy {
private defaultCertificates: { key: string; cert: string };
private certificateCache: Map<string, { key: string; cert: string; expires?: Date }> = new Map();
// ACME certificate manager
private certManager: AcmeCertManager | null = null;
private certificateStoreDir: string;
// New connection pool for backend connections
private connectionPool: Map<string, Array<{
socket: plugins.net.Socket;
lastUsed: number;
isIdle: boolean;
}>> = new Map();
// Track round-robin positions for load balancing
private roundRobinPositions: Map<string, number> = new Map();
/**
* Creates a new NetworkProxy instance
@ -85,9 +105,33 @@ export class NetworkProxy {
},
// New defaults for PortProxy integration
connectionPoolSize: optionsArg.connectionPoolSize || 50,
portProxyIntegration: optionsArg.portProxyIntegration || false
portProxyIntegration: optionsArg.portProxyIntegration || false,
// Default ACME options
acme: {
enabled: optionsArg.acme?.enabled || false,
port: optionsArg.acme?.port || 80,
contactEmail: optionsArg.acme?.contactEmail || '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
certificateStore: optionsArg.acme?.certificateStore || './certs',
skipConfiguredCerts: optionsArg.acme?.skipConfiguredCerts || false
}
};
// Set up certificate store directory
this.certificateStoreDir = path.resolve(this.options.acme.certificateStore);
// Ensure certificate store directory exists
try {
if (!fs.existsSync(this.certificateStoreDir)) {
fs.mkdirSync(this.certificateStoreDir, { recursive: true });
this.log('info', `Created certificate store directory: ${this.certificateStoreDir}`);
}
} catch (error) {
this.log('warn', `Failed to create certificate store directory: ${error}`);
}
this.loadDefaultCertificates();
}
@ -330,17 +374,230 @@ export class NetworkProxy {
}
}
/**
* Initializes the ACME certificate manager for automatic certificate issuance
* @private
*/
private async initializeAcmeManager(): Promise<void> {
if (!this.options.acme.enabled) {
return;
}
// Create certificate manager
this.certManager = new AcmeCertManager({
port: this.options.acme.port,
contactEmail: this.options.acme.contactEmail,
useProduction: this.options.acme.useProduction,
renewThresholdDays: this.options.acme.renewThresholdDays,
httpsRedirectPort: this.options.port, // Redirect to our HTTPS port
renewCheckIntervalHours: 24 // Check daily for renewals
});
// Register event handlers
this.certManager.on(CertManagerEvents.CERTIFICATE_ISSUED, this.handleCertificateIssued.bind(this));
this.certManager.on(CertManagerEvents.CERTIFICATE_RENEWED, this.handleCertificateIssued.bind(this));
this.certManager.on(CertManagerEvents.CERTIFICATE_FAILED, this.handleCertificateFailed.bind(this));
this.certManager.on(CertManagerEvents.CERTIFICATE_EXPIRING, (data) => {
this.log('info', `Certificate for ${data.domain} expires in ${data.daysRemaining} days`);
});
// Start the manager
try {
await this.certManager.start();
this.log('info', `ACME Certificate Manager started on port ${this.options.acme.port}`);
// Add domains from proxy configs
this.registerDomainsWithAcmeManager();
} catch (error) {
this.log('error', `Failed to start ACME Certificate Manager: ${error}`);
this.certManager = null;
}
}
/**
* Registers domains from proxy configs with the ACME manager
* @private
*/
private registerDomainsWithAcmeManager(): void {
if (!this.certManager) return;
// Get all hostnames from proxy configs
this.proxyConfigs.forEach(config => {
const hostname = config.hostName;
// Skip wildcard domains - can't get certs for these with HTTP-01 validation
if (hostname.includes('*')) {
this.log('info', `Skipping wildcard domain for ACME: ${hostname}`);
return;
}
// Skip domains already with certificates if configured to do so
if (this.options.acme.skipConfiguredCerts) {
const cachedCert = this.certificateCache.get(hostname);
if (cachedCert) {
this.log('info', `Skipping domain with existing certificate: ${hostname}`);
return;
}
}
// Check for existing certificate in the store
const certPath = path.join(this.certificateStoreDir, `${hostname}.cert.pem`);
const keyPath = path.join(this.certificateStoreDir, `${hostname}.key.pem`);
try {
if (fs.existsSync(certPath) && fs.existsSync(keyPath)) {
// Load existing certificate and key
const cert = fs.readFileSync(certPath, 'utf8');
const key = fs.readFileSync(keyPath, 'utf8');
// Extract expiry date from certificate if possible
let expiryDate: Date | undefined;
try {
const matches = cert.match(/Not After\s*:\s*(.*?)(?:\n|$)/i);
if (matches && matches[1]) {
expiryDate = new Date(matches[1]);
}
} catch (error) {
this.log('warn', `Failed to extract expiry date from certificate for ${hostname}`);
}
// Update the certificate in the manager
this.certManager.setCertificate(hostname, cert, key, expiryDate);
// Also update our own certificate cache
this.updateCertificateCache(hostname, cert, key, expiryDate);
this.log('info', `Loaded existing certificate for ${hostname}`);
} else {
// Register the domain for certificate issuance
this.certManager.addDomain(hostname);
this.log('info', `Registered domain for ACME certificate issuance: ${hostname}`);
}
} catch (error) {
this.log('error', `Error registering domain ${hostname} with ACME manager: ${error}`);
}
});
}
/**
* Handles newly issued or renewed certificates from ACME manager
* @private
*/
private handleCertificateIssued(data: { domain: string; certificate: string; privateKey: string; expiryDate: Date }): void {
const { domain, certificate, privateKey, expiryDate } = data;
this.log('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
this.saveCertificateToStore(domain, certificate, privateKey);
}
/**
* Handles certificate issuance failures
* @private
*/
private handleCertificateFailed(data: { domain: string; error: string }): void {
this.log('error', `Certificate issuance failed for ${data.domain}: ${data.error}`);
}
/**
* Saves certificate and private key to the filesystem
* @private
*/
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.log('warn', `Failed to set permissions on private key for ${domain}: ${error}`);
}
this.log('info', `Saved certificate for ${domain} to ${certPath}`);
} catch (error) {
this.log('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
* @private
*/
private handleSNI(domain: string, cb: (err: Error | null, ctx: plugins.tls.SecureContext) => void): void {
this.log('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.log('debug', `Using cached certificate for ${domain}`);
cb(null, context);
return;
} catch (err) {
this.log('error', `Error creating secure context for ${domain}:`, err);
}
}
// Check if we should trigger certificate issuance
if (this.options.acme?.enabled && this.certManager && !domain.includes('*')) {
// Check if this domain is already registered
const certData = this.certManager.getCertificate(domain);
if (!certData) {
this.log('info', `No certificate found for ${domain}, registering for issuance`);
this.certManager.addDomain(domain);
}
}
// Fall back to default certificate
try {
const context = plugins.tls.createSecureContext({
key: this.defaultCertificates.key,
cert: this.defaultCertificates.cert
});
this.log('debug', `Using default certificate for ${domain}`);
cb(null, context);
} catch (err) {
this.log('error', `Error creating default secure context:`, err);
cb(new Error('Cannot create secure context'), null);
}
}
/**
* Starts the proxy server
*/
public async start(): Promise<void> {
this.startTime = Date.now();
// Initialize ACME certificate manager if enabled
if (this.options.acme.enabled) {
await this.initializeAcmeManager();
}
// Create the HTTPS server
this.httpsServer = plugins.https.createServer(
{
key: this.defaultCertificates.key,
cert: this.defaultCertificates.cert
cert: this.defaultCertificates.cert,
SNICallback: (domain, cb) => this.handleSNI(domain, cb)
},
(req, res) => this.handleRequest(req, res)
);
@ -556,7 +813,10 @@ export class NetworkProxy {
const outGoingDeferred = plugins.smartpromise.defer();
try {
const wsTarget = `ws://${wsDestinationConfig.destinationIp}:${wsDestinationConfig.destinationPort}${reqArg.url}`;
// Select destination IP and port for WebSocket
const wsDestinationIp = this.selectDestinationIp(wsDestinationConfig);
const wsDestinationPort = this.selectDestinationPort(wsDestinationConfig);
const wsTarget = `ws://${wsDestinationIp}:${wsDestinationPort}${reqArg.url}`;
this.log('debug', `Proxying WebSocket to ${wsTarget}`);
wsOutgoing = new plugins.wsDefault(wsTarget);
@ -688,8 +948,12 @@ export class NetworkProxy {
const useConnectionPool = this.options.portProxyIntegration &&
originRequest.socket.remoteAddress?.includes('127.0.0.1');
// Select destination IP and port from the arrays
const destinationIp = this.selectDestinationIp(destinationConfig);
const destinationPort = this.selectDestinationPort(destinationConfig);
// Construct destination URL
const destinationUrl = `http://${destinationConfig.destinationIp}:${destinationConfig.destinationPort}${originRequest.url}`;
const destinationUrl = `http://${destinationIp}:${destinationPort}${originRequest.url}`;
if (useConnectionPool) {
this.log('debug', `[${reqId}] Proxying to ${destinationUrl} (using connection pool)`);
@ -697,8 +961,8 @@ export class NetworkProxy {
reqId,
originRequest,
originResponse,
destinationConfig.destinationIp,
destinationConfig.destinationPort,
destinationIp,
destinationPort,
originRequest.url
);
} else {
@ -1084,6 +1348,80 @@ export class NetworkProxy {
}
}
/**
* Selects a destination IP from the array using round-robin
* @param config The proxy configuration
* @returns A destination IP address
*/
private selectDestinationIp(config: plugins.tsclass.network.IReverseProxyConfig): string {
// For array-based configs
if (Array.isArray(config.destinationIps) && config.destinationIps.length > 0) {
// Get the current position or initialize it
const key = `ip_${config.hostName}`;
let position = this.roundRobinPositions.get(key) || 0;
// Select the IP using round-robin
const selectedIp = config.destinationIps[position];
// Update the position for next time
position = (position + 1) % config.destinationIps.length;
this.roundRobinPositions.set(key, position);
return selectedIp;
}
// For backward compatibility with test suites that rely on specific behavior
// Check if there's a proxyConfigs entry that matches this hostname
const matchingConfig = this.proxyConfigs.find(cfg =>
cfg.hostName === config.hostName &&
(cfg as any).destinationIp
);
if (matchingConfig) {
return (matchingConfig as any).destinationIp;
}
// Fallback to localhost
return 'localhost';
}
/**
* Selects a destination port from the array using round-robin
* @param config The proxy configuration
* @returns A destination port number
*/
private selectDestinationPort(config: plugins.tsclass.network.IReverseProxyConfig): number {
// For array-based configs
if (Array.isArray(config.destinationPorts) && config.destinationPorts.length > 0) {
// Get the current position or initialize it
const key = `port_${config.hostName}`;
let position = this.roundRobinPositions.get(key) || 0;
// Select the port using round-robin
const selectedPort = config.destinationPorts[position];
// Update the position for next time
position = (position + 1) % config.destinationPorts.length;
this.roundRobinPositions.set(key, position);
return selectedPort;
}
// For backward compatibility with test suites that rely on specific behavior
// Check if there's a proxyConfigs entry that matches this hostname
const matchingConfig = this.proxyConfigs.find(cfg =>
cfg.hostName === config.hostName &&
(cfg as any).destinationPort
);
if (matchingConfig) {
return parseInt((matchingConfig as any).destinationPort, 10);
}
// Fallback to port 80
return 80;
}
/**
* Updates proxy configurations
*/
@ -1144,6 +1482,48 @@ export class NetworkProxy {
}
}
/**
* Converts PortProxy domain configurations to NetworkProxy configs
* @param domainConfigs PortProxy domain configs
* @param sslKeyPair Default SSL key pair to use if not specified
* @returns Array of NetworkProxy configs
*/
public convertPortProxyConfigs(
domainConfigs: Array<{
domains: string[];
targetIPs?: string[];
allowedIPs?: string[];
}>,
sslKeyPair?: { key: string; cert: string }
): plugins.tsclass.network.IReverseProxyConfig[] {
const proxyConfigs: plugins.tsclass.network.IReverseProxyConfig[] = [];
// Use default certificates if not provided
const sslKey = sslKeyPair?.key || this.defaultCertificates.key;
const sslCert = sslKeyPair?.cert || this.defaultCertificates.cert;
for (const domainConfig of domainConfigs) {
// Each domain in the domains array gets its own config
for (const domain of domainConfig.domains) {
// Skip non-hostname patterns (like IP addresses)
if (domain.match(/^\d+\.\d+\.\d+\.\d+$/) || domain === '*' || domain === 'localhost') {
continue;
}
proxyConfigs.push({
hostName: domain,
destinationIps: domainConfig.targetIPs || ['localhost'],
destinationPorts: [this.options.port], // Use the NetworkProxy port
privateKey: sslKey,
publicKey: sslCert
});
}
}
this.log('info', `Converted ${domainConfigs.length} PortProxy configs to ${proxyConfigs.length} NetworkProxy configs`);
return proxyConfigs;
}
/**
* Adds default headers to be included in all responses
*/
@ -1208,6 +1588,16 @@ export class NetworkProxy {
}
this.connectionPool.clear();
// Stop ACME certificate manager if it's running
if (this.certManager) {
try {
await this.certManager.stop();
this.log('info', 'ACME Certificate Manager stopped');
} catch (error) {
this.log('error', 'Error stopping ACME Certificate Manager', error);
}
}
// Close the HTTPS server
return new Promise((resolve) => {
this.httpsServer.close(() => {
@ -1217,6 +1607,71 @@ export class NetworkProxy {
});
}
/**
* Requests a new certificate for a domain
* This can be used to manually trigger certificate issuance
* @param domain The domain to request a certificate for
* @returns A promise that resolves when the request is submitted (not when the certificate is issued)
*/
public async requestCertificate(domain: string): Promise<boolean> {
if (!this.options.acme.enabled) {
this.log('warn', 'ACME certificate management is not enabled');
return false;
}
if (!this.certManager) {
this.log('error', 'ACME certificate manager is not initialized');
return false;
}
// Skip wildcard domains - can't get certs for these with HTTP-01 validation
if (domain.includes('*')) {
this.log('error', `Cannot request certificate for wildcard domain: ${domain}`);
return false;
}
try {
this.certManager.addDomain(domain);
this.log('info', `Certificate request submitted for domain: ${domain}`);
return true;
} catch (error) {
this.log('error', `Error requesting certificate for domain ${domain}:`, error);
return false;
}
}
/**
* Updates the certificate cache for a domain
* @param domain The domain name
* @param certificate The certificate (PEM format)
* @param privateKey The private key (PEM format)
* @param expiryDate Optional expiry date
*/
private 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.log('debug', `Updated SSL context for domain: ${domain}`);
} catch (error) {
this.log('error', `Error updating SSL context for domain ${domain}:`, error);
}
}
// Update certificate in cache
this.certificateCache.set(domain, {
key: privateKey,
cert: certificate,
expires: expiryDate
});
// Add to active contexts set
this.activeContexts.add(domain);
}
/**
* Logs a message according to the configured log level
*/

View File

@ -56,9 +56,21 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
keepAliveInactivityMultiplier?: number; // Multiplier for inactivity timeout for keep-alive connections
extendedKeepAliveLifetime?: number; // Extended lifetime for keep-alive connections (ms)
// New property for NetworkProxy integration
// NetworkProxy integration
useNetworkProxy?: number[]; // Array of ports to forward to NetworkProxy
networkProxyPort?: number; // Port where NetworkProxy is listening (default: 8443)
// ACME certificate management options
acme?: {
enabled?: boolean; // Whether to enable automatic certificate management
port?: number; // Port to listen on for ACME challenges (default: 80)
contactEmail?: string; // Email for Let's Encrypt account
useProduction?: boolean; // Whether to use Let's Encrypt production (default: false for staging)
renewThresholdDays?: number; // Days before expiry to renew certificates (default: 30)
autoRenew?: boolean; // Whether to automatically renew certificates (default: true)
certificateStore?: string; // Directory to store certificates (default: ./certs)
skipConfiguredCerts?: boolean; // Skip domains that already have certificates configured
};
}
/**
@ -418,6 +430,18 @@ export class PortProxy {
// NetworkProxy settings
networkProxyPort: settingsArg.networkProxyPort || 8443, // Default NetworkProxy port
// ACME certificate settings with reasonable defaults
acme: settingsArg.acme || {
enabled: false,
port: 80,
contactEmail: 'admin@example.com',
useProduction: false,
renewThresholdDays: 30,
autoRenew: true,
certificateStore: './certs',
skipConfiguredCerts: false
}
};
// Initialize NetworkProxy if enabled
@ -429,15 +453,182 @@ export class PortProxy {
/**
* Initialize NetworkProxy instance
*/
private initializeNetworkProxy(): void {
private async initializeNetworkProxy(): Promise<void> {
if (!this.networkProxy) {
this.networkProxy = new NetworkProxy({
// Configure NetworkProxy options based on PortProxy settings
const networkProxyOptions: any = {
port: this.settings.networkProxyPort!,
portProxyIntegration: true,
logLevel: this.settings.enableDetailedLogging ? 'debug' : 'info'
});
};
// Add ACME settings if configured
if (this.settings.acme) {
networkProxyOptions.acme = { ...this.settings.acme };
}
this.networkProxy = new NetworkProxy(networkProxyOptions);
console.log(`Initialized NetworkProxy on port ${this.settings.networkProxyPort}`);
// Convert and apply domain configurations to NetworkProxy
await this.syncDomainConfigsToNetworkProxy();
}
}
/**
* Updates the domain configurations for the proxy
* @param newDomainConfigs The new domain configurations
*/
public async updateDomainConfigs(newDomainConfigs: IDomainConfig[]): Promise<void> {
console.log(`Updating domain configurations (${newDomainConfigs.length} configs)`);
this.settings.domainConfigs = newDomainConfigs;
// If NetworkProxy is initialized, resync the configurations
if (this.networkProxy) {
await this.syncDomainConfigsToNetworkProxy();
}
}
/**
* Updates the ACME certificate settings
* @param acmeSettings New ACME settings
*/
public async updateAcmeSettings(acmeSettings: IPortProxySettings['acme']): Promise<void> {
console.log('Updating ACME certificate settings');
// Update settings
this.settings.acme = {
...this.settings.acme,
...acmeSettings
};
// If NetworkProxy is initialized, update its ACME settings
if (this.networkProxy) {
try {
// Recreate NetworkProxy with new settings if ACME enabled state has changed
if (this.settings.acme.enabled !== acmeSettings.enabled) {
console.log(`ACME enabled state changed to: ${acmeSettings.enabled}`);
// Stop the current NetworkProxy
await this.networkProxy.stop();
this.networkProxy = null;
// Reinitialize with new settings
await this.initializeNetworkProxy();
// Use start() to make sure ACME gets initialized if newly enabled
await this.networkProxy.start();
} else {
// Update existing NetworkProxy with new settings
// Note: Some settings may require a restart to take effect
console.log('Updating ACME settings in NetworkProxy');
// For certificate renewals, we might want to trigger checks with the new settings
if (acmeSettings.renewThresholdDays) {
console.log(`Setting new renewal threshold to ${acmeSettings.renewThresholdDays} days`);
// This is implementation-dependent but gives an example
if (this.networkProxy.options.acme) {
this.networkProxy.options.acme.renewThresholdDays = acmeSettings.renewThresholdDays;
}
}
}
} catch (err) {
console.log(`Error updating ACME settings: ${err}`);
}
}
}
/**
* Synchronizes PortProxy domain configurations to NetworkProxy
* This allows domains configured in PortProxy to be used by NetworkProxy
*/
private async syncDomainConfigsToNetworkProxy(): Promise<void> {
if (!this.networkProxy) {
console.log('Cannot sync configurations - NetworkProxy not initialized');
return;
}
try {
// Get SSL certificates from assets
// Import fs directly since it's not in plugins
const fs = await import('fs');
let certPair;
try {
certPair = {
key: fs.readFileSync('assets/certs/key.pem', 'utf8'),
cert: fs.readFileSync('assets/certs/cert.pem', 'utf8')
};
} catch (certError) {
console.log(`Warning: Could not read default certificates: ${certError}`);
console.log('Using empty certificate placeholders - ACME will generate proper certificates if enabled');
// Use empty placeholders - NetworkProxy will use its internal defaults
// or ACME will generate proper ones if enabled
certPair = {
key: '',
cert: ''
};
}
// Convert domain configs to NetworkProxy configs
const proxyConfigs = this.networkProxy.convertPortProxyConfigs(
this.settings.domainConfigs,
certPair
);
// Log ACME-eligible domains if ACME is enabled
if (this.settings.acme?.enabled) {
const acmeEligibleDomains = proxyConfigs
.filter(config => !config.hostName.includes('*')) // Exclude wildcards
.map(config => config.hostName);
if (acmeEligibleDomains.length > 0) {
console.log(`Domains eligible for ACME certificates: ${acmeEligibleDomains.join(', ')}`);
} else {
console.log('No domains eligible for ACME certificates found in configuration');
}
}
// Update NetworkProxy with the converted configs
this.networkProxy.updateProxyConfigs(proxyConfigs).then(() => {
console.log(`Successfully synchronized ${proxyConfigs.length} domain configurations to NetworkProxy`);
}).catch(err => {
console.log(`Error synchronizing configurations: ${err.message}`);
});
} catch (err) {
console.log(`Failed to sync configurations: ${err}`);
}
}
/**
* Requests a certificate for a specific domain
* @param domain The domain to request a certificate for
* @returns Promise that resolves to true if the request was successful, false otherwise
*/
public async requestCertificate(domain: string): Promise<boolean> {
if (!this.networkProxy) {
console.log('Cannot request certificate - NetworkProxy not initialized');
return false;
}
if (!this.settings.acme?.enabled) {
console.log('Cannot request certificate - ACME is not enabled');
return false;
}
try {
const result = await this.networkProxy.requestCertificate(domain);
if (result) {
console.log(`Certificate request for ${domain} submitted successfully`);
} else {
console.log(`Certificate request for ${domain} failed`);
}
return result;
} catch (err) {
console.log(`Error requesting certificate: ${err}`);
return false;
}
}
@ -1278,10 +1469,27 @@ export class PortProxy {
return;
}
// Initialize NetworkProxy if needed (useNetworkProxy is set but networkProxy isn't initialized)
if (this.settings.useNetworkProxy && this.settings.useNetworkProxy.length > 0 && !this.networkProxy) {
await this.initializeNetworkProxy();
}
// Start NetworkProxy if configured
if (this.networkProxy) {
await this.networkProxy.start();
console.log(`NetworkProxy started on port ${this.settings.networkProxyPort}`);
// Log ACME status
if (this.settings.acme?.enabled) {
console.log(`ACME certificate management is enabled (${this.settings.acme.useProduction ? 'Production' : 'Staging'} mode)`);
console.log(`ACME HTTP challenge server on port ${this.settings.acme.port}`);
// Register domains for ACME certificates if enabled
if (this.networkProxy.options.acme?.enabled) {
console.log('Registering domains with ACME certificate manager...');
// The NetworkProxy will handle this internally via registerDomainsWithAcmeManager()
}
}
}
// Define a unified connection handler for all listening ports.
@ -2036,11 +2244,17 @@ export class PortProxy {
}
}
// Stop NetworkProxy if it was started
// Stop NetworkProxy if it was started (which also stops ACME manager)
if (this.networkProxy) {
try {
console.log('Stopping NetworkProxy...');
await this.networkProxy.stop();
console.log('NetworkProxy stopped successfully');
// Log ACME shutdown if it was enabled
if (this.settings.acme?.enabled) {
console.log('ACME certificate manager stopped');
}
} catch (err) {
console.log(`Error stopping NetworkProxy: ${err}`);
}