Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
12b18373db | |||
30c25ec70c | |||
434834fc06 | |||
e7243243d0 | |||
cce2aed892 | |||
8cd693c063 | |||
09ad7644f4 | |||
f72f884eda | |||
73f3dfcad4 | |||
8291f1f33a | |||
f512fb4252 | |||
1f3ee1eafc | |||
910c8160f6 | |||
0e634c46a6 | |||
32b4e32bf0 | |||
878e76ab23 |
57
changelog.md
57
changelog.md
@ -1,5 +1,62 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
- Removed redundant IReverseProxyConfig extension from ts/common/types.ts
|
||||||
|
- Updated ts/index.ts to export networkproxy via index.js instead of classes.np.networkproxy.js
|
||||||
|
- Simplified module exports to avoid duplicate interface definitions
|
||||||
|
|
||||||
|
## 2025-05-04 - 10.0.4 - fix(core)
|
||||||
|
Refactor module exports and update packageManager version in package.json
|
||||||
|
|
||||||
|
- In package.json, bumped pnpm version from 10.7.0 to 10.10.0 for dependency consistency.
|
||||||
|
- In ts/index.ts, removed redundant type export and now export common types directly.
|
||||||
|
- In ts/smartproxy/classes.smartproxy.ts, reorganized imports and explicitly export IPortProxySettings and IDomainConfig.
|
||||||
|
|
||||||
|
## 2025-05-04 - 10.0.3 - fix(smartproxy)
|
||||||
|
Update dependency versions (@push.rocks/smartacme to ^7.2.4, @push.rocks/smartnetwork to ^4.0.1, ws to ^8.18.2) and export common types via index.ts for easier imports.
|
||||||
|
|
||||||
|
- Upgrade @push.rocks/smartacme from ^7.2.3 to ^7.2.4
|
||||||
|
- Upgrade @push.rocks/smartnetwork from ^4.0.0 to ^4.0.1
|
||||||
|
- Upgrade ws from ^8.18.1 to ^8.18.2
|
||||||
|
- Export common types from ts/common/types.ts in index.ts
|
||||||
|
|
||||||
|
## 2025-05-03 - 10.0.2 - fix(tlsalert)
|
||||||
|
Centralize plugin imports in TlsAlert and update plan checklist
|
||||||
|
|
||||||
|
- Mark the 'Centralize plugin imports in ts/plugins.ts' item as complete in readme.plan.md
|
||||||
|
- Replace direct 'net' imports with a centralized 'plugins' import in ts/smartproxy/classes.pp.tlsalert.ts
|
||||||
|
- Update all socket type references from net.Socket to plugins.net.Socket for consistency
|
||||||
|
|
||||||
|
## 2025-05-03 - 10.0.1 - fix(docs)
|
||||||
|
Improve mermaid diagram formatting in readme.md using HTML <br> tags for line breaks
|
||||||
|
|
||||||
|
- Replaced newline characters with <br> in the SmartProxy Components diagram nodes for better HTML rendering
|
||||||
|
- Improved visual clarity of the architectural diagrams in the readme
|
||||||
|
|
||||||
|
## 2025-05-03 - 10.0.0 - BREAKING CHANGE(smartproxy)
|
||||||
|
Update documentation and refactor core proxy components; remove legacy performRenewals method from SmartProxy; update router type imports and adjust test suites for improved coverage
|
||||||
|
|
||||||
|
- Expanded README with detailed Quick Start examples for HTTP/HTTPS reverse proxy, ACME integration, HTTP→HTTPS redirect, nftables port forwarding, and SNI-based TCP proxying
|
||||||
|
- Updated readme.plan.md checkboxes to show completed tasks
|
||||||
|
- Refactored ProxyRouter to import types via plugins.tsclass, ensuring consistency in type imports
|
||||||
|
- Removed deprecated performRenewals method from SmartProxy, constituting a breaking change for users relying on it
|
||||||
|
- Updated multiple test suites (router, networkproxy, certprovisioner, etc.) to reflect new behaviors and improved diagnostics
|
||||||
|
|
||||||
|
## 2025-05-02 - 9.0.0 - BREAKING CHANGE(acme)
|
||||||
|
Refactor ACME configuration and certificate provisioning by replacing legacy port80HandlerConfig with unified acme options and updating CertProvisioner event subscriptions
|
||||||
|
|
||||||
|
- Remove deprecated port80HandlerConfig references and merge configuration into a single acme options schema
|
||||||
|
- Use buildPort80Handler factory for consistent Port80Handler instantiation
|
||||||
|
- Integrate subscribeToPort80Handler utility in CertProvisioner and NetworkProxyBridge for event management
|
||||||
|
- Update types in common modules and IPortProxySettings to reflect unified acme configurations
|
||||||
|
- Adjust documentation (readme.plan.md and code-level comments) to reflect the new refactored flow
|
||||||
|
|
||||||
## 2025-05-02 - 8.0.0 - BREAKING CHANGE(certProvisioner)
|
## 2025-05-02 - 8.0.0 - BREAKING CHANGE(certProvisioner)
|
||||||
Refactor: Introduce unified CertProvisioner to centralize certificate provisioning and renewal; remove legacy ACME config from Port80Handler and update SmartProxy to delegate certificate lifecycle management.
|
Refactor: Introduce unified CertProvisioner to centralize certificate provisioning and renewal; remove legacy ACME config from Port80Handler and update SmartProxy to delegate certificate lifecycle management.
|
||||||
|
|
||||||
|
10
package.json
10
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartproxy",
|
"name": "@push.rocks/smartproxy",
|
||||||
"version": "8.0.0",
|
"version": "10.0.6",
|
||||||
"private": false,
|
"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.",
|
"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",
|
"main": "dist_ts/index.js",
|
||||||
@ -24,9 +24,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@push.rocks/lik": "^6.2.2",
|
"@push.rocks/lik": "^6.2.2",
|
||||||
"@push.rocks/smartacme": "^7.2.3",
|
"@push.rocks/smartacme": "^7.2.4",
|
||||||
"@push.rocks/smartdelay": "^3.0.5",
|
"@push.rocks/smartdelay": "^3.0.5",
|
||||||
"@push.rocks/smartnetwork": "^4.0.0",
|
"@push.rocks/smartnetwork": "^4.0.1",
|
||||||
"@push.rocks/smartpromise": "^4.2.3",
|
"@push.rocks/smartpromise": "^4.2.3",
|
||||||
"@push.rocks/smartrequest": "^2.1.0",
|
"@push.rocks/smartrequest": "^2.1.0",
|
||||||
"@push.rocks/smartstring": "^4.0.15",
|
"@push.rocks/smartstring": "^4.0.15",
|
||||||
@ -36,7 +36,7 @@
|
|||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"minimatch": "^10.0.1",
|
"minimatch": "^10.0.1",
|
||||||
"pretty-ms": "^9.2.0",
|
"pretty-ms": "^9.2.0",
|
||||||
"ws": "^8.18.1"
|
"ws": "^8.18.2"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"ts/**/*",
|
"ts/**/*",
|
||||||
@ -86,5 +86,5 @@
|
|||||||
"puppeteer"
|
"puppeteer"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6"
|
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
||||||
}
|
}
|
||||||
|
43
pnpm-lock.yaml
generated
43
pnpm-lock.yaml
generated
@ -12,14 +12,14 @@ importers:
|
|||||||
specifier: ^6.2.2
|
specifier: ^6.2.2
|
||||||
version: 6.2.2
|
version: 6.2.2
|
||||||
'@push.rocks/smartacme':
|
'@push.rocks/smartacme':
|
||||||
specifier: ^7.2.3
|
specifier: ^7.2.4
|
||||||
version: 7.2.3(@aws-sdk/credential-providers@3.798.0)(socks@2.8.4)
|
version: 7.2.4(@aws-sdk/credential-providers@3.798.0)(socks@2.8.4)
|
||||||
'@push.rocks/smartdelay':
|
'@push.rocks/smartdelay':
|
||||||
specifier: ^3.0.5
|
specifier: ^3.0.5
|
||||||
version: 3.0.5
|
version: 3.0.5
|
||||||
'@push.rocks/smartnetwork':
|
'@push.rocks/smartnetwork':
|
||||||
specifier: ^4.0.0
|
specifier: ^4.0.1
|
||||||
version: 4.0.0
|
version: 4.0.1
|
||||||
'@push.rocks/smartpromise':
|
'@push.rocks/smartpromise':
|
||||||
specifier: ^4.2.3
|
specifier: ^4.2.3
|
||||||
version: 4.2.3
|
version: 4.2.3
|
||||||
@ -48,8 +48,8 @@ importers:
|
|||||||
specifier: ^9.2.0
|
specifier: ^9.2.0
|
||||||
version: 9.2.0
|
version: 9.2.0
|
||||||
ws:
|
ws:
|
||||||
specifier: ^8.18.1
|
specifier: ^8.18.2
|
||||||
version: 8.18.1
|
version: 8.18.2
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@git.zone/tsbuild':
|
'@git.zone/tsbuild':
|
||||||
specifier: ^2.3.2
|
specifier: ^2.3.2
|
||||||
@ -355,8 +355,8 @@ packages:
|
|||||||
'@cloudflare/workers-types@4.20250303.0':
|
'@cloudflare/workers-types@4.20250303.0':
|
||||||
resolution: {integrity: sha512-O7F7nRT4bbmwHf3gkRBLfJ7R6vHIJ/oZzWdby6obOiw2yavUfp/AIwS7aO2POu5Cv8+h3TXS3oHs3kKCZLraUA==}
|
resolution: {integrity: sha512-O7F7nRT4bbmwHf3gkRBLfJ7R6vHIJ/oZzWdby6obOiw2yavUfp/AIwS7aO2POu5Cv8+h3TXS3oHs3kKCZLraUA==}
|
||||||
|
|
||||||
'@cloudflare/workers-types@4.20250430.0':
|
'@cloudflare/workers-types@4.20250504.0':
|
||||||
resolution: {integrity: sha512-JWAX7ZhQ7KjkdJwASgG58MZ/pQ15brlnZ9/0YBwDQ0hrJ/LaK392aTRFlj2r/PRKDZ5dOuujRywNYaNpfeFiEA==}
|
resolution: {integrity: sha512-/70Kb5vrqj+O0krOuS8LVLiCeDuCGzQy4X+wGGs4/rHv0gZJulv7Uj5YlUjIaRemK/Dyrzlk7WNJwTy8yv0cIw==}
|
||||||
|
|
||||||
'@colors/colors@1.6.0':
|
'@colors/colors@1.6.0':
|
||||||
resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==}
|
resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==}
|
||||||
@ -872,8 +872,8 @@ packages:
|
|||||||
'@push.rocks/qenv@6.1.0':
|
'@push.rocks/qenv@6.1.0':
|
||||||
resolution: {integrity: sha512-1FUFMlSVwFSFg8LbqfkzJ2LLP4lMGApUtgOpsvrde6+AxBmB4gjoNgCUH7z3xXfDAtYqcrtSELXBNE0xVL1MqQ==}
|
resolution: {integrity: sha512-1FUFMlSVwFSFg8LbqfkzJ2LLP4lMGApUtgOpsvrde6+AxBmB4gjoNgCUH7z3xXfDAtYqcrtSELXBNE0xVL1MqQ==}
|
||||||
|
|
||||||
'@push.rocks/smartacme@7.2.3':
|
'@push.rocks/smartacme@7.2.4':
|
||||||
resolution: {integrity: sha512-PTwn/Zf7l+IMWqeiQ8mTxi7fdrtObQH13YzF65si/VxXTqHeZ7zvisLLKZcMEgSaOj1aQ/Ku83gaO8YqO4gDig==}
|
resolution: {integrity: sha512-0ciewRheDAwv0ER0ZyLQVLAn0ZoG1++ibSZ14HoXn8GOTOLyuRLWLNDQL2fI4LtLxeaNYmQUS+f7tt4KaZb/UA==}
|
||||||
|
|
||||||
'@push.rocks/smartarchive@3.0.8':
|
'@push.rocks/smartarchive@3.0.8':
|
||||||
resolution: {integrity: sha512-1jPmR0b7hXmjYQoRiTlRXrIbZcdcFmSdGOfznufjcDpGPe86Km0d8TBnzqghTx4dTihzKC67IxAaz/DM3lvxpA==}
|
resolution: {integrity: sha512-1jPmR0b7hXmjYQoRiTlRXrIbZcdcFmSdGOfznufjcDpGPe86Km0d8TBnzqghTx4dTihzKC67IxAaz/DM3lvxpA==}
|
||||||
@ -974,8 +974,8 @@ packages:
|
|||||||
'@push.rocks/smartnetwork@3.0.2':
|
'@push.rocks/smartnetwork@3.0.2':
|
||||||
resolution: {integrity: sha512-s6CNGzQ1n/d/6cOKXbxeW6/tO//dr1woLqI01g7XhqTriw0nsm2G2kWaZh2J0VOguGNWBgQVCIpR0LjdRNWb3g==}
|
resolution: {integrity: sha512-s6CNGzQ1n/d/6cOKXbxeW6/tO//dr1woLqI01g7XhqTriw0nsm2G2kWaZh2J0VOguGNWBgQVCIpR0LjdRNWb3g==}
|
||||||
|
|
||||||
'@push.rocks/smartnetwork@4.0.0':
|
'@push.rocks/smartnetwork@4.0.1':
|
||||||
resolution: {integrity: sha512-hLE1JNrBjlWtibgFz7t2aMfP15VOfPFyKMpo6FI0JdhmJfD3V5w/nFpSdD6WdXeXUBjCVTJ3C6SrRl8izoG55g==}
|
resolution: {integrity: sha512-zLH88bKY6/cK6vVnCW4Fsugu4T+l6OerWWappit+BecdnQ6vrgShXSAa13JIkkWkWcs4dxEirlEfycQEEQw8BQ==}
|
||||||
|
|
||||||
'@push.rocks/smartnpm@2.0.4':
|
'@push.rocks/smartnpm@2.0.4':
|
||||||
resolution: {integrity: sha512-ljRPqnUsXzL5qnuAEt5POy0NnfKs7eYPuuJPJjYiK9VUdP/CyF4h14qTB4H816vNEuF7VU/ASRtz0qDlXmrztg==}
|
resolution: {integrity: sha512-ljRPqnUsXzL5qnuAEt5POy0NnfKs7eYPuuJPJjYiK9VUdP/CyF4h14qTB4H816vNEuF7VU/ASRtz0qDlXmrztg==}
|
||||||
@ -4622,8 +4622,8 @@ packages:
|
|||||||
utf-8-validate:
|
utf-8-validate:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
ws@8.18.1:
|
ws@8.18.2:
|
||||||
resolution: {integrity: sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==}
|
resolution: {integrity: sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
bufferutil: ^4.0.1
|
bufferutil: ^4.0.1
|
||||||
@ -4760,7 +4760,7 @@ snapshots:
|
|||||||
'@api.global/typedrequest': 3.1.10
|
'@api.global/typedrequest': 3.1.10
|
||||||
'@api.global/typedrequest-interfaces': 3.0.19
|
'@api.global/typedrequest-interfaces': 3.0.19
|
||||||
'@api.global/typedsocket': 3.0.1
|
'@api.global/typedsocket': 3.0.1
|
||||||
'@cloudflare/workers-types': 4.20250430.0
|
'@cloudflare/workers-types': 4.20250504.0
|
||||||
'@design.estate/dees-comms': 1.0.27
|
'@design.estate/dees-comms': 1.0.27
|
||||||
'@push.rocks/lik': 6.2.2
|
'@push.rocks/lik': 6.2.2
|
||||||
'@push.rocks/smartchok': 1.0.34
|
'@push.rocks/smartchok': 1.0.34
|
||||||
@ -5671,7 +5671,7 @@ snapshots:
|
|||||||
|
|
||||||
'@cloudflare/workers-types@4.20250303.0': {}
|
'@cloudflare/workers-types@4.20250303.0': {}
|
||||||
|
|
||||||
'@cloudflare/workers-types@4.20250430.0': {}
|
'@cloudflare/workers-types@4.20250504.0': {}
|
||||||
|
|
||||||
'@colors/colors@1.6.0': {}
|
'@colors/colors@1.6.0': {}
|
||||||
|
|
||||||
@ -5950,7 +5950,7 @@ snapshots:
|
|||||||
'@push.rocks/tapbundle': 5.6.3(@aws-sdk/credential-providers@3.798.0)(socks@2.8.4)
|
'@push.rocks/tapbundle': 5.6.3(@aws-sdk/credential-providers@3.798.0)(socks@2.8.4)
|
||||||
'@types/ws': 8.18.1
|
'@types/ws': 8.18.1
|
||||||
figures: 6.1.0
|
figures: 6.1.0
|
||||||
ws: 8.18.1
|
ws: 8.18.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@aws-sdk/credential-providers'
|
- '@aws-sdk/credential-providers'
|
||||||
- '@mongodb-js/zstd'
|
- '@mongodb-js/zstd'
|
||||||
@ -6284,7 +6284,7 @@ snapshots:
|
|||||||
'@push.rocks/smartlog': 3.0.7
|
'@push.rocks/smartlog': 3.0.7
|
||||||
'@push.rocks/smartpath': 5.0.18
|
'@push.rocks/smartpath': 5.0.18
|
||||||
|
|
||||||
'@push.rocks/smartacme@7.2.3(@aws-sdk/credential-providers@3.798.0)(socks@2.8.4)':
|
'@push.rocks/smartacme@7.2.4(@aws-sdk/credential-providers@3.798.0)(socks@2.8.4)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@api.global/typedserver': 3.0.74
|
'@api.global/typedserver': 3.0.74
|
||||||
'@apiclient.xyz/cloudflare': 6.4.1
|
'@apiclient.xyz/cloudflare': 6.4.1
|
||||||
@ -6304,6 +6304,7 @@ snapshots:
|
|||||||
- '@aws-sdk/credential-providers'
|
- '@aws-sdk/credential-providers'
|
||||||
- '@mongodb-js/zstd'
|
- '@mongodb-js/zstd'
|
||||||
- '@nuxt/kit'
|
- '@nuxt/kit'
|
||||||
|
- aws-crt
|
||||||
- bufferutil
|
- bufferutil
|
||||||
- encoding
|
- encoding
|
||||||
- gcp-metadata
|
- gcp-metadata
|
||||||
@ -6605,7 +6606,7 @@ snapshots:
|
|||||||
public-ip: 6.0.2
|
public-ip: 6.0.2
|
||||||
systeminformation: 5.25.11
|
systeminformation: 5.25.11
|
||||||
|
|
||||||
'@push.rocks/smartnetwork@4.0.0':
|
'@push.rocks/smartnetwork@4.0.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartping': 1.0.8
|
'@push.rocks/smartping': 1.0.8
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
@ -10544,7 +10545,7 @@ snapshots:
|
|||||||
debug: 4.4.0
|
debug: 4.4.0
|
||||||
devtools-protocol: 0.0.1413902
|
devtools-protocol: 0.0.1413902
|
||||||
typed-query-selector: 2.12.0
|
typed-query-selector: 2.12.0
|
||||||
ws: 8.18.1
|
ws: 8.18.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- bare-buffer
|
- bare-buffer
|
||||||
- bufferutil
|
- bufferutil
|
||||||
@ -11303,7 +11304,7 @@ snapshots:
|
|||||||
|
|
||||||
ws@8.17.1: {}
|
ws@8.17.1: {}
|
||||||
|
|
||||||
ws@8.18.1: {}
|
ws@8.18.2: {}
|
||||||
|
|
||||||
xml-js@1.6.11:
|
xml-js@1.6.11:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
709
readme.md
709
readme.md
@ -1,11 +1,178 @@
|
|||||||
# @push.rocks/smartproxy
|
# @push.rocks/smartproxy
|
||||||
|
|
||||||
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.
|
A high-performance proxy toolkit for Node.js, offering:
|
||||||
|
- HTTP/HTTPS reverse proxy with TLS termination and WebSocket support
|
||||||
|
- Automatic ACME certificate management (HTTP-01)
|
||||||
|
- Low-level port forwarding via nftables
|
||||||
|
- HTTP-to-HTTPS and custom URL redirects
|
||||||
|
- Advanced TCP/SNI-based proxying with IP filtering and rules
|
||||||
|
|
||||||
|
## Exports
|
||||||
|
The following classes and interfaces are provided:
|
||||||
|
|
||||||
|
- **NetworkProxy** (ts/networkproxy/classes.np.networkproxy.ts)
|
||||||
|
HTTP/HTTPS reverse proxy with TLS termination, WebSocket support,
|
||||||
|
connection pooling, and optional ACME integration.
|
||||||
|
- **Port80Handler** (ts/port80handler/classes.port80handler.ts)
|
||||||
|
ACME HTTP-01 challenge handler and certificate manager.
|
||||||
|
- **NfTablesProxy** (ts/nfttablesproxy/classes.nftablesproxy.ts)
|
||||||
|
Low-level port forwarding using nftables NAT rules.
|
||||||
|
- **Redirect**, **SslRedirect** (ts/redirect/classes.redirect.ts)
|
||||||
|
HTTP/HTTPS redirect server and shortcut for HTTP→HTTPS.
|
||||||
|
- **SmartProxy** (ts/smartproxy/classes.smartproxy.ts)
|
||||||
|
TCP/SNI-based proxy with dynamic routing, IP filtering, and unified certificates.
|
||||||
|
- **SniHandler** (ts/smartproxy/classes.pp.snihandler.ts)
|
||||||
|
Static utilities to extract SNI hostnames from TLS handshakes.
|
||||||
|
- **Interfaces**
|
||||||
|
- IPortProxySettings, IDomainConfig (ts/smartproxy/classes.pp.interfaces.ts)
|
||||||
|
- INetworkProxyOptions (ts/networkproxy/classes.np.types.ts)
|
||||||
|
- IAcmeOptions, IDomainOptions, IForwardConfig (ts/common/types.ts)
|
||||||
|
- INfTableProxySettings (ts/nfttablesproxy/classes.nftablesproxy.ts)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
Install via npm:
|
||||||
|
```bash
|
||||||
|
npm install @push.rocks/smartproxy
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. HTTP(S) Reverse Proxy (NetworkProxy)
|
||||||
|
```typescript
|
||||||
|
import { NetworkProxy } from '@push.rocks/smartproxy';
|
||||||
|
|
||||||
|
const proxy = new NetworkProxy({ port: 443 });
|
||||||
|
await proxy.start();
|
||||||
|
|
||||||
|
await proxy.updateProxyConfigs([
|
||||||
|
{
|
||||||
|
hostName: 'example.com',
|
||||||
|
destinationIps: ['127.0.0.1'],
|
||||||
|
destinationPorts: [3000],
|
||||||
|
publicKey: fs.readFileSync('cert.pem', 'utf8'),
|
||||||
|
privateKey: fs.readFileSync('key.pem', 'utf8'),
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Add default headers to all responses
|
||||||
|
await proxy.addDefaultHeaders({
|
||||||
|
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains'
|
||||||
|
});
|
||||||
|
// ...
|
||||||
|
await proxy.stop();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. HTTP→HTTPS Redirect (Redirect / SslRedirect)
|
||||||
|
```typescript
|
||||||
|
import { Redirect, SslRedirect } from '@push.rocks/smartproxy';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
// Custom redirect rules
|
||||||
|
const redirect = new Redirect({
|
||||||
|
httpPort: 80,
|
||||||
|
httpsPort: 443,
|
||||||
|
sslOptions: {
|
||||||
|
key: fs.readFileSync('key.pem'),
|
||||||
|
cert: fs.readFileSync('cert.pem'),
|
||||||
|
},
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
fromProtocol: 'http',
|
||||||
|
fromHost: '*',
|
||||||
|
toProtocol: 'https',
|
||||||
|
toHost: '$1',
|
||||||
|
statusCode: 301
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
await redirect.start();
|
||||||
|
|
||||||
|
// Quick HTTP→HTTPS helper on port 80
|
||||||
|
const quick = new SslRedirect(80);
|
||||||
|
await quick.start();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Automatic Certificates (ACME Port80Handler)
|
||||||
|
```typescript
|
||||||
|
import { Port80Handler } from '@push.rocks/smartproxy';
|
||||||
|
|
||||||
|
// Configure ACME on port 80 with contact email
|
||||||
|
const acme = new Port80Handler({
|
||||||
|
port: 80,
|
||||||
|
contactEmail: 'admin@example.com',
|
||||||
|
useProduction: true,
|
||||||
|
renewThresholdDays: 30
|
||||||
|
});
|
||||||
|
acme.on('certificate-issued', evt => {
|
||||||
|
console.log(`Certificate ready for ${evt.domain}, expires ${evt.expiryDate}`);
|
||||||
|
});
|
||||||
|
await acme.start();
|
||||||
|
acme.addDomain({
|
||||||
|
domainName: 'example.com',
|
||||||
|
sslRedirect: true,
|
||||||
|
acmeMaintenance: true
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Low-Level Port Forwarding (NfTablesProxy)
|
||||||
|
```typescript
|
||||||
|
import { NfTablesProxy } from '@push.rocks/smartproxy';
|
||||||
|
|
||||||
|
// Forward port 80→8080 with source IP preservation
|
||||||
|
const nft = new NfTablesProxy({
|
||||||
|
fromPort: 80,
|
||||||
|
toPort: 8080,
|
||||||
|
toHost: 'localhost',
|
||||||
|
preserveSourceIP: true,
|
||||||
|
deleteOnExit: true
|
||||||
|
});
|
||||||
|
await nft.start();
|
||||||
|
// ...
|
||||||
|
await nft.stop();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. TCP/SNI Proxy (SmartProxy)
|
||||||
|
```typescript
|
||||||
|
import { SmartProxy } from '@push.rocks/smartproxy';
|
||||||
|
|
||||||
|
const smart = new SmartProxy({
|
||||||
|
fromPort: 443,
|
||||||
|
toPort: 8443,
|
||||||
|
domainConfigs: [
|
||||||
|
{
|
||||||
|
domains: ['example.com', '*.example.com'],
|
||||||
|
allowedIPs: ['*'],
|
||||||
|
targetIPs: ['127.0.0.1'],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
sniEnabled: true
|
||||||
|
});
|
||||||
|
smart.on('certificate', evt => console.log(evt));
|
||||||
|
await smart.start();
|
||||||
|
// Update domains later
|
||||||
|
await smart.updateDomainConfigs([/* new configs */]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. SNI Utilities (SniHandler)
|
||||||
|
```js
|
||||||
|
import { SniHandler } from '@push.rocks/smartproxy';
|
||||||
|
|
||||||
|
// Extract SNI from a TLS ClientHello buffer
|
||||||
|
const sni = SniHandler.extractSNI(buffer);
|
||||||
|
|
||||||
|
// Reassemble fragmented ClientHello
|
||||||
|
const complete = SniHandler.handleFragmentedClientHello(buf, connId);
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
For full configuration options and type definitions, see the TypeScript interfaces in the `ts/` directory:
|
||||||
|
- `INetworkProxyOptions` (ts/networkproxy/classes.np.types.ts)
|
||||||
|
- `IAcmeOptions`, `IDomainOptions`, `IForwardConfig` (ts/common/types.ts)
|
||||||
|
- `INfTableProxySettings` (ts/nfttablesproxy/classes.nftablesproxy.ts)
|
||||||
|
- `IPortProxySettings`, `IDomainConfig` (ts/smartproxy/classes.pp.interfaces.ts)
|
||||||
|
|
||||||
## Architecture & Flow Diagrams
|
## Architecture & Flow Diagrams
|
||||||
|
|
||||||
### Component Architecture
|
|
||||||
The diagram below illustrates the main components of SmartProxy and how they interact:
|
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart TB
|
flowchart TB
|
||||||
@ -13,12 +180,12 @@ flowchart TB
|
|||||||
|
|
||||||
subgraph "SmartProxy Components"
|
subgraph "SmartProxy Components"
|
||||||
direction TB
|
direction TB
|
||||||
HTTP80[HTTP Port 80\nSslRedirect]
|
HTTP80["HTTP Port 80<br>Redirect / SslRedirect"]
|
||||||
HTTPS443[HTTPS Port 443\nNetworkProxy]
|
HTTPS443["HTTPS Port 443<br>NetworkProxy"]
|
||||||
SmartProxy[SmartProxy\nwith SNI routing]
|
SmartProxy["SmartProxy<br>(TCP/SNI Proxy)"]
|
||||||
NfTables[NfTablesProxy]
|
NfTables[NfTablesProxy]
|
||||||
Router[ProxyRouter]
|
Router[ProxyRouter]
|
||||||
ACME[Port80Handler\nACME/Let's Encrypt]
|
ACME["Port80Handler<br>(ACME HTTP-01)"]
|
||||||
Certs[(SSL Certificates)]
|
Certs[(SSL Certificates)]
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -190,470 +357,104 @@ sequenceDiagram
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **HTTPS Reverse Proxy** - Route traffic to backend services based on hostname with TLS termination
|
- HTTP/HTTPS Reverse Proxy (NetworkProxy)
|
||||||
- **WebSocket Support** - Full WebSocket proxying with heartbeat monitoring
|
• TLS termination, virtual-host routing, HTTP/2 & WebSocket support, pooling & metrics
|
||||||
- **TCP Connection Handling** - Advanced connection handling with SNI inspection and domain-based routing
|
|
||||||
- **Enhanced TLS Handling** - Robust TLS handshake processing with improved certificate error handling
|
|
||||||
- **HTTP to HTTPS Redirection** - Automatically redirect HTTP requests to HTTPS
|
|
||||||
- **Let's Encrypt Integration** - Automatic certificate management using ACME protocol
|
|
||||||
- **IP Filtering** - Control access with IP allow/block lists using glob patterns
|
|
||||||
- **NfTables Integration** - Direct manipulation of nftables for advanced low-level port forwarding
|
|
||||||
|
|
||||||
## Certificate Provider Hook & Events
|
- Automatic ACME Certificates (Port80Handler)
|
||||||
|
• HTTP-01 challenge handling, certificate issuance/renewal, pluggable storage
|
||||||
|
|
||||||
You can customize how certificates are provisioned per domain by using the `certProvider` callback and listen for certificate events emitted by `SmartProxy`.
|
- Low-Level Port Forwarding (NfTablesProxy)
|
||||||
|
• nftables NAT rules for ports/ranges, IPv4/IPv6, IP filtering, QoS & ipset support
|
||||||
|
|
||||||
```typescript
|
- Custom Redirects (Redirect / SslRedirect)
|
||||||
import { SmartProxy } from '@push.rocks/smartproxy';
|
• URL redirects with wildcard host/path, template variables & status codes
|
||||||
import * as fs from 'fs';
|
|
||||||
|
|
||||||
// Example certProvider: static for a specific domain, HTTP-01 otherwise
|
- TCP/SNI Proxy (SmartProxy)
|
||||||
const certProvider = async (domain: string) => {
|
• SNI-based routing, IP allow/block lists, port ranges, timeouts & graceful shutdown
|
||||||
if (domain === 'static.example.com') {
|
|
||||||
// Load from disk or vault
|
|
||||||
return {
|
|
||||||
id: 'static-cert',
|
|
||||||
domainName: domain,
|
|
||||||
created: Date.now(),
|
|
||||||
validUntil: Date.now() + 90 * 24 * 60 * 60 * 1000,
|
|
||||||
privateKey: fs.readFileSync('/etc/ssl/private/static.key', 'utf8'),
|
|
||||||
publicKey: fs.readFileSync('/etc/ssl/certs/static.crt', 'utf8'),
|
|
||||||
csr: ''
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// Fallback to ACME HTTP-01 challenge
|
|
||||||
return 'http01';
|
|
||||||
};
|
|
||||||
|
|
||||||
const proxy = new SmartProxy({
|
- SNI Utilities (SniHandler)
|
||||||
fromPort: 80,
|
• Robust ClientHello parsing, fragmentation & session resumption support
|
||||||
toPort: 8080,
|
|
||||||
domainConfigs: [{
|
|
||||||
domains: ['static.example.com', 'dynamic.example.com'],
|
|
||||||
allowedIPs: ['*']
|
|
||||||
}],
|
|
||||||
certProvider
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listen for certificate issuance or renewal
|
## Certificate Hooks & Events
|
||||||
proxy.on('certificate', (evt) => {
|
|
||||||
console.log(`Certificate for ${evt.domain} ready, expires on ${evt.expiryDate}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
await proxy.start();
|
Listen for certificate events via EventEmitter:
|
||||||
```
|
- **Port80Handler**:
|
||||||
|
- `certificate-issued`, `certificate-renewed`, `certificate-failed`
|
||||||
|
- `manager-started`, `manager-stopped`, `request-forwarded`
|
||||||
|
- **SmartProxy**:
|
||||||
|
- `certificate` (domain, publicKey, privateKey, expiryDate, source, isRenewal)
|
||||||
|
|
||||||
|
Provide a `certProvider(domain)` in SmartProxy settings to supply static certs or return `'http01'`.
|
||||||
|
|
||||||
## Configuration Options
|
## Configuration Options
|
||||||
|
|
||||||
### backendProtocol
|
### NetworkProxy (INetworkProxyOptions)
|
||||||
|
- `port` (number, required)
|
||||||
Type: 'http1' | 'http2' (default: 'http1')
|
- `backendProtocol` ('http1'|'http2', default 'http1')
|
||||||
|
- `maxConnections` (number, default 10000)
|
||||||
Controls the protocol used when proxying requests to backend services. By default, the proxy uses HTTP/1.x (`http.request`). Setting `backendProtocol: 'http2'` establishes HTTP/2 client sessions (`http2.connect`) to your backends for full end-to-end HTTP/2 support (assuming your backend servers support HTTP/2).
|
- `keepAliveTimeout` (ms, default 120000)
|
||||||
|
- `headersTimeout` (ms, default 60000)
|
||||||
Example:
|
- `cors` (object)
|
||||||
```js
|
- `connectionPoolSize` (number, default 50)
|
||||||
import { NetworkProxy } from '@push.rocks/smartproxy';
|
- `logLevel` ('error'|'warn'|'info'|'debug')
|
||||||
|
- `acme` (IAcmeOptions)
|
||||||
const proxy = new NetworkProxy({
|
- `useExternalPort80Handler` (boolean)
|
||||||
port: 8443,
|
- `portProxyIntegration` (boolean)
|
||||||
backendProtocol: 'http2',
|
|
||||||
// other options...
|
### Port80Handler (IAcmeOptions)
|
||||||
});
|
- `enabled` (boolean, default true)
|
||||||
proxy.start();
|
- `port` (number, default 80)
|
||||||
```
|
- `contactEmail` (string)
|
||||||
- **Basic Authentication** - Support for basic auth on proxied routes
|
- `useProduction` (boolean, default false)
|
||||||
- **Connection Management** - Intelligent connection tracking and cleanup with configurable timeouts
|
- `renewThresholdDays` (number, default 30)
|
||||||
- **Browser Compatibility** - Optimized for modern browsers with fixes for common TLS handshake issues
|
- `autoRenew` (boolean, default true)
|
||||||
|
- `certificateStore` (string)
|
||||||
## Installation
|
- `skipConfiguredCerts` (boolean)
|
||||||
|
- `domainForwards` (IDomainForwardConfig[])
|
||||||
```bash
|
|
||||||
npm install @push.rocks/smartproxy
|
### NfTablesProxy (INfTableProxySettings)
|
||||||
```
|
- `fromPort` / `toPort` (number|range|array)
|
||||||
|
- `toHost` (string, default 'localhost')
|
||||||
## Usage
|
- `preserveSourceIP`, `deleteOnExit`, `protocol`, `enableLogging`, `ipv6Support` (booleans)
|
||||||
|
- `allowedSourceIPs`, `bannedSourceIPs` (string[])
|
||||||
### Basic Reverse Proxy Setup
|
- `useIPSets` (boolean, default true)
|
||||||
|
- `qos`, `netProxyIntegration` (objects)
|
||||||
```typescript
|
|
||||||
import { NetworkProxy } from '@push.rocks/smartproxy';
|
### Redirect / SslRedirect
|
||||||
|
- Constructor options: `httpPort`, `httpsPort`, `sslOptions`, `rules` (RedirectRule[])
|
||||||
// Create a reverse proxy listening on port 443
|
|
||||||
const proxy = new NetworkProxy({
|
### SmartProxy (IPortProxySettings)
|
||||||
port: 443
|
- `fromPort`, `toPort` (number)
|
||||||
});
|
- `domainConfigs` (IDomainConfig[])
|
||||||
|
- `sniEnabled`, `defaultAllowedIPs`, `preserveSourceIP` (booleans)
|
||||||
// Define reverse proxy configurations
|
- Timeouts: `initialDataTimeout`, `socketTimeout`, `inactivityTimeout`, etc.
|
||||||
const proxyConfigs = [
|
- Socket opts: `noDelay`, `keepAlive`, `enableKeepAliveProbes`
|
||||||
{
|
- `acme` (IAcmeOptions), `certProvider` (callback)
|
||||||
hostName: 'example.com',
|
- `useNetworkProxy` (number[]), `networkProxyPort` (number)
|
||||||
destinationIps: ['127.0.0.1'],
|
|
||||||
destinationPorts: [3000],
|
|
||||||
publicKey: 'your-cert-content',
|
|
||||||
privateKey: 'your-key-content',
|
|
||||||
rewriteHostHeader: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hostName: 'api.example.com',
|
|
||||||
destinationIps: ['127.0.0.1'],
|
|
||||||
destinationPorts: [4000],
|
|
||||||
publicKey: 'your-cert-content',
|
|
||||||
privateKey: 'your-key-content',
|
|
||||||
// Optional basic auth
|
|
||||||
authentication: {
|
|
||||||
type: 'Basic',
|
|
||||||
user: 'admin',
|
|
||||||
pass: 'secret'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// Start the proxy and update configurations
|
|
||||||
(async () => {
|
|
||||||
await proxy.start();
|
|
||||||
await proxy.updateProxyConfigs(proxyConfigs);
|
|
||||||
|
|
||||||
// Add default headers to all responses
|
|
||||||
await proxy.addDefaultHeaders({
|
|
||||||
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains; preload'
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
```
|
|
||||||
|
|
||||||
### HTTP to HTTPS Redirection
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { SslRedirect } from '@push.rocks/smartproxy';
|
|
||||||
|
|
||||||
// Create and start HTTP to HTTPS redirect service on port 80
|
|
||||||
const redirector = new SslRedirect(80);
|
|
||||||
redirector.start();
|
|
||||||
```
|
|
||||||
|
|
||||||
### TCP Connection Handling with Domain-based Routing
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { SmartProxy } from '@push.rocks/smartproxy';
|
|
||||||
|
|
||||||
// Configure SmartProxy with domain-based routing
|
|
||||||
const smartProxy = new SmartProxy({
|
|
||||||
fromPort: 443,
|
|
||||||
toPort: 8443,
|
|
||||||
targetIP: 'localhost', // Default target host
|
|
||||||
sniEnabled: true, // Enable SNI inspection
|
|
||||||
|
|
||||||
// Enhanced reliability settings
|
|
||||||
initialDataTimeout: 60000, // 60 seconds for initial TLS handshake
|
|
||||||
socketTimeout: 3600000, // 1 hour socket timeout
|
|
||||||
maxConnectionLifetime: 3600000, // 1 hour connection lifetime
|
|
||||||
inactivityTimeout: 3600000, // 1 hour inactivity timeout
|
|
||||||
maxPendingDataSize: 10 * 1024 * 1024, // 10MB buffer for large TLS handshakes
|
|
||||||
|
|
||||||
// Browser compatibility enhancement
|
|
||||||
enableTlsDebugLogging: false, // Enable for troubleshooting TLS issues
|
|
||||||
|
|
||||||
// Port and IP configuration
|
|
||||||
globalPortRanges: [{ from: 443, to: 443 }],
|
|
||||||
defaultAllowedIPs: ['*'], // Allow all IPs by default
|
|
||||||
|
|
||||||
// Socket optimizations for better connection stability
|
|
||||||
noDelay: true, // Disable Nagle's algorithm
|
|
||||||
keepAlive: true, // Enable TCP keepalive
|
|
||||||
enableKeepAliveProbes: true, // Enhanced keepalive for stability
|
|
||||||
|
|
||||||
// Domain-specific routing configuration
|
|
||||||
domainConfigs: [
|
|
||||||
{
|
|
||||||
domains: ['example.com', '*.example.com'], // Glob patterns for matching domains
|
|
||||||
allowedIPs: ['192.168.1.*'], // Restrict access by IP
|
|
||||||
blockedIPs: ['192.168.1.100'], // Block specific IPs
|
|
||||||
targetIPs: ['10.0.0.1', '10.0.0.2'], // Round-robin between multiple targets
|
|
||||||
portRanges: [{ from: 443, to: 443 }],
|
|
||||||
connectionTimeout: 7200000 // Domain-specific timeout (2 hours)
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
preserveSourceIP: true
|
|
||||||
});
|
|
||||||
|
|
||||||
smartProxy.start();
|
|
||||||
```
|
|
||||||
|
|
||||||
### NfTables Port Forwarding
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { NfTablesProxy } from '@push.rocks/smartproxy';
|
|
||||||
|
|
||||||
// Basic usage - forward single port
|
|
||||||
const basicProxy = new NfTablesProxy({
|
|
||||||
fromPort: 80,
|
|
||||||
toPort: 8080,
|
|
||||||
toHost: 'localhost',
|
|
||||||
preserveSourceIP: true,
|
|
||||||
deleteOnExit: true // Automatically clean up rules on process exit
|
|
||||||
});
|
|
||||||
|
|
||||||
// Forward port ranges
|
|
||||||
const rangeProxy = new NfTablesProxy({
|
|
||||||
fromPort: { from: 3000, to: 3010 }, // Forward ports 3000-3010
|
|
||||||
toPort: { from: 8000, to: 8010 }, // To ports 8000-8010
|
|
||||||
protocol: 'tcp', // TCP protocol (default)
|
|
||||||
ipv6Support: true, // Enable IPv6 support
|
|
||||||
enableLogging: true // Enable detailed logging
|
|
||||||
});
|
|
||||||
|
|
||||||
// Multiple port specifications with IP filtering
|
|
||||||
const advancedProxy = new NfTablesProxy({
|
|
||||||
fromPort: [80, 443, { from: 8000, to: 8010 }], // Multiple ports/ranges
|
|
||||||
toPort: [8080, 8443, { from: 18000, to: 18010 }],
|
|
||||||
allowedSourceIPs: ['10.0.0.0/8', '192.168.1.0/24'], // Only allow these IPs
|
|
||||||
bannedSourceIPs: ['192.168.1.100'], // Explicitly block these IPs
|
|
||||||
useIPSets: true, // Use IP sets for efficient IP management
|
|
||||||
forceCleanSlate: false // Clean all NfTablesProxy rules before starting
|
|
||||||
});
|
|
||||||
|
|
||||||
// Advanced features: QoS, connection tracking, and NetworkProxy integration
|
|
||||||
const advancedProxy = new NfTablesProxy({
|
|
||||||
fromPort: 443,
|
|
||||||
toPort: 8443,
|
|
||||||
toHost: 'localhost',
|
|
||||||
useAdvancedNAT: true, // Use connection tracking for stateful NAT
|
|
||||||
qos: {
|
|
||||||
enabled: true,
|
|
||||||
maxRate: '10mbps', // Limit bandwidth
|
|
||||||
priority: 1 // Set traffic priority (1-10)
|
|
||||||
},
|
|
||||||
netProxyIntegration: {
|
|
||||||
enabled: true,
|
|
||||||
redirectLocalhost: true, // Redirect localhost traffic to NetworkProxy
|
|
||||||
sslTerminationPort: 8443 // Port where NetworkProxy handles SSL
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Start any of the proxies
|
|
||||||
await basicProxy.start();
|
|
||||||
```
|
|
||||||
|
|
||||||
### Automatic HTTPS Certificate Management
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { Port80Handler } from '@push.rocks/smartproxy';
|
|
||||||
|
|
||||||
// Create an ACME handler for Let's Encrypt
|
|
||||||
const acmeHandler = new Port80Handler({
|
|
||||||
port: 80,
|
|
||||||
contactEmail: 'admin@example.com',
|
|
||||||
useProduction: true, // Use Let's Encrypt production servers (default is staging)
|
|
||||||
renewThresholdDays: 30, // Renew certificates 30 days before expiry
|
|
||||||
httpsRedirectPort: 443 // Redirect HTTP to HTTPS on this port
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add domains to manage certificates for
|
|
||||||
acmeHandler.addDomain({
|
|
||||||
domainName: 'example.com',
|
|
||||||
sslRedirect: true,
|
|
||||||
acmeMaintenance: true
|
|
||||||
});
|
|
||||||
|
|
||||||
acmeHandler.addDomain({
|
|
||||||
domainName: 'api.example.com',
|
|
||||||
sslRedirect: true,
|
|
||||||
acmeMaintenance: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Support for glob pattern domains for routing (certificates not issued for glob patterns)
|
|
||||||
acmeHandler.addDomain({
|
|
||||||
domainName: '*.example.com',
|
|
||||||
sslRedirect: true,
|
|
||||||
acmeMaintenance: false, // Can't issue certificates for wildcard domains via HTTP-01
|
|
||||||
forward: { ip: '192.168.1.10', port: 8080 } // Forward requests to this target
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration Options
|
|
||||||
|
|
||||||
### NetworkProxy Options
|
|
||||||
|
|
||||||
| Option | Description | Default |
|
|
||||||
|----------------|---------------------------------------------------|---------|
|
|
||||||
| `port` | Port to listen on for HTTPS connections | - |
|
|
||||||
| `maxConnections` | Maximum concurrent connections | 10000 |
|
|
||||||
| `keepAliveTimeout` | Keep-alive timeout in milliseconds | 60000 |
|
|
||||||
| `headersTimeout` | Headers timeout in milliseconds | 60000 |
|
|
||||||
| `logLevel` | Logging level ('error', 'warn', 'info', 'debug') | 'info' |
|
|
||||||
| `cors` | CORS configuration object | - |
|
|
||||||
| `rewriteHostHeader` | Whether to rewrite the Host header | false |
|
|
||||||
|
|
||||||
### SmartProxy Settings
|
|
||||||
|
|
||||||
| Option | Description | Default |
|
|
||||||
|---------------------------|--------------------------------------------------------|-------------|
|
|
||||||
| `fromPort` | Port to listen on | - |
|
|
||||||
| `toPort` | Destination port to forward to | - |
|
|
||||||
| `targetIP` | Default destination IP if not specified in domainConfig | 'localhost' |
|
|
||||||
| `sniEnabled` | Enable SNI inspection for TLS connections | false |
|
|
||||||
| `defaultAllowedIPs` | IP patterns allowed by default | - |
|
|
||||||
| `defaultBlockedIPs` | IP patterns blocked by default | - |
|
|
||||||
| `preserveSourceIP` | Preserve the original client IP | false |
|
|
||||||
| `maxConnectionLifetime` | Maximum time in ms to keep a connection open | 3600000 |
|
|
||||||
| `initialDataTimeout` | Timeout for initial data/handshake in ms | 60000 |
|
|
||||||
| `socketTimeout` | Socket inactivity timeout in ms | 3600000 |
|
|
||||||
| `inactivityTimeout` | Connection inactivity check timeout in ms | 3600000 |
|
|
||||||
| `inactivityCheckInterval` | How often to check for inactive connections in ms | 60000 |
|
|
||||||
| `maxPendingDataSize` | Maximum bytes to buffer during connection setup | 10485760 |
|
|
||||||
| `globalPortRanges` | Array of port ranges to listen on | - |
|
|
||||||
| `forwardAllGlobalRanges` | Forward all global range connections to targetIP | false |
|
|
||||||
| `gracefulShutdownTimeout` | Time in ms to wait during shutdown | 30000 |
|
|
||||||
| `noDelay` | Disable Nagle's algorithm | true |
|
|
||||||
| `keepAlive` | Enable TCP keepalive | true |
|
|
||||||
| `keepAliveInitialDelay` | Initial delay before sending keepalive probes in ms | 30000 |
|
|
||||||
| `enableKeepAliveProbes` | Enable enhanced TCP keep-alive probes | false |
|
|
||||||
| `enableTlsDebugLogging` | Enable detailed TLS handshake debugging | false |
|
|
||||||
| `enableDetailedLogging` | Enable detailed connection logging | false |
|
|
||||||
| `enableRandomizedTimeouts`| Randomize timeouts slightly to prevent thundering herd | true |
|
|
||||||
|
|
||||||
### NfTablesProxy Settings
|
|
||||||
|
|
||||||
| Option | Description | Default |
|
|
||||||
|-----------------------|---------------------------------------------------|-------------|
|
|
||||||
| `fromPort` | Source port(s) or range(s) to forward from | - |
|
|
||||||
| `toPort` | Destination port(s) or range(s) to forward to | - |
|
|
||||||
| `toHost` | Destination host to forward to | 'localhost' |
|
|
||||||
| `preserveSourceIP` | Preserve the original client IP | false |
|
|
||||||
| `deleteOnExit` | Remove nftables rules when process exits | false |
|
|
||||||
| `protocol` | Protocol to forward ('tcp', 'udp', or 'all') | 'tcp' |
|
|
||||||
| `enableLogging` | Enable detailed logging | false |
|
|
||||||
| `logFormat` | Format for logs ('plain' or 'json') | 'plain' |
|
|
||||||
| `ipv6Support` | Enable IPv6 support | false |
|
|
||||||
| `allowedSourceIPs` | Array of IP addresses/CIDR allowed to connect | - |
|
|
||||||
| `bannedSourceIPs` | Array of IP addresses/CIDR blocked from connecting | - |
|
|
||||||
| `useIPSets` | Use nftables sets for efficient IP management | true |
|
|
||||||
| `forceCleanSlate` | Clear all NfTablesProxy rules before starting | false |
|
|
||||||
| `tableName` | Custom table name | 'portproxy' |
|
|
||||||
| `maxRetries` | Maximum number of retries for failed commands | 3 |
|
|
||||||
| `retryDelayMs` | Delay between retries in milliseconds | 1000 |
|
|
||||||
| `useAdvancedNAT` | Use connection tracking for stateful NAT | false |
|
|
||||||
| `qos` | Quality of Service options (object) | - |
|
|
||||||
| `netProxyIntegration` | NetworkProxy integration options (object) | - |
|
|
||||||
|
|
||||||
## Advanced Features
|
|
||||||
|
|
||||||
### TLS Handshake Optimization
|
|
||||||
|
|
||||||
The enhanced `SmartProxy` implementation includes significant improvements for TLS handshake handling:
|
|
||||||
|
|
||||||
- Robust SNI extraction with improved error handling
|
|
||||||
- Increased buffer size for complex TLS handshakes (10MB)
|
|
||||||
- Longer initial handshake timeout (60 seconds)
|
|
||||||
- Detection and tracking of TLS connection states
|
|
||||||
- Optional detailed TLS debug logging for troubleshooting
|
|
||||||
- Browser compatibility fixes for Chrome certificate errors
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Example configuration to solve Chrome certificate errors
|
|
||||||
const portProxy = new SmartProxy({
|
|
||||||
// ... other settings
|
|
||||||
initialDataTimeout: 60000, // Give browser more time for handshake
|
|
||||||
maxPendingDataSize: 10 * 1024 * 1024, // Larger buffer for complex handshakes
|
|
||||||
enableTlsDebugLogging: true, // Enable when troubleshooting
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Connection Management and Monitoring
|
|
||||||
|
|
||||||
The `SmartProxy` class includes built-in connection tracking and monitoring:
|
|
||||||
|
|
||||||
- Automatic cleanup of idle connections with configurable timeouts
|
|
||||||
- Timeouts for connections that exceed maximum lifetime
|
|
||||||
- Detailed logging of connection states
|
|
||||||
- Termination statistics
|
|
||||||
- Randomized timeouts to prevent "thundering herd" problems
|
|
||||||
- Per-domain timeout configuration
|
|
||||||
|
|
||||||
### WebSocket Support
|
|
||||||
|
|
||||||
The `NetworkProxy` class provides WebSocket support with:
|
|
||||||
|
|
||||||
- WebSocket connection proxying
|
|
||||||
- Automatic heartbeat monitoring
|
|
||||||
- Connection cleanup for inactive WebSockets
|
|
||||||
|
|
||||||
### SNI-based Routing
|
|
||||||
|
|
||||||
The `SmartProxy` class can inspect the SNI (Server Name Indication) field in TLS handshakes to route connections based on the requested domain:
|
|
||||||
|
|
||||||
- Multiple backend targets per domain
|
|
||||||
- Round-robin load balancing
|
|
||||||
- Domain-specific allowed IP ranges
|
|
||||||
- Protection against SNI renegotiation attacks
|
|
||||||
|
|
||||||
### Enhanced NfTables Management
|
|
||||||
|
|
||||||
The `NfTablesProxy` class offers advanced capabilities:
|
|
||||||
|
|
||||||
- Support for multiple port ranges and individual ports
|
|
||||||
- More efficient IP filtering using nftables sets
|
|
||||||
- IPv6 support with full feature parity
|
|
||||||
- Quality of Service (QoS) features including bandwidth limiting and traffic prioritization
|
|
||||||
- Advanced connection tracking for stateful NAT
|
|
||||||
- Robust error handling with retry mechanisms
|
|
||||||
- Structured logging with JSON support
|
|
||||||
- NetworkProxy integration for SSL termination
|
|
||||||
- Comprehensive cleanup on shutdown
|
|
||||||
|
|
||||||
### Port80Handler with Glob Pattern Support
|
|
||||||
|
|
||||||
The `Port80Handler` class includes support for glob pattern domain matching:
|
|
||||||
|
|
||||||
- Supports wildcard domains like `*.example.com` for HTTP request routing
|
|
||||||
- Detects glob patterns and skips certificate issuance for them
|
|
||||||
- Smart routing that first attempts exact matches, then tries pattern matching
|
|
||||||
- Supports forwarding HTTP requests to backend services
|
|
||||||
- Separate forwarding configuration for ACME challenges
|
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### Browser Certificate Errors
|
### NetworkProxy
|
||||||
|
- Verify ports, certificates and `rejectUnauthorized` for TLS errors
|
||||||
|
- Configure CORS or use `addDefaultHeaders` for preflight issues
|
||||||
|
- Increase `maxConnections` or `connectionPoolSize` under load
|
||||||
|
|
||||||
If you experience certificate errors in browsers, especially in Chrome, try these solutions:
|
### Port80Handler
|
||||||
|
- Run as root or grant CAP_NET_BIND_SERVICE for port 80
|
||||||
|
- Inspect `certificate-failed` events and switch staging/production
|
||||||
|
|
||||||
1. **Increase Initial Data Timeout**: Set `initialDataTimeout` to 60 seconds or higher
|
### NfTablesProxy
|
||||||
2. **Increase Buffer Size**: Set `maxPendingDataSize` to 10MB or higher
|
- Ensure `nft` is installed and run with sufficient privileges
|
||||||
3. **Enable TLS Debug Logging**: Set `enableTlsDebugLogging: true` to troubleshoot handshake issues
|
- Use `forceCleanSlate:true` to clear conflicting rules
|
||||||
4. **Enable Keep-Alive Probes**: Set `enableKeepAliveProbes: true` for better connection stability
|
|
||||||
5. **Check Certificate Chain**: Ensure your certificate chain is complete and in the correct order
|
|
||||||
|
|
||||||
```typescript
|
### Redirect / SslRedirect
|
||||||
// Configuration to fix Chrome certificate errors
|
- Check `fromHost`/`fromPath` patterns and Host headers
|
||||||
const smartProxy = new SmartProxy({
|
- Validate `sslOptions` key/cert correctness
|
||||||
// ... other settings
|
|
||||||
initialDataTimeout: 60000,
|
|
||||||
maxPendingDataSize: 10 * 1024 * 1024,
|
|
||||||
enableTlsDebugLogging: true,
|
|
||||||
enableKeepAliveProbes: true
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Connection Stability
|
### SmartProxy & SniHandler
|
||||||
|
- Increase `initialDataTimeout`/`maxPendingDataSize` for large ClientHello
|
||||||
For improved connection stability in high-traffic environments:
|
- Enable `enableTlsDebugLogging` to trace handshake
|
||||||
|
- Ensure `allowSessionTicket` and fragmentation support for resumption
|
||||||
1. **Set Appropriate Timeouts**: Use longer timeouts for long-lived connections
|
|
||||||
2. **Use Domain-Specific Timeouts**: Configure per-domain timeouts for different types of services
|
|
||||||
3. **Enable TCP Keep-Alive**: Ensure `keepAlive` is set to `true`
|
|
||||||
4. **Monitor Connection Statistics**: Enable detailed logging to track termination reasons
|
|
||||||
5. **Fine-tune Inactivity Checks**: Adjust `inactivityCheckInterval` based on your traffic patterns
|
|
||||||
|
|
||||||
### NfTables Troubleshooting
|
|
||||||
|
|
||||||
If you're experiencing issues with NfTablesProxy:
|
|
||||||
|
|
||||||
1. **Enable Detailed Logging**: Set `enableLogging: true` to see all rule operations
|
|
||||||
2. **Force Clean Slate**: Use `forceCleanSlate: true` to remove any lingering rules
|
|
||||||
3. **Use IP Sets**: Enable `useIPSets: true` for cleaner rule management
|
|
||||||
4. **Check Permissions**: Ensure your process has sufficient permissions to modify nftables
|
|
||||||
5. **Verify IPv6 Support**: If using `ipv6Support: true`, ensure ip6tables is available
|
|
||||||
|
|
||||||
## License and Legal Information
|
## License and Legal Information
|
||||||
|
|
||||||
|
@ -1,47 +1,29 @@
|
|||||||
## Refactor: Introduce a Unified CertProvisioner for Certificate Lifecycle
|
# Project Simplification Plan
|
||||||
|
|
||||||
- [x] Ensure Port80Handler is challenge-only:
|
This document outlines a roadmap to simplify and refactor the SmartProxy & NetworkProxy codebase for better maintainability, reduced duplication, and clearer configuration.
|
||||||
- Remove any internal scheduling and deprecated ACME flows (`getAcmeClient`, `processAuthorizations`, `handleAcmeChallenge`) from Port80Handler.
|
|
||||||
- Remove legacy ACME options (`renewThresholdDays`, `renewCheckIntervalHours`, `mongoDescriptor`, etc.) from `IPort80HandlerOptions`.
|
|
||||||
- Retain only methods for HTTP-01 challenge and direct renewals (`obtainCertificate`, `renewCertificate`, `getDomainCertificateStatus`).
|
|
||||||
- [x] Clean up deprecated `acme` configuration:
|
|
||||||
- Remove the `acme` property from `IPortProxySettings` and all legacy references in code.
|
|
||||||
|
|
||||||
- [x] Implement `CertProvisioner` component:
|
## Goals
|
||||||
- [x] Create class `ts/smartproxy/classes.pp.certprovisioner.ts`.
|
- Eliminate duplicate code and shared types
|
||||||
- [x] Constructor accepts:
|
- Unify certificate management flow across components
|
||||||
* `domainConfigs: IDomainConfig[]`
|
- Simplify configuration schemas and option handling
|
||||||
* `port80Handler: Port80Handler`
|
- Centralize plugin imports and module interfaces
|
||||||
* `networkProxyBridge: NetworkProxyBridge`
|
- Strengthen type safety and linting
|
||||||
* optional `certProvider: (domain) => Promise<ICert | 'http01'>`
|
- Improve test coverage and CI integration
|
||||||
* `renewThresholdDays`, `renewCheckIntervalHours`, `autoRenew` settings.
|
|
||||||
- Responsibilities:
|
|
||||||
* Initial provisioning: static vs HTTP-01.
|
|
||||||
* Subscribe to Port80Handler events (CERTIFICATE_ISSUED/RENEWED) and to static cert updates.
|
|
||||||
* Re-emit unified `'certificate'` events to SmartProxy.
|
|
||||||
* Central scheduling of renewals via `@push.rocks/taskbuffer`.
|
|
||||||
|
|
||||||
- [x] Refactor SmartProxy:
|
## Plan
|
||||||
- [x] Remove existing scheduling / renewal logic.
|
- [x] Extract all shared interfaces and types (e.g., certificate, proxy, domain configs) into a common `ts/common` module
|
||||||
- [x] Instantiate `CertProvisioner` in `start()`, delegate cert workflows entirely.
|
- [x] Consolidate ACME/Port80Handler logic:
|
||||||
- [x] Forward CertProvisioner events to SmartProxy’s `'certificate'` listener.
|
- [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
|
||||||
|
|
||||||
- [x] CertProvisioner lifecycle methods:
|
Once these steps are complete, the project will be cleaner, easier to understand, and simpler to extend.
|
||||||
- [x] `start()`: provision all domains, start scheduler.
|
|
||||||
- [x] `stop()`: stop scheduler.
|
|
||||||
- [x] `requestCertificate(domain)`: on-demand provisioning.
|
|
||||||
|
|
||||||
- [x] Handle static certificate auto-refresh:
|
|
||||||
- [x] In the renewal scheduler, for domains with static certs, re-call `certProvider(domain)` near expiry.
|
|
||||||
- [x] Apply returned cert via `networkProxyBridge.applyExternalCertificate()`.
|
|
||||||
|
|
||||||
- [ ] Tests:
|
|
||||||
- Unit tests for `CertProvisioner`, mocking Port80Handler and `certProvider`:
|
|
||||||
* Validate initial provisioning and dynamic/static flows.
|
|
||||||
* Validate scheduling triggers correct renewals.
|
|
||||||
- Integration tests:
|
|
||||||
* Use actual in-memory Port80Handler with short intervals to verify renewals and event emission.
|
|
||||||
|
|
||||||
- [ ] Documentation:
|
|
||||||
- Add code-level TS doc for `CertProvisioner` API (options, methods, events).
|
|
||||||
- Update root `README.md` and architecture diagrams to show `CertProvisioner` role.
|
|
@ -1,45 +0,0 @@
|
|||||||
import { tap, expect } from '@push.rocks/tapbundle';
|
|
||||||
import { SmartProxy } from '../ts/smartproxy/classes.smartproxy.js';
|
|
||||||
|
|
||||||
tap.test('performRenewals only renews domains below threshold', async () => {
|
|
||||||
// Set up SmartProxy instance without real servers
|
|
||||||
const proxy = new SmartProxy({
|
|
||||||
fromPort: 0,
|
|
||||||
toPort: 0,
|
|
||||||
domainConfigs: [],
|
|
||||||
sniEnabled: false,
|
|
||||||
defaultAllowedIPs: [],
|
|
||||||
globalPortRanges: []
|
|
||||||
});
|
|
||||||
// Stub port80Handler status and renewal
|
|
||||||
const statuses = new Map<string, any>();
|
|
||||||
const now = new Date();
|
|
||||||
statuses.set('expiring.com', {
|
|
||||||
certObtained: true,
|
|
||||||
expiryDate: new Date(now.getTime() + 2 * 24 * 60 * 60 * 1000),
|
|
||||||
obtainingInProgress: false
|
|
||||||
});
|
|
||||||
statuses.set('ok.com', {
|
|
||||||
certObtained: true,
|
|
||||||
expiryDate: new Date(now.getTime() + 100 * 24 * 60 * 60 * 1000),
|
|
||||||
obtainingInProgress: false
|
|
||||||
});
|
|
||||||
const renewed: string[] = [];
|
|
||||||
// Inject fake handler
|
|
||||||
(proxy as any).port80Handler = {
|
|
||||||
getDomainCertificateStatus: () => statuses,
|
|
||||||
renewCertificate: async (domain: string) => { renewed.push(domain); }
|
|
||||||
};
|
|
||||||
// Configure threshold
|
|
||||||
proxy.settings.port80HandlerConfig.enabled = true;
|
|
||||||
proxy.settings.port80HandlerConfig.autoRenew = true;
|
|
||||||
proxy.settings.port80HandlerConfig.renewThresholdDays = 10;
|
|
||||||
|
|
||||||
// Execute renewals
|
|
||||||
await (proxy as any).performRenewals();
|
|
||||||
|
|
||||||
// Only the expiring.com domain should be renewed
|
|
||||||
expect(renewed).toEqual(['expiring.com']);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartproxy',
|
name: '@push.rocks/smartproxy',
|
||||||
version: '8.0.0',
|
version: '10.0.6',
|
||||||
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.'
|
description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.'
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
import * as http from 'http';
|
import * as plugins from './plugins.js';
|
||||||
import * as url from 'url';
|
|
||||||
import * as tsclass from '@tsclass/tsclass';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional path pattern configuration that can be added to proxy configs
|
* Optional path pattern configuration that can be added to proxy configs
|
||||||
@ -13,7 +11,7 @@ export interface IPathPatternConfig {
|
|||||||
* Interface for router result with additional metadata
|
* Interface for router result with additional metadata
|
||||||
*/
|
*/
|
||||||
export interface IRouterResult {
|
export interface IRouterResult {
|
||||||
config: tsclass.network.IReverseProxyConfig;
|
config: plugins.tsclass.network.IReverseProxyConfig;
|
||||||
pathMatch?: string;
|
pathMatch?: string;
|
||||||
pathParams?: Record<string, string>;
|
pathParams?: Record<string, string>;
|
||||||
pathRemainder?: string;
|
pathRemainder?: string;
|
||||||
@ -36,11 +34,11 @@ export interface IRouterResult {
|
|||||||
*/
|
*/
|
||||||
export class ProxyRouter {
|
export class ProxyRouter {
|
||||||
// Store original configs for reference
|
// Store original configs for reference
|
||||||
private reverseProxyConfigs: tsclass.network.IReverseProxyConfig[] = [];
|
private reverseProxyConfigs: plugins.tsclass.network.IReverseProxyConfig[] = [];
|
||||||
// Default config to use when no match is found (optional)
|
// Default config to use when no match is found (optional)
|
||||||
private defaultConfig?: tsclass.network.IReverseProxyConfig;
|
private defaultConfig?: plugins.tsclass.network.IReverseProxyConfig;
|
||||||
// Store path patterns separately since they're not in the original interface
|
// Store path patterns separately since they're not in the original interface
|
||||||
private pathPatterns: Map<tsclass.network.IReverseProxyConfig, string> = new Map();
|
private pathPatterns: Map<plugins.tsclass.network.IReverseProxyConfig, string> = new Map();
|
||||||
// Logger interface
|
// Logger interface
|
||||||
private logger: {
|
private logger: {
|
||||||
error: (message: string, data?: any) => void;
|
error: (message: string, data?: any) => void;
|
||||||
@ -50,7 +48,7 @@ export class ProxyRouter {
|
|||||||
};
|
};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
configs?: tsclass.network.IReverseProxyConfig[],
|
configs?: plugins.tsclass.network.IReverseProxyConfig[],
|
||||||
logger?: {
|
logger?: {
|
||||||
error: (message: string, data?: any) => void;
|
error: (message: string, data?: any) => void;
|
||||||
warn: (message: string, data?: any) => void;
|
warn: (message: string, data?: any) => void;
|
||||||
@ -68,7 +66,7 @@ export class ProxyRouter {
|
|||||||
* Sets a new set of reverse configs to be routed to
|
* Sets a new set of reverse configs to be routed to
|
||||||
* @param reverseCandidatesArg Array of reverse proxy configurations
|
* @param reverseCandidatesArg Array of reverse proxy configurations
|
||||||
*/
|
*/
|
||||||
public setNewProxyConfigs(reverseCandidatesArg: tsclass.network.IReverseProxyConfig[]): void {
|
public setNewProxyConfigs(reverseCandidatesArg: plugins.tsclass.network.IReverseProxyConfig[]): void {
|
||||||
this.reverseProxyConfigs = [...reverseCandidatesArg];
|
this.reverseProxyConfigs = [...reverseCandidatesArg];
|
||||||
|
|
||||||
// Find default config if any (config with "*" as hostname)
|
// Find default config if any (config with "*" as hostname)
|
||||||
@ -82,7 +80,7 @@ export class ProxyRouter {
|
|||||||
* @param req The incoming HTTP request
|
* @param req The incoming HTTP request
|
||||||
* @returns The matching proxy config or undefined if no match found
|
* @returns The matching proxy config or undefined if no match found
|
||||||
*/
|
*/
|
||||||
public routeReq(req: http.IncomingMessage): tsclass.network.IReverseProxyConfig {
|
public routeReq(req: plugins.http.IncomingMessage): plugins.tsclass.network.IReverseProxyConfig {
|
||||||
const result = this.routeReqWithDetails(req);
|
const result = this.routeReqWithDetails(req);
|
||||||
return result ? result.config : undefined;
|
return result ? result.config : undefined;
|
||||||
}
|
}
|
||||||
@ -92,7 +90,7 @@ export class ProxyRouter {
|
|||||||
* @param req The incoming HTTP request
|
* @param req The incoming HTTP request
|
||||||
* @returns Detailed routing result including matched config and path information
|
* @returns Detailed routing result including matched config and path information
|
||||||
*/
|
*/
|
||||||
public routeReqWithDetails(req: http.IncomingMessage): IRouterResult | undefined {
|
public routeReqWithDetails(req: plugins.http.IncomingMessage): IRouterResult | undefined {
|
||||||
// Extract and validate host header
|
// Extract and validate host header
|
||||||
const originalHost = req.headers.host;
|
const originalHost = req.headers.host;
|
||||||
if (!originalHost) {
|
if (!originalHost) {
|
||||||
@ -101,7 +99,7 @@ export class ProxyRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse URL for path matching
|
// Parse URL for path matching
|
||||||
const parsedUrl = url.parse(req.url || '/');
|
const parsedUrl = plugins.url.parse(req.url || '/');
|
||||||
const urlPath = parsedUrl.pathname || '/';
|
const urlPath = parsedUrl.pathname || '/';
|
||||||
|
|
||||||
// Extract hostname without port
|
// Extract hostname without port
|
||||||
@ -351,7 +349,7 @@ export class ProxyRouter {
|
|||||||
* Gets all currently active proxy configurations
|
* Gets all currently active proxy configurations
|
||||||
* @returns Array of all active configurations
|
* @returns Array of all active configurations
|
||||||
*/
|
*/
|
||||||
public getProxyConfigs(): tsclass.network.IReverseProxyConfig[] {
|
public getProxyConfigs(): plugins.tsclass.network.IReverseProxyConfig[] {
|
||||||
return [...this.reverseProxyConfigs];
|
return [...this.reverseProxyConfigs];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -375,7 +373,7 @@ export class ProxyRouter {
|
|||||||
* @param pathPattern Optional path pattern for route matching
|
* @param pathPattern Optional path pattern for route matching
|
||||||
*/
|
*/
|
||||||
public addProxyConfig(
|
public addProxyConfig(
|
||||||
config: tsclass.network.IReverseProxyConfig,
|
config: plugins.tsclass.network.IReverseProxyConfig,
|
||||||
pathPattern?: string
|
pathPattern?: string
|
||||||
): void {
|
): void {
|
||||||
this.reverseProxyConfigs.push(config);
|
this.reverseProxyConfigs.push(config);
|
||||||
@ -393,7 +391,7 @@ export class ProxyRouter {
|
|||||||
* @returns Boolean indicating if the config was found and updated
|
* @returns Boolean indicating if the config was found and updated
|
||||||
*/
|
*/
|
||||||
public setPathPattern(
|
public setPathPattern(
|
||||||
config: tsclass.network.IReverseProxyConfig,
|
config: plugins.tsclass.network.IReverseProxyConfig,
|
||||||
pathPattern: string
|
pathPattern: string
|
||||||
): boolean {
|
): boolean {
|
||||||
const exists = this.reverseProxyConfigs.includes(config);
|
const exists = this.reverseProxyConfigs.includes(config);
|
||||||
|
23
ts/common/acmeFactory.ts
Normal file
23
ts/common/acmeFactory.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import type { IAcmeOptions } from './types.js';
|
||||||
|
import { Port80Handler } from '../port80handler/classes.port80handler.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) {
|
||||||
|
const certStorePath = path.resolve(options.certificateStore);
|
||||||
|
if (!fs.existsSync(certStorePath)) {
|
||||||
|
fs.mkdirSync(certStorePath, { recursive: true });
|
||||||
|
console.log(`Created certificate store directory: ${certStorePath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Port80Handler(options);
|
||||||
|
}
|
34
ts/common/eventUtils.ts
Normal file
34
ts/common/eventUtils.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import type { Port80Handler } from '../port80handler/classes.port80handler.js';
|
||||||
|
import { Port80HandlerEvents } from './types.js';
|
||||||
|
import type { ICertificateData, ICertificateFailure, ICertificateExpiring } from './types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribers callback definitions for Port80Handler events
|
||||||
|
*/
|
||||||
|
export interface Port80HandlerSubscribers {
|
||||||
|
onCertificateIssued?: (data: ICertificateData) => void;
|
||||||
|
onCertificateRenewed?: (data: ICertificateData) => void;
|
||||||
|
onCertificateFailed?: (data: ICertificateFailure) => void;
|
||||||
|
onCertificateExpiring?: (data: ICertificateExpiring) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes to Port80Handler events based on provided callbacks
|
||||||
|
*/
|
||||||
|
export function subscribeToPort80Handler(
|
||||||
|
handler: Port80Handler,
|
||||||
|
subscribers: Port80HandlerSubscribers
|
||||||
|
): 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);
|
||||||
|
}
|
||||||
|
}
|
91
ts/common/types.ts
Normal file
91
ts/common/types.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared types for certificate management and domain options
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Domain forwarding configuration
|
||||||
|
*/
|
||||||
|
export interface IForwardConfig {
|
||||||
|
ip: string;
|
||||||
|
port: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Domain configuration options
|
||||||
|
*/
|
||||||
|
export interface IDomainOptions {
|
||||||
|
domainName: string;
|
||||||
|
sslRedirect: boolean; // if true redirects the request to port 443
|
||||||
|
acmeMaintenance: boolean; // tries to always have a valid cert for this domain
|
||||||
|
forward?: IForwardConfig; // forwards all http requests to that target
|
||||||
|
acmeForward?: IForwardConfig; // forwards letsencrypt requests to this config
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Certificate data that can be emitted via events or set from outside
|
||||||
|
*/
|
||||||
|
export interface ICertificateData {
|
||||||
|
domain: string;
|
||||||
|
certificate: string;
|
||||||
|
privateKey: string;
|
||||||
|
expiryDate: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Events emitted by the Port80Handler
|
||||||
|
*/
|
||||||
|
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 failure payload type
|
||||||
|
*/
|
||||||
|
export interface ICertificateFailure {
|
||||||
|
domain: string;
|
||||||
|
error: string;
|
||||||
|
isRenewal: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Certificate expiry payload type
|
||||||
|
*/
|
||||||
|
export interface ICertificateExpiring {
|
||||||
|
domain: string;
|
||||||
|
expiryDate: Date;
|
||||||
|
daysRemaining: number;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Forwarding configuration for specific domains in ACME setup
|
||||||
|
*/
|
||||||
|
export interface IDomainForwardConfig {
|
||||||
|
domain: string;
|
||||||
|
forwardConfig?: IForwardConfig;
|
||||||
|
acmeForwardConfig?: IForwardConfig;
|
||||||
|
sslRedirect?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unified ACME configuration options used across proxies and handlers
|
||||||
|
*/
|
||||||
|
export interface IAcmeOptions {
|
||||||
|
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
|
||||||
|
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
|
||||||
|
domainForwards?: IDomainForwardConfig[]; // Domain-specific forwarding configs
|
||||||
|
}
|
@ -1,7 +1,9 @@
|
|||||||
export * from './nfttablesproxy/classes.nftablesproxy.js';
|
export * from './nfttablesproxy/classes.nftablesproxy.js';
|
||||||
export * from './networkproxy/classes.np.networkproxy.js';
|
export * from './networkproxy/index.js';
|
||||||
export * from './port80handler/classes.port80handler.js';
|
export * from './port80handler/classes.port80handler.js';
|
||||||
export * from './redirect/classes.redirect.js';
|
export * from './redirect/classes.redirect.js';
|
||||||
export * from './smartproxy/classes.smartproxy.js';
|
export * from './smartproxy/classes.smartproxy.js';
|
||||||
export * from './smartproxy/classes.pp.snihandler.js';
|
export * from './smartproxy/classes.pp.snihandler.js';
|
||||||
export * from './smartproxy/classes.pp.interfaces.js';
|
export * from './smartproxy/classes.pp.interfaces.js';
|
||||||
|
|
||||||
|
export * from './common/types.js';
|
||||||
|
@ -3,7 +3,11 @@ import * as fs from 'fs';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { type INetworkProxyOptions, type ICertificateEntry, type ILogger, createLogger } from './classes.np.types.js';
|
import { type INetworkProxyOptions, type ICertificateEntry, type ILogger, createLogger } from './classes.np.types.js';
|
||||||
import { Port80Handler, Port80HandlerEvents, type IDomainOptions } from '../port80handler/classes.port80handler.js';
|
import { Port80Handler } from '../port80handler/classes.port80handler.js';
|
||||||
|
import { Port80HandlerEvents } from '../common/types.js';
|
||||||
|
import { buildPort80Handler } from '../common/acmeFactory.js';
|
||||||
|
import { subscribeToPort80Handler } from '../common/eventUtils.js';
|
||||||
|
import type { IDomainOptions } from '../common/types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages SSL certificates for NetworkProxy including ACME integration
|
* Manages SSL certificates for NetworkProxy including ACME integration
|
||||||
@ -101,12 +105,14 @@ export class CertificateManager {
|
|||||||
this.port80Handler = handler;
|
this.port80Handler = handler;
|
||||||
this.externalPort80Handler = true;
|
this.externalPort80Handler = true;
|
||||||
|
|
||||||
// Register event handlers
|
// Subscribe to Port80Handler events
|
||||||
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_ISSUED, this.handleCertificateIssued.bind(this));
|
subscribeToPort80Handler(this.port80Handler, {
|
||||||
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_RENEWED, this.handleCertificateIssued.bind(this));
|
onCertificateIssued: this.handleCertificateIssued.bind(this),
|
||||||
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_FAILED, this.handleCertificateFailed.bind(this));
|
onCertificateRenewed: this.handleCertificateIssued.bind(this),
|
||||||
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_EXPIRING, (data) => {
|
onCertificateFailed: this.handleCertificateFailed.bind(this),
|
||||||
this.logger.info(`Certificate for ${data.domain} expires in ${data.daysRemaining} days`);
|
onCertificateExpiring: (data) => {
|
||||||
|
this.logger.info(`Certificate for ${data.domain} expires in ${data.daysRemaining} days`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.info('External Port80Handler connected to CertificateManager');
|
this.logger.info('External Port80Handler connected to CertificateManager');
|
||||||
@ -348,8 +354,8 @@ export class CertificateManager {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create certificate manager
|
// Build and configure Port80Handler
|
||||||
this.port80Handler = new Port80Handler({
|
this.port80Handler = buildPort80Handler({
|
||||||
port: this.options.acme.port,
|
port: this.options.acme.port,
|
||||||
contactEmail: this.options.acme.contactEmail,
|
contactEmail: this.options.acme.contactEmail,
|
||||||
useProduction: this.options.acme.useProduction,
|
useProduction: this.options.acme.useProduction,
|
||||||
@ -358,13 +364,14 @@ export class CertificateManager {
|
|||||||
certificateStore: this.options.acme.certificateStore,
|
certificateStore: this.options.acme.certificateStore,
|
||||||
skipConfiguredCerts: this.options.acme.skipConfiguredCerts
|
skipConfiguredCerts: this.options.acme.skipConfiguredCerts
|
||||||
});
|
});
|
||||||
|
// Subscribe to Port80Handler events
|
||||||
// Register event handlers
|
subscribeToPort80Handler(this.port80Handler, {
|
||||||
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_ISSUED, this.handleCertificateIssued.bind(this));
|
onCertificateIssued: this.handleCertificateIssued.bind(this),
|
||||||
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_RENEWED, this.handleCertificateIssued.bind(this));
|
onCertificateRenewed: this.handleCertificateIssued.bind(this),
|
||||||
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_FAILED, this.handleCertificateFailed.bind(this));
|
onCertificateFailed: this.handleCertificateFailed.bind(this),
|
||||||
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_EXPIRING, (data) => {
|
onCertificateExpiring: (data) => {
|
||||||
this.logger.info(`Certificate for ${data.domain} expires in ${data.daysRemaining} days`);
|
this.logger.info(`Certificate for ${data.domain} expires in ${data.daysRemaining} days`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start the handler
|
// Start the handler
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration options for NetworkProxy
|
||||||
|
*/
|
||||||
|
import type { IAcmeOptions } from '../common/types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration options for NetworkProxy
|
* Configuration options for NetworkProxy
|
||||||
*/
|
*/
|
||||||
@ -24,16 +29,7 @@ export interface INetworkProxyOptions {
|
|||||||
backendProtocol?: 'http1' | 'http2';
|
backendProtocol?: 'http1' | 'http2';
|
||||||
|
|
||||||
// ACME certificate management options
|
// ACME certificate management options
|
||||||
acme?: {
|
acme?: IAcmeOptions;
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,5 +1,14 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
import { IncomingMessage, ServerResponse } from 'http';
|
import { IncomingMessage, ServerResponse } from 'http';
|
||||||
|
import { Port80HandlerEvents } from '../common/types.js';
|
||||||
|
import type {
|
||||||
|
IForwardConfig,
|
||||||
|
IDomainOptions,
|
||||||
|
ICertificateData,
|
||||||
|
ICertificateFailure,
|
||||||
|
ICertificateExpiring,
|
||||||
|
IAcmeOptions
|
||||||
|
} from '../common/types.js';
|
||||||
// (fs and path I/O moved to CertProvisioner)
|
// (fs and path I/O moved to CertProvisioner)
|
||||||
// ACME HTTP-01 challenge handler storing tokens in memory (diskless)
|
// ACME HTTP-01 challenge handler storing tokens in memory (diskless)
|
||||||
class DisklessHttp01Handler {
|
class DisklessHttp01Handler {
|
||||||
@ -45,24 +54,6 @@ export class ServerError extends Port80HandlerError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Domain forwarding configuration
|
|
||||||
*/
|
|
||||||
export interface IForwardConfig {
|
|
||||||
ip: string;
|
|
||||||
port: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Domain configuration options
|
|
||||||
*/
|
|
||||||
export interface IDomainOptions {
|
|
||||||
domainName: string;
|
|
||||||
sslRedirect: boolean; // if true redirects the request to port 443
|
|
||||||
acmeMaintenance: boolean; // tries to always have a valid cert for this domain
|
|
||||||
forward?: IForwardConfig; // forwards all http requests to that target
|
|
||||||
acmeForward?: IForwardConfig; // forwards letsencrypt requests to this config
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a domain configuration with certificate status information
|
* Represents a domain configuration with certificate status information
|
||||||
@ -80,55 +71,8 @@ interface IDomainCertificate {
|
|||||||
/**
|
/**
|
||||||
* Configuration options for the Port80Handler
|
* Configuration options for the Port80Handler
|
||||||
*/
|
*/
|
||||||
interface IPort80HandlerOptions {
|
// Port80Handler options moved to common types
|
||||||
port?: number;
|
|
||||||
contactEmail?: string;
|
|
||||||
useProduction?: boolean;
|
|
||||||
httpsRedirectPort?: number;
|
|
||||||
enabled?: boolean; // Whether ACME is enabled at all
|
|
||||||
// (Persistence moved to CertProvisioner)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Certificate data that can be emitted via events or set from outside
|
|
||||||
*/
|
|
||||||
export interface ICertificateData {
|
|
||||||
domain: string;
|
|
||||||
certificate: string;
|
|
||||||
privateKey: string;
|
|
||||||
expiryDate: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Events emitted by the Port80Handler
|
|
||||||
*/
|
|
||||||
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 failure payload type
|
|
||||||
*/
|
|
||||||
export interface ICertificateFailure {
|
|
||||||
domain: string;
|
|
||||||
error: string;
|
|
||||||
isRenewal: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Certificate expiry payload type
|
|
||||||
*/
|
|
||||||
export interface ICertificateExpiring {
|
|
||||||
domain: string;
|
|
||||||
expiryDate: Date;
|
|
||||||
daysRemaining: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Port80Handler with ACME certificate management and request forwarding capabilities
|
* Port80Handler with ACME certificate management and request forwarding capabilities
|
||||||
@ -144,13 +88,13 @@ export class Port80Handler extends plugins.EventEmitter {
|
|||||||
// Renewal scheduling is handled externally by SmartProxy
|
// Renewal scheduling is handled externally by SmartProxy
|
||||||
// (Removed internal renewal timer)
|
// (Removed internal renewal timer)
|
||||||
private isShuttingDown: boolean = false;
|
private isShuttingDown: boolean = false;
|
||||||
private options: Required<IPort80HandlerOptions>;
|
private options: Required<IAcmeOptions>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new Port80Handler
|
* Creates a new Port80Handler
|
||||||
* @param options Configuration options
|
* @param options Configuration options
|
||||||
*/
|
*/
|
||||||
constructor(options: IPort80HandlerOptions = {}) {
|
constructor(options: IAcmeOptions = {}) {
|
||||||
super();
|
super();
|
||||||
this.domainCertificates = new Map<string, IDomainCertificate>();
|
this.domainCertificates = new Map<string, IDomainCertificate>();
|
||||||
|
|
||||||
@ -160,7 +104,13 @@ export class Port80Handler extends plugins.EventEmitter {
|
|||||||
contactEmail: options.contactEmail ?? 'admin@example.com',
|
contactEmail: options.contactEmail ?? 'admin@example.com',
|
||||||
useProduction: options.useProduction ?? false, // Safer default: staging
|
useProduction: options.useProduction ?? false, // Safer default: staging
|
||||||
httpsRedirectPort: options.httpsRedirectPort ?? 443,
|
httpsRedirectPort: options.httpsRedirectPort ?? 443,
|
||||||
enabled: options.enabled ?? true // Enable by default
|
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,
|
||||||
|
domainForwards: options.domainForwards ?? []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -810,7 +760,7 @@ export class Port80Handler extends plugins.EventEmitter {
|
|||||||
* Gets configuration details
|
* Gets configuration details
|
||||||
* @returns Current configuration
|
* @returns Current configuration
|
||||||
*/
|
*/
|
||||||
public getConfig(): Required<IPort80HandlerOptions> {
|
public getConfig(): Required<IAcmeOptions> {
|
||||||
return { ...this.options };
|
return { ...this.options };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
import type { IDomainConfig, ISmartProxyCertProvisionObject } from './classes.pp.interfaces.js';
|
import type { IDomainConfig, ISmartProxyCertProvisionObject } from './classes.pp.interfaces.js';
|
||||||
import { Port80Handler, Port80HandlerEvents, type ICertificateData } from '../port80handler/classes.port80handler.js';
|
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 { NetworkProxyBridge } from './classes.pp.networkproxybridge.js';
|
import type { NetworkProxyBridge } from './classes.pp.networkproxybridge.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -56,11 +59,13 @@ export class CertProvisioner extends plugins.EventEmitter {
|
|||||||
*/
|
*/
|
||||||
public async start(): Promise<void> {
|
public async start(): Promise<void> {
|
||||||
// Subscribe to Port80Handler certificate events
|
// Subscribe to Port80Handler certificate events
|
||||||
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_ISSUED, (data: ICertificateData) => {
|
subscribeToPort80Handler(this.port80Handler, {
|
||||||
this.emit('certificate', { ...data, source: 'http01', isRenewal: false });
|
onCertificateIssued: (data: ICertificateData) => {
|
||||||
});
|
this.emit('certificate', { ...data, source: 'http01', isRenewal: false });
|
||||||
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_RENEWED, (data: ICertificateData) => {
|
},
|
||||||
this.emit('certificate', { ...data, source: 'http01', isRenewal: true });
|
onCertificateRenewed: (data: ICertificateData) => {
|
||||||
|
this.emit('certificate', { ...data, source: 'http01', isRenewal: true });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Apply external forwarding for ACME challenges (e.g. Synology)
|
// Apply external forwarding for ACME challenges (e.g. Synology)
|
||||||
|
@ -21,6 +21,7 @@ export interface IDomainConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Port proxy settings including global allowed port ranges */
|
/** Port proxy settings including global allowed port ranges */
|
||||||
|
import type { IAcmeOptions } from '../common/types.js';
|
||||||
export interface IPortProxySettings {
|
export interface IPortProxySettings {
|
||||||
fromPort: number;
|
fromPort: number;
|
||||||
toPort: number;
|
toPort: number;
|
||||||
@ -83,31 +84,8 @@ export interface IPortProxySettings {
|
|||||||
useNetworkProxy?: number[]; // Array of ports to forward to NetworkProxy
|
useNetworkProxy?: number[]; // Array of ports to forward to NetworkProxy
|
||||||
networkProxyPort?: number; // Port where NetworkProxy is listening (default: 8443)
|
networkProxyPort?: number; // Port where NetworkProxy is listening (default: 8443)
|
||||||
|
|
||||||
// Port80Handler configuration (replaces ACME configuration)
|
// ACME configuration options for SmartProxy
|
||||||
port80HandlerConfig?: {
|
acme?: IAcmeOptions;
|
||||||
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
|
|
||||||
httpsRedirectPort?: number; // Port to redirect HTTP requests to HTTPS (default: 443)
|
|
||||||
renewCheckIntervalHours?: number; // How often to check for renewals (default: 24)
|
|
||||||
// Domain-specific forwarding configurations
|
|
||||||
domainForwards?: Array<{
|
|
||||||
domain: string;
|
|
||||||
forwardConfig?: {
|
|
||||||
ip: string;
|
|
||||||
port: number;
|
|
||||||
};
|
|
||||||
acmeForwardConfig?: {
|
|
||||||
ip: string;
|
|
||||||
port: number;
|
|
||||||
};
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional certificate provider callback. Return 'http01' to use HTTP-01 challenges,
|
* Optional certificate provider callback. Return 'http01' to use HTTP-01 challenges,
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
import { NetworkProxy } from '../networkproxy/classes.np.networkproxy.js';
|
import { NetworkProxy } from '../networkproxy/classes.np.networkproxy.js';
|
||||||
import { Port80Handler, Port80HandlerEvents, type ICertificateData } from '../port80handler/classes.port80handler.js';
|
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, IPortProxySettings, IDomainConfig } from './classes.pp.interfaces.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -18,9 +21,11 @@ export class NetworkProxyBridge {
|
|||||||
public setPort80Handler(handler: Port80Handler): void {
|
public setPort80Handler(handler: Port80Handler): void {
|
||||||
this.port80Handler = handler;
|
this.port80Handler = handler;
|
||||||
|
|
||||||
// Register for certificate events
|
// Subscribe to certificate events
|
||||||
handler.on(Port80HandlerEvents.CERTIFICATE_ISSUED, this.handleCertificateEvent.bind(this));
|
subscribeToPort80Handler(handler, {
|
||||||
handler.on(Port80HandlerEvents.CERTIFICATE_RENEWED, this.handleCertificateEvent.bind(this));
|
onCertificateIssued: this.handleCertificateEvent.bind(this),
|
||||||
|
onCertificateRenewed: this.handleCertificateEvent.bind(this)
|
||||||
|
});
|
||||||
|
|
||||||
// If NetworkProxy is already initialized, connect it with Port80Handler
|
// If NetworkProxy is already initialized, connect it with Port80Handler
|
||||||
if (this.networkProxy) {
|
if (this.networkProxy) {
|
||||||
@ -284,7 +289,7 @@ export class NetworkProxyBridge {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Log ACME-eligible domains
|
// Log ACME-eligible domains
|
||||||
const acmeEnabled = !!this.settings.port80HandlerConfig?.enabled;
|
const acmeEnabled = !!this.settings.acme?.enabled;
|
||||||
if (acmeEnabled) {
|
if (acmeEnabled) {
|
||||||
const acmeEligibleDomains = proxyConfigs
|
const acmeEligibleDomains = proxyConfigs
|
||||||
.filter((config) => !config.hostName.includes('*')) // Exclude wildcards
|
.filter((config) => !config.hostName.includes('*')) // Exclude wildcards
|
||||||
@ -345,7 +350,7 @@ export class NetworkProxyBridge {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.settings.port80HandlerConfig?.enabled) {
|
if (!this.settings.acme?.enabled) {
|
||||||
console.log('Cannot request certificate - ACME is not enabled');
|
console.log('Cannot request certificate - ACME is not enabled');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import * as net from 'net';
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TlsAlert class for managing TLS alert messages
|
* TlsAlert class for managing TLS alert messages
|
||||||
@ -99,7 +99,7 @@ export class TlsAlert {
|
|||||||
* @returns Promise that resolves when the alert has been sent
|
* @returns Promise that resolves when the alert has been sent
|
||||||
*/
|
*/
|
||||||
static async send(
|
static async send(
|
||||||
socket: net.Socket,
|
socket: plugins.net.Socket,
|
||||||
level: number,
|
level: number,
|
||||||
description: number,
|
description: number,
|
||||||
closeAfterSend: boolean = false,
|
closeAfterSend: boolean = false,
|
||||||
@ -183,7 +183,7 @@ export class TlsAlert {
|
|||||||
* @param socket The socket to send the alert to
|
* @param socket The socket to send the alert to
|
||||||
* @returns Promise that resolves when the alert has been sent
|
* @returns Promise that resolves when the alert has been sent
|
||||||
*/
|
*/
|
||||||
static async sendSniRequired(socket: net.Socket): Promise<void> {
|
static async sendSniRequired(socket: plugins.net.Socket): Promise<void> {
|
||||||
return this.send(socket, this.LEVEL_WARNING, this.UNRECOGNIZED_NAME);
|
return this.send(socket, this.LEVEL_WARNING, this.UNRECOGNIZED_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -194,7 +194,7 @@ export class TlsAlert {
|
|||||||
* @param closeDelay Milliseconds to wait before closing the connection (default: 200ms)
|
* @param closeDelay Milliseconds to wait before closing the connection (default: 200ms)
|
||||||
* @returns Promise that resolves when the alert has been sent and the connection closed
|
* @returns Promise that resolves when the alert has been sent and the connection closed
|
||||||
*/
|
*/
|
||||||
static async sendCloseNotify(socket: net.Socket, closeDelay: number = 200): Promise<void> {
|
static async sendCloseNotify(socket: plugins.net.Socket, closeDelay: number = 200): Promise<void> {
|
||||||
return this.send(socket, this.LEVEL_WARNING, this.CLOSE_NOTIFY, true, closeDelay);
|
return this.send(socket, this.LEVEL_WARNING, this.CLOSE_NOTIFY, true, closeDelay);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,7 +208,7 @@ export class TlsAlert {
|
|||||||
* @returns Promise that resolves when the alert has been sent
|
* @returns Promise that resolves when the alert has been sent
|
||||||
*/
|
*/
|
||||||
static async sendCertificateExpired(
|
static async sendCertificateExpired(
|
||||||
socket: net.Socket,
|
socket: plugins.net.Socket,
|
||||||
fatal: boolean = false,
|
fatal: boolean = false,
|
||||||
closeAfterSend: boolean = true,
|
closeAfterSend: boolean = true,
|
||||||
closeDelay: number = 200
|
closeDelay: number = 200
|
||||||
@ -224,7 +224,7 @@ export class TlsAlert {
|
|||||||
* @param socket The socket to send the alerts to
|
* @param socket The socket to send the alerts to
|
||||||
* @returns Promise that resolves when all alerts have been sent
|
* @returns Promise that resolves when all alerts have been sent
|
||||||
*/
|
*/
|
||||||
static async sendForceSniSequence(socket: net.Socket): Promise<void> {
|
static async sendForceSniSequence(socket: plugins.net.Socket): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Send unrecognized_name (warning)
|
// Send unrecognized_name (warning)
|
||||||
socket.cork();
|
socket.cork();
|
||||||
@ -249,7 +249,7 @@ export class TlsAlert {
|
|||||||
* @returns Promise that resolves when the alert has been sent and the connection closed
|
* @returns Promise that resolves when the alert has been sent and the connection closed
|
||||||
*/
|
*/
|
||||||
static async sendFatalAndClose(
|
static async sendFatalAndClose(
|
||||||
socket: net.Socket,
|
socket: plugins.net.Socket,
|
||||||
description: number,
|
description: number,
|
||||||
closeDelay: number = 100
|
closeDelay: number = 100
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
import type { IPortProxySettings, IDomainConfig } from './classes.pp.interfaces.js';
|
|
||||||
import { ConnectionManager } from './classes.pp.connectionmanager.js';
|
import { ConnectionManager } from './classes.pp.connectionmanager.js';
|
||||||
import { SecurityManager } from './classes.pp.securitymanager.js';
|
import { SecurityManager } from './classes.pp.securitymanager.js';
|
||||||
import { DomainConfigManager } from './classes.pp.domainconfigmanager.js';
|
import { DomainConfigManager } from './classes.pp.domainconfigmanager.js';
|
||||||
@ -10,9 +10,11 @@ import { PortRangeManager } from './classes.pp.portrangemanager.js';
|
|||||||
import { ConnectionHandler } from './classes.pp.connectionhandler.js';
|
import { ConnectionHandler } from './classes.pp.connectionhandler.js';
|
||||||
import { Port80Handler } from '../port80handler/classes.port80handler.js';
|
import { Port80Handler } from '../port80handler/classes.port80handler.js';
|
||||||
import { CertProvisioner } from './classes.pp.certprovisioner.js';
|
import { CertProvisioner } from './classes.pp.certprovisioner.js';
|
||||||
import type { ICertificateData } from '../port80handler/classes.port80handler.js';
|
import type { ICertificateData } from '../common/types.js';
|
||||||
import * as path from 'path';
|
import { buildPort80Handler } from '../common/acmeFactory.js';
|
||||||
import * as fs from 'fs';
|
|
||||||
|
import type { IPortProxySettings, IDomainConfig } from './classes.pp.interfaces.js';
|
||||||
|
export type { IPortProxySettings, IDomainConfig };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SmartProxy - Main class that coordinates all components
|
* SmartProxy - Main class that coordinates all components
|
||||||
@ -67,13 +69,13 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6,
|
keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6,
|
||||||
extendedKeepAliveLifetime: settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000,
|
extendedKeepAliveLifetime: settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000,
|
||||||
networkProxyPort: settingsArg.networkProxyPort || 8443,
|
networkProxyPort: settingsArg.networkProxyPort || 8443,
|
||||||
port80HandlerConfig: settingsArg.port80HandlerConfig || {},
|
acme: settingsArg.acme || {},
|
||||||
globalPortRanges: settingsArg.globalPortRanges || [],
|
globalPortRanges: settingsArg.globalPortRanges || [],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set default port80HandlerConfig if not provided
|
// Set default ACME options if not provided
|
||||||
if (!this.settings.port80HandlerConfig || Object.keys(this.settings.port80HandlerConfig).length === 0) {
|
if (!this.settings.acme || Object.keys(this.settings.acme).length === 0) {
|
||||||
this.settings.port80HandlerConfig = {
|
this.settings.acme = {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
port: 80,
|
port: 80,
|
||||||
contactEmail: 'admin@example.com',
|
contactEmail: 'admin@example.com',
|
||||||
@ -83,7 +85,8 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
certificateStore: './certs',
|
certificateStore: './certs',
|
||||||
skipConfiguredCerts: false,
|
skipConfiguredCerts: false,
|
||||||
httpsRedirectPort: this.settings.fromPort,
|
httpsRedirectPort: this.settings.fromPort,
|
||||||
renewCheckIntervalHours: 24
|
renewCheckIntervalHours: 24,
|
||||||
|
domainForwards: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,40 +125,20 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
* Initialize the Port80Handler for ACME certificate management
|
* Initialize the Port80Handler for ACME certificate management
|
||||||
*/
|
*/
|
||||||
private async initializePort80Handler(): Promise<void> {
|
private async initializePort80Handler(): Promise<void> {
|
||||||
const config = this.settings.port80HandlerConfig;
|
const config = this.settings.acme!;
|
||||||
|
if (!config.enabled) {
|
||||||
if (!config || !config.enabled) {
|
console.log('ACME is disabled in configuration');
|
||||||
console.log('Port80Handler is disabled in configuration');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Ensure the certificate store directory exists
|
// Build and start the Port80Handler
|
||||||
if (config.certificateStore) {
|
this.port80Handler = buildPort80Handler({
|
||||||
const certStorePath = path.resolve(config.certificateStore);
|
...config,
|
||||||
if (!fs.existsSync(certStorePath)) {
|
httpsRedirectPort: config.httpsRedirectPort || this.settings.fromPort
|
||||||
fs.mkdirSync(certStorePath, { recursive: true });
|
|
||||||
console.log(`Created certificate store directory: ${certStorePath}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create Port80Handler with options from config
|
|
||||||
this.port80Handler = new Port80Handler({
|
|
||||||
port: config.port,
|
|
||||||
contactEmail: config.contactEmail,
|
|
||||||
useProduction: config.useProduction,
|
|
||||||
httpsRedirectPort: config.httpsRedirectPort || this.settings.fromPort,
|
|
||||||
enabled: config.enabled,
|
|
||||||
certificateStore: config.certificateStore,
|
|
||||||
skipConfiguredCerts: config.skipConfiguredCerts
|
|
||||||
});
|
});
|
||||||
|
// Share Port80Handler with NetworkProxyBridge before start
|
||||||
|
|
||||||
|
|
||||||
// Share Port80Handler with NetworkProxyBridge
|
|
||||||
this.networkProxyBridge.setPort80Handler(this.port80Handler);
|
this.networkProxyBridge.setPort80Handler(this.port80Handler);
|
||||||
|
|
||||||
// Start Port80Handler
|
|
||||||
await this.port80Handler.start();
|
await this.port80Handler.start();
|
||||||
console.log(`Port80Handler started on port ${config.port}`);
|
console.log(`Port80Handler started on port ${config.port}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -177,20 +160,20 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
await this.initializePort80Handler();
|
await this.initializePort80Handler();
|
||||||
// Initialize CertProvisioner for unified certificate workflows
|
// Initialize CertProvisioner for unified certificate workflows
|
||||||
if (this.port80Handler) {
|
if (this.port80Handler) {
|
||||||
|
const acme = this.settings.acme!;
|
||||||
this.certProvisioner = new CertProvisioner(
|
this.certProvisioner = new CertProvisioner(
|
||||||
this.settings.domainConfigs,
|
this.settings.domainConfigs,
|
||||||
this.port80Handler,
|
this.port80Handler,
|
||||||
this.networkProxyBridge,
|
this.networkProxyBridge,
|
||||||
this.settings.certProvider,
|
this.settings.certProvider,
|
||||||
this.settings.port80HandlerConfig?.renewThresholdDays || 30,
|
acme.renewThresholdDays!,
|
||||||
this.settings.port80HandlerConfig?.renewCheckIntervalHours || 24,
|
acme.renewCheckIntervalHours!,
|
||||||
this.settings.port80HandlerConfig?.autoRenew !== false,
|
acme.autoRenew!,
|
||||||
// External ACME forwarding for specific domains
|
acme.domainForwards?.map(f => ({
|
||||||
this.settings.port80HandlerConfig?.domainForwards?.map(f => ({
|
|
||||||
domain: f.domain,
|
domain: f.domain,
|
||||||
forwardConfig: f.forwardConfig,
|
forwardConfig: f.forwardConfig,
|
||||||
acmeForwardConfig: f.acmeForwardConfig,
|
acmeForwardConfig: f.acmeForwardConfig,
|
||||||
sslRedirect: false
|
sslRedirect: f.sslRedirect || false
|
||||||
})) || []
|
})) || []
|
||||||
);
|
);
|
||||||
this.certProvisioner.on('certificate', (certData) => {
|
this.certProvisioner.on('certificate', (certData) => {
|
||||||
@ -405,7 +388,7 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If Port80Handler is running, provision certificates per new domain
|
// If Port80Handler is running, provision certificates per new domain
|
||||||
if (this.port80Handler && this.settings.port80HandlerConfig?.enabled) {
|
if (this.port80Handler && this.settings.acme?.enabled) {
|
||||||
for (const domainConfig of newDomainConfigs) {
|
for (const domainConfig of newDomainConfigs) {
|
||||||
for (const domain of domainConfig.domains) {
|
for (const domain of domainConfig.domains) {
|
||||||
if (domain.includes('*')) continue;
|
if (domain.includes('*')) continue;
|
||||||
@ -441,94 +424,7 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the Port80Handler configuration
|
|
||||||
*/
|
|
||||||
public async updatePort80HandlerConfig(config: IPortProxySettings['port80HandlerConfig']): Promise<void> {
|
|
||||||
if (!config) return;
|
|
||||||
|
|
||||||
console.log('Updating Port80Handler configuration');
|
|
||||||
|
|
||||||
// Update the settings
|
|
||||||
this.settings.port80HandlerConfig = {
|
|
||||||
...this.settings.port80HandlerConfig,
|
|
||||||
...config
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if we need to restart Port80Handler
|
|
||||||
let needsRestart = false;
|
|
||||||
|
|
||||||
// Restart if enabled state changed
|
|
||||||
if (this.port80Handler && config.enabled === false) {
|
|
||||||
needsRestart = true;
|
|
||||||
} else if (!this.port80Handler && config.enabled === true) {
|
|
||||||
needsRestart = true;
|
|
||||||
} else if (this.port80Handler && (
|
|
||||||
config.port !== undefined ||
|
|
||||||
config.contactEmail !== undefined ||
|
|
||||||
config.useProduction !== undefined ||
|
|
||||||
config.renewThresholdDays !== undefined ||
|
|
||||||
config.renewCheckIntervalHours !== undefined
|
|
||||||
)) {
|
|
||||||
// Restart if critical settings changed
|
|
||||||
needsRestart = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (needsRestart) {
|
|
||||||
// Stop if running
|
|
||||||
if (this.port80Handler) {
|
|
||||||
try {
|
|
||||||
await this.port80Handler.stop();
|
|
||||||
this.port80Handler = null;
|
|
||||||
console.log('Stopped Port80Handler for configuration update');
|
|
||||||
} catch (err) {
|
|
||||||
console.log(`Error stopping Port80Handler: ${err}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start with new config if enabled
|
|
||||||
if (this.settings.port80HandlerConfig.enabled) {
|
|
||||||
await this.initializePort80Handler();
|
|
||||||
console.log('Restarted Port80Handler with new configuration');
|
|
||||||
}
|
|
||||||
} else if (this.port80Handler) {
|
|
||||||
// Just update domain forwards if they changed
|
|
||||||
if (config.domainForwards) {
|
|
||||||
for (const forward of config.domainForwards) {
|
|
||||||
this.port80Handler.addDomain({
|
|
||||||
domainName: forward.domain,
|
|
||||||
sslRedirect: true,
|
|
||||||
acmeMaintenance: true,
|
|
||||||
forward: forward.forwardConfig,
|
|
||||||
acmeForward: forward.acmeForwardConfig
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log('Updated domain forwards in Port80Handler');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Perform scheduled renewals for managed domains
|
|
||||||
*/
|
|
||||||
private async performRenewals(): Promise<void> {
|
|
||||||
if (!this.port80Handler) return;
|
|
||||||
const statuses = this.port80Handler.getDomainCertificateStatus();
|
|
||||||
const threshold = this.settings.port80HandlerConfig.renewThresholdDays ?? 30;
|
|
||||||
const now = new Date();
|
|
||||||
for (const [domain, status] of statuses.entries()) {
|
|
||||||
if (!status.certObtained || status.obtainingInProgress || !status.expiryDate) continue;
|
|
||||||
const msRemaining = status.expiryDate.getTime() - now.getTime();
|
|
||||||
const daysRemaining = Math.ceil(msRemaining / (24 * 60 * 60 * 1000));
|
|
||||||
if (daysRemaining <= threshold) {
|
|
||||||
try {
|
|
||||||
await this.port80Handler.renewCertificate(domain);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`Error renewing certificate for ${domain}:`, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* Request a certificate for a specific domain
|
* Request a certificate for a specific domain
|
||||||
*/
|
*/
|
||||||
@ -621,7 +517,7 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
networkProxyConnections,
|
networkProxyConnections,
|
||||||
terminationStats,
|
terminationStats,
|
||||||
acmeEnabled: !!this.port80Handler,
|
acmeEnabled: !!this.port80Handler,
|
||||||
port80HandlerPort: this.port80Handler ? this.settings.port80HandlerConfig?.port : null
|
port80HandlerPort: this.port80Handler ? this.settings.acme?.port : null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -672,7 +568,7 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
status: 'valid',
|
status: 'valid',
|
||||||
expiryDate: expiryDate.toISOString(),
|
expiryDate: expiryDate.toISOString(),
|
||||||
daysRemaining,
|
daysRemaining,
|
||||||
renewalNeeded: daysRemaining <= this.settings.port80HandlerConfig.renewThresholdDays
|
renewalNeeded: daysRemaining <= (this.settings.acme?.renewThresholdDays ?? 0)
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
certificateStatus[domain] = {
|
certificateStatus[domain] = {
|
||||||
@ -682,11 +578,12 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const acme = this.settings.acme!;
|
||||||
return {
|
return {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
port: this.settings.port80HandlerConfig.port,
|
port: acme.port!,
|
||||||
useProduction: this.settings.port80HandlerConfig.useProduction,
|
useProduction: acme.useProduction!,
|
||||||
autoRenew: this.settings.port80HandlerConfig.autoRenew,
|
autoRenew: acme.autoRenew!,
|
||||||
certificates: certificateStatus
|
certificates: certificateStatus
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user