Compare commits
27 Commits
Author | SHA1 | Date | |
---|---|---|---|
479f5160da | |||
0f356c9bbf | |||
036d522048 | |||
9c05f71cd6 | |||
a9963f3b8a | |||
05c9156458 | |||
47e3c86487 | |||
1387928938 | |||
19578b061e | |||
e8a539829a | |||
a646f4ad28 | |||
aa70dcc299 | |||
adb85d920f | |||
2e4c6312cd | |||
9b773608c7 | |||
3502807023 | |||
c6dff8b78d | |||
12b18373db | |||
30c25ec70c | |||
434834fc06 | |||
e7243243d0 | |||
cce2aed892 | |||
8cd693c063 | |||
09ad7644f4 | |||
f72f884eda | |||
73f3dfcad4 | |||
8291f1f33a |
99
changelog.md
99
changelog.md
@ -1,5 +1,104 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-05-05 - 10.2.0 - feat(CertificateManager)
|
||||
Implement on-demand certificate retrieval for missing SNI certificates. When no certificate is found for a TLS ClientHello, the system now automatically registers the domain with the Port80Handler to trigger ACME issuance and immediately falls back to using the default certificate to complete the handshake. Additionally, HTTP requests on port 80 for unrecognized domains now return a 503 indicating that certificate issuance is in progress.
|
||||
|
||||
- In CertificateManager.handleSNI, if no certificate is cached, call port80Handler.addDomain to trigger on-demand provisioning.
|
||||
- Update Port80Handler.handleRequest to register unknown domains and return a 503 for ACME HTTP-01 challenge requests.
|
||||
- Emit observability events (e.g. certificateRequested) so dynamic certificate requests can be tracked.
|
||||
- Fallback to default SSL context to allow TLS handshake while certificate issuance is performed.
|
||||
- Update and extend unit and integration tests to verify the new on-demand certificate flow.
|
||||
|
||||
## 2025-05-05 - 10.1.0 - feat(smartproxy)
|
||||
Implement fallback to NetworkProxy on missing SNI and rename certProvider to certProvisionFunction in CertProvisioner
|
||||
|
||||
- When a TLS ClientHello is received without an SNI extension and allowSessionTicket is false, the code now attempts to forward the connection to NetworkProxy instead of immediately closing the connection with a TLS alert.
|
||||
- An error callback has been added to handle proxy forwarding failures; if forwarding fails or no NetworkProxy is available, the TLS unrecognized_name alert is sent and the connection is terminated.
|
||||
- Renamed all instances of 'certProvider' to 'certProvisionFunction' in the CertProvisioner implementation, updating the associated types and call sites.
|
||||
- Updated unit tests to simulate a ClientHello without SNI and to verify that with NetworkProxy enabled the connection is correctly forwarded.
|
||||
|
||||
## 2025-05-05 - 10.0.12 - fix(port80handler)
|
||||
refactor ACME challenge handling to use dedicated Http01MemoryHandler, remove obsolete readme.plan.md, and update version to 10.0.12
|
||||
|
||||
- Removed readme.plan.md planning document
|
||||
- Eliminated internal acmeHttp01Storage from Port80Handler
|
||||
- Instantiated and integrated Http01MemoryHandler as a class property for managing HTTP-01 challenges
|
||||
- Delegated ACME HTTP-01 challenge responses to smartAcmeHttp01Handler
|
||||
- Updated ts/00_commitinfo_data.ts version from 10.0.11 to 10.0.12
|
||||
- Adjusted certificate provisioning logic to properly handle wildcard domains and on-demand requests
|
||||
|
||||
## 2025-05-05 - 10.0.12 - fix(port80handler)
|
||||
Remove obsolete readme.plan.md and refactor Port80Handler's ACME challenge handling to use a dedicated Http01MemoryHandler
|
||||
|
||||
- Deleted readme.plan.md planning document which was no longer needed
|
||||
- Removed internal acmeHttp01Storage map from Port80Handler
|
||||
- Instantiated Http01MemoryHandler as a class property and provided it to SmartAcme for challenge handling
|
||||
- Delegated ACME HTTP-01 challenge responses to the new smartAcmeHttp01Handler instead of in-memory storage
|
||||
|
||||
## 2025-05-05 - 10.0.11 - fix(dependencies)
|
||||
Bump @push.rocks/smartacme to ^7.2.5 and @tsclass/tsclass to ^9.2.0; update MemoryCertManager import to use plugins.smartacme.certmanagers.MemoryCertManager()
|
||||
|
||||
- Updated @push.rocks/smartacme from ^7.2.4 to ^7.2.5
|
||||
- Updated @tsclass/tsclass from ^9.1.0 to ^9.2.0
|
||||
- Refactored MemoryCertManager instantiation to use the new import path
|
||||
|
||||
## 2025-05-05 - 10.0.10 - fix(docs)
|
||||
Update README: rename certProviderFunction to certProvisionFunction in configuration options for consistency.
|
||||
|
||||
- Replaced 'certProviderFunction' with 'certProvisionFunction' in the docs to reflect the updated API.
|
||||
- Ensured all references in the readme are consistent with the new naming convention.
|
||||
|
||||
## 2025-05-05 - 10.0.9 - fix(documentation)
|
||||
Update documentation to use 'certProviderFunction' instead of 'certProvider' in SmartProxy settings.
|
||||
|
||||
- Renamed 'certProvider' to 'certProviderFunction' in README examples and configuration options.
|
||||
- Ensured consistency in the configuration section of the documentation.
|
||||
|
||||
## 2025-05-05 - 10.0.8 - fix(smartproxy)
|
||||
rename certProvider to certProvisionFunction in certificate provisioning interfaces and SmartProxy
|
||||
|
||||
- In ts/smartproxy/classes.pp.interfaces.ts, renamed the optional property 'certProvider' to 'certProvisionFunction'.
|
||||
- In ts/smartproxy/classes.smartproxy.ts, updated references from this.settings.certProvider to this.settings.certProvisionFunction.
|
||||
|
||||
## 2025-05-04 - 10.0.7 - fix(core)
|
||||
refactor: Rename IPortProxySettings to ISmartProxyOptions in internal modules
|
||||
|
||||
- Replaced IPortProxySettings with ISmartProxyOptions in connection handler, connection manager, domain config manager, security manager, timeout manager, TLS manager, and network proxy bridge.
|
||||
- Updated type imports and constructors accordingly while preserving backward compatibility via export alias.
|
||||
|
||||
## 2025-05-04 - 10.0.6 - fix(smartproxy)
|
||||
No changes detected in project files. This commit updates commit info without modifying any functionality.
|
||||
|
||||
|
||||
## 2025-05-04 - 10.0.5 - fix(exports/types)
|
||||
Refactor exports and remove duplicate IReverseProxyConfig interface
|
||||
|
||||
- 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
|
||||
|
||||
|
12
package.json
12
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@push.rocks/smartproxy",
|
||||
"version": "10.0.1",
|
||||
"version": "10.2.0",
|
||||
"private": false,
|
||||
"description": "A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.",
|
||||
"main": "dist_ts/index.js",
|
||||
@ -24,19 +24,19 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@push.rocks/lik": "^6.2.2",
|
||||
"@push.rocks/smartacme": "^7.2.3",
|
||||
"@push.rocks/smartacme": "^7.3.2",
|
||||
"@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/smartrequest": "^2.1.0",
|
||||
"@push.rocks/smartstring": "^4.0.15",
|
||||
"@push.rocks/taskbuffer": "^3.1.7",
|
||||
"@tsclass/tsclass": "^9.1.0",
|
||||
"@tsclass/tsclass": "^9.2.0",
|
||||
"@types/minimatch": "^5.1.2",
|
||||
"@types/ws": "^8.18.1",
|
||||
"minimatch": "^10.0.1",
|
||||
"pretty-ms": "^9.2.0",
|
||||
"ws": "^8.18.1"
|
||||
"ws": "^8.18.2"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
@ -86,5 +86,5 @@
|
||||
"puppeteer"
|
||||
]
|
||||
},
|
||||
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6"
|
||||
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
||||
}
|
||||
|
59
pnpm-lock.yaml
generated
59
pnpm-lock.yaml
generated
@ -12,14 +12,14 @@ importers:
|
||||
specifier: ^6.2.2
|
||||
version: 6.2.2
|
||||
'@push.rocks/smartacme':
|
||||
specifier: ^7.2.3
|
||||
version: 7.2.3(@aws-sdk/credential-providers@3.798.0)(socks@2.8.4)
|
||||
specifier: ^7.3.2
|
||||
version: 7.3.2(@aws-sdk/credential-providers@3.798.0)(socks@2.8.4)
|
||||
'@push.rocks/smartdelay':
|
||||
specifier: ^3.0.5
|
||||
version: 3.0.5
|
||||
'@push.rocks/smartnetwork':
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1
|
||||
'@push.rocks/smartpromise':
|
||||
specifier: ^4.2.3
|
||||
version: 4.2.3
|
||||
@ -33,8 +33,8 @@ importers:
|
||||
specifier: ^3.1.7
|
||||
version: 3.1.7
|
||||
'@tsclass/tsclass':
|
||||
specifier: ^9.1.0
|
||||
version: 9.1.0
|
||||
specifier: ^9.2.0
|
||||
version: 9.2.0
|
||||
'@types/minimatch':
|
||||
specifier: ^5.1.2
|
||||
version: 5.1.2
|
||||
@ -48,8 +48,8 @@ importers:
|
||||
specifier: ^9.2.0
|
||||
version: 9.2.0
|
||||
ws:
|
||||
specifier: ^8.18.1
|
||||
version: 8.18.1
|
||||
specifier: ^8.18.2
|
||||
version: 8.18.2
|
||||
devDependencies:
|
||||
'@git.zone/tsbuild':
|
||||
specifier: ^2.3.2
|
||||
@ -355,8 +355,8 @@ packages:
|
||||
'@cloudflare/workers-types@4.20250303.0':
|
||||
resolution: {integrity: sha512-O7F7nRT4bbmwHf3gkRBLfJ7R6vHIJ/oZzWdby6obOiw2yavUfp/AIwS7aO2POu5Cv8+h3TXS3oHs3kKCZLraUA==}
|
||||
|
||||
'@cloudflare/workers-types@4.20250430.0':
|
||||
resolution: {integrity: sha512-JWAX7ZhQ7KjkdJwASgG58MZ/pQ15brlnZ9/0YBwDQ0hrJ/LaK392aTRFlj2r/PRKDZ5dOuujRywNYaNpfeFiEA==}
|
||||
'@cloudflare/workers-types@4.20250505.0':
|
||||
resolution: {integrity: sha512-pLQ/UaCupEy3fTTfy7yCR7FuAbawvCohYAdadGHPUfzssksA9MhkqBLlzYWRwIoC34R8grVn4XOCknEg+NMr0Q==}
|
||||
|
||||
'@colors/colors@1.6.0':
|
||||
resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==}
|
||||
@ -872,8 +872,8 @@ packages:
|
||||
'@push.rocks/qenv@6.1.0':
|
||||
resolution: {integrity: sha512-1FUFMlSVwFSFg8LbqfkzJ2LLP4lMGApUtgOpsvrde6+AxBmB4gjoNgCUH7z3xXfDAtYqcrtSELXBNE0xVL1MqQ==}
|
||||
|
||||
'@push.rocks/smartacme@7.2.3':
|
||||
resolution: {integrity: sha512-PTwn/Zf7l+IMWqeiQ8mTxi7fdrtObQH13YzF65si/VxXTqHeZ7zvisLLKZcMEgSaOj1aQ/Ku83gaO8YqO4gDig==}
|
||||
'@push.rocks/smartacme@7.3.2':
|
||||
resolution: {integrity: sha512-pfNd31wqvEn/2Bi9qZGCzvpV6/5V1jB9xOuWlsUTp4RihDVwQq2/se69pUeXDd1smWOM1yF4zq+45VO5DMDsCg==}
|
||||
|
||||
'@push.rocks/smartarchive@3.0.8':
|
||||
resolution: {integrity: sha512-1jPmR0b7hXmjYQoRiTlRXrIbZcdcFmSdGOfznufjcDpGPe86Km0d8TBnzqghTx4dTihzKC67IxAaz/DM3lvxpA==}
|
||||
@ -974,8 +974,8 @@ packages:
|
||||
'@push.rocks/smartnetwork@3.0.2':
|
||||
resolution: {integrity: sha512-s6CNGzQ1n/d/6cOKXbxeW6/tO//dr1woLqI01g7XhqTriw0nsm2G2kWaZh2J0VOguGNWBgQVCIpR0LjdRNWb3g==}
|
||||
|
||||
'@push.rocks/smartnetwork@4.0.0':
|
||||
resolution: {integrity: sha512-hLE1JNrBjlWtibgFz7t2aMfP15VOfPFyKMpo6FI0JdhmJfD3V5w/nFpSdD6WdXeXUBjCVTJ3C6SrRl8izoG55g==}
|
||||
'@push.rocks/smartnetwork@4.0.1':
|
||||
resolution: {integrity: sha512-zLH88bKY6/cK6vVnCW4Fsugu4T+l6OerWWappit+BecdnQ6vrgShXSAa13JIkkWkWcs4dxEirlEfycQEEQw8BQ==}
|
||||
|
||||
'@push.rocks/smartnpm@2.0.4':
|
||||
resolution: {integrity: sha512-ljRPqnUsXzL5qnuAEt5POy0NnfKs7eYPuuJPJjYiK9VUdP/CyF4h14qTB4H816vNEuF7VU/ASRtz0qDlXmrztg==}
|
||||
@ -1567,8 +1567,8 @@ packages:
|
||||
'@tsclass/tsclass@8.2.1':
|
||||
resolution: {integrity: sha512-bRDCfJTipsTcK6eEokWdsOR1mGCQFeM7zTg6PRHzbxTWQcWQD9AhEr2q3CrPcmAbvIS7fvkO6/pU/mPm1MZxhQ==}
|
||||
|
||||
'@tsclass/tsclass@9.1.0':
|
||||
resolution: {integrity: sha512-PkG1bXK/bqVtxaRHje+iJHjtcdRHLHrNTOkzqh+jv2A7mgiyNo2YBJIl4eEJLkw1X3FwEFU4vCAtsegSmJgRug==}
|
||||
'@tsclass/tsclass@9.2.0':
|
||||
resolution: {integrity: sha512-A6ULEkQfYgOnCKQVQRt26O7PRzFo4PE2EoD25RAtnuFuVrNwGynYC20Vee2c8KAOyI7nQ/LaREki9KAX4AHOHQ==}
|
||||
|
||||
'@types/accepts@1.3.7':
|
||||
resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==}
|
||||
@ -4622,8 +4622,8 @@ packages:
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
|
||||
ws@8.18.1:
|
||||
resolution: {integrity: sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==}
|
||||
ws@8.18.2:
|
||||
resolution: {integrity: sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
@ -4760,7 +4760,7 @@ snapshots:
|
||||
'@api.global/typedrequest': 3.1.10
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
'@api.global/typedsocket': 3.0.1
|
||||
'@cloudflare/workers-types': 4.20250430.0
|
||||
'@cloudflare/workers-types': 4.20250505.0
|
||||
'@design.estate/dees-comms': 1.0.27
|
||||
'@push.rocks/lik': 6.2.2
|
||||
'@push.rocks/smartchok': 1.0.34
|
||||
@ -4827,7 +4827,7 @@ snapshots:
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartrequest': 2.1.0
|
||||
'@push.rocks/smartstring': 4.0.15
|
||||
'@tsclass/tsclass': 9.1.0
|
||||
'@tsclass/tsclass': 9.2.0
|
||||
cloudflare: 4.2.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
@ -5671,7 +5671,7 @@ snapshots:
|
||||
|
||||
'@cloudflare/workers-types@4.20250303.0': {}
|
||||
|
||||
'@cloudflare/workers-types@4.20250430.0': {}
|
||||
'@cloudflare/workers-types@4.20250505.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)
|
||||
'@types/ws': 8.18.1
|
||||
figures: 6.1.0
|
||||
ws: 8.18.1
|
||||
ws: 8.18.2
|
||||
transitivePeerDependencies:
|
||||
- '@aws-sdk/credential-providers'
|
||||
- '@mongodb-js/zstd'
|
||||
@ -6284,7 +6284,7 @@ snapshots:
|
||||
'@push.rocks/smartlog': 3.0.7
|
||||
'@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.3.2(@aws-sdk/credential-providers@3.798.0)(socks@2.8.4)':
|
||||
dependencies:
|
||||
'@api.global/typedserver': 3.0.74
|
||||
'@apiclient.xyz/cloudflare': 6.4.1
|
||||
@ -6292,18 +6292,21 @@ snapshots:
|
||||
'@push.rocks/smartdata': 5.15.1(@aws-sdk/credential-providers@3.798.0)(socks@2.8.4)
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
'@push.rocks/smartdns': 6.2.2
|
||||
'@push.rocks/smartfile': 11.2.0
|
||||
'@push.rocks/smartlog': 3.0.7
|
||||
'@push.rocks/smartnetwork': 4.0.1
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartrequest': 2.1.0
|
||||
'@push.rocks/smartstring': 4.0.15
|
||||
'@push.rocks/smarttime': 4.1.1
|
||||
'@push.rocks/smartunique': 3.0.9
|
||||
'@tsclass/tsclass': 9.1.0
|
||||
'@tsclass/tsclass': 9.2.0
|
||||
acme-client: 5.4.0
|
||||
transitivePeerDependencies:
|
||||
- '@aws-sdk/credential-providers'
|
||||
- '@mongodb-js/zstd'
|
||||
- '@nuxt/kit'
|
||||
- aws-crt
|
||||
- bufferutil
|
||||
- encoding
|
||||
- gcp-metadata
|
||||
@ -6605,7 +6608,7 @@ snapshots:
|
||||
public-ip: 6.0.2
|
||||
systeminformation: 5.25.11
|
||||
|
||||
'@push.rocks/smartnetwork@4.0.0':
|
||||
'@push.rocks/smartnetwork@4.0.1':
|
||||
dependencies:
|
||||
'@push.rocks/smartping': 1.0.8
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
@ -7735,7 +7738,7 @@ snapshots:
|
||||
dependencies:
|
||||
type-fest: 4.40.1
|
||||
|
||||
'@tsclass/tsclass@9.1.0':
|
||||
'@tsclass/tsclass@9.2.0':
|
||||
dependencies:
|
||||
type-fest: 4.40.1
|
||||
|
||||
@ -10544,7 +10547,7 @@ snapshots:
|
||||
debug: 4.4.0
|
||||
devtools-protocol: 0.0.1413902
|
||||
typed-query-selector: 2.12.0
|
||||
ws: 8.18.1
|
||||
ws: 8.18.2
|
||||
transitivePeerDependencies:
|
||||
- bare-buffer
|
||||
- bufferutil
|
||||
@ -11303,7 +11306,7 @@ snapshots:
|
||||
|
||||
ws@8.17.1: {}
|
||||
|
||||
ws@8.18.1: {}
|
||||
ws@8.18.2: {}
|
||||
|
||||
xml-js@1.6.11:
|
||||
dependencies:
|
||||
|
@ -384,7 +384,7 @@ Listen for certificate events via EventEmitter:
|
||||
- **SmartProxy**:
|
||||
- `certificate` (domain, publicKey, privateKey, expiryDate, source, isRenewal)
|
||||
|
||||
Provide a `certProvider(domain)` in SmartProxy settings to supply static certs or return `'http01'`.
|
||||
Provide a `certProvisionFunction(domain)` in SmartProxy settings to supply static certs or return `'http01'`.
|
||||
|
||||
## Configuration Options
|
||||
|
||||
@ -429,7 +429,7 @@ Provide a `certProvider(domain)` in SmartProxy settings to supply static certs o
|
||||
- `sniEnabled`, `defaultAllowedIPs`, `preserveSourceIP` (booleans)
|
||||
- Timeouts: `initialDataTimeout`, `socketTimeout`, `inactivityTimeout`, etc.
|
||||
- Socket opts: `noDelay`, `keepAlive`, `enableKeepAliveProbes`
|
||||
- `acme` (IAcmeOptions), `certProvider` (callback)
|
||||
- `acme` (IAcmeOptions), `certProvisionFunction` (callback)
|
||||
- `useNetworkProxy` (number[]), `networkProxyPort` (number)
|
||||
|
||||
## Troubleshooting
|
||||
|
@ -1,29 +1,42 @@
|
||||
# Project Simplification Plan
|
||||
# Plan: On-Demand Certificate Retrieval in NetworkProxy
|
||||
|
||||
This document outlines a roadmap to simplify and refactor the SmartProxy & NetworkProxy codebase for better maintainability, reduced duplication, and clearer configuration.
|
||||
When a TLS connection arrives with an SNI for a domain that has no certificate yet, we want to automatically kick off certificate issuance (ACME HTTP-01 or DNS-01) so the domain is provisioned on the fly without prior manual configuration.
|
||||
|
||||
## Goals
|
||||
- Eliminate duplicate code and shared types
|
||||
- Unify certificate management flow across components
|
||||
- Simplify configuration schemas and option handling
|
||||
- Centralize plugin imports and module interfaces
|
||||
- Strengthen type safety and linting
|
||||
- Improve test coverage and CI integration
|
||||
- Automatically initiate certificate issuance upon first TLS handshake for an unprovisioned domain.
|
||||
- Use Port80Handler (HTTP-01) or custom `certProvisionFunction` (e.g., DNS-01) to retrieve the certificate.
|
||||
- Continue the TLS handshake immediately using the default certificate, then swap to the new certificate on subsequent connections.
|
||||
- For HTTP traffic on port 80, register the domain for ACME and return a 503 until the challenge is complete.
|
||||
|
||||
## Plan
|
||||
- [x] Extract all shared interfaces and types (e.g., certificate, proxy, domain configs) into a common `ts/common` module
|
||||
- [x] Consolidate ACME/Port80Handler logic:
|
||||
- [x] Merge standalone Port80Handler into a single certificate service
|
||||
- [x] Remove duplicate ACME setup in SmartProxy and NetworkProxy
|
||||
- [x] Unify configuration options:
|
||||
- [x] Merge `INetworkProxyOptions.acme`, `IPort80HandlerOptions`, and `port80HandlerConfig` into one schema
|
||||
- [x] Deprecate old option names and provide clear upgrade path
|
||||
- [ ] 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
|
||||
1. Detect missing certificate in SNI callback:
|
||||
- In `ts/networkproxy/classes.np.networkproxy.ts` (or within `CertificateManager.handleSNI`), after looking up `certificateCache`, if no cert is found:
|
||||
- Call `port80Handler.addDomain({ domainName, sslRedirect: false, acmeMaintenance: true })` to trigger dynamic provisioning.
|
||||
- Emit a `certificateRequested` event for observability.
|
||||
- Immediately call `cb(null, defaultSecureContext)` so the handshake uses the default cert.
|
||||
|
||||
Once these steps are complete, the project will be cleaner, easier to understand, and simpler to extend.
|
||||
2. HTTP-01 fallback on port 80:
|
||||
- In `ts/port80handler/classes.port80handler.ts``, in `handleRequest()`, when a request arrives for a new domain not in `domainCertificates`:
|
||||
- Call `addDomain({ domainName, sslRedirect: false, acmeMaintenance: true })`.
|
||||
- Return HTTP 503 with a message like “Certificate issuance in progress.”
|
||||
|
||||
3. CertProvisioner & events:
|
||||
- Ensure `CertProvisioner` is subscribed to `Port80Handler` for newly added domains.
|
||||
- After certificate issuance completes, `Port80Handler` emits `CERTIFICATE_ISSUED`, `CertificateManager` caches and writes disk, and future SNI callbacks will serve the new cert.
|
||||
|
||||
4. Metrics and cleanup:
|
||||
- Track dynamic requests count via a `certificateRequested` event or metric.
|
||||
- Handle error paths: if ACME/DNS fails, emit `CERTIFICATE_FAILED` and continue serving default cert.
|
||||
|
||||
5. Tests:
|
||||
- Simulate a TLS ClientHello for an unconfigured domain:
|
||||
• Verify `port80Handler.addDomain` is called and `certificateRequested` event emitted.
|
||||
• Confirm handshake completes with default cert context.
|
||||
- Simulate HTTP-01 challenge flow for a new domain:
|
||||
• Verify on first HTTP request, `addDomain` is invoked and 503 returned.
|
||||
• After manually injecting a challenge in `Http01MemoryHandler`, verify 200 with key authorization.
|
||||
- Simulate successful ACME response and ensure SNI now returns the real cert.
|
||||
|
||||
6. Final validation:
|
||||
- Run `pnpm test` to ensure all existing tests pass.
|
||||
- Add new unit/integration tests for the dynamic provisioning flow.
|
@ -2,7 +2,7 @@ import { tap, expect } from '@push.rocks/tapbundle';
|
||||
import * as plugins from '../ts/plugins.js';
|
||||
import { CertProvisioner } from '../ts/smartproxy/classes.pp.certprovisioner.js';
|
||||
import type { IDomainConfig, ISmartProxyCertProvisionObject } from '../ts/smartproxy/classes.pp.interfaces.js';
|
||||
import type { ICertificateData } from '../ts/port80handler/classes.port80handler.js';
|
||||
import type { ICertificateData } from '../ts/common/types.js';
|
||||
|
||||
// Fake Port80Handler stub
|
||||
class FakePort80Handler extends plugins.EventEmitter {
|
||||
@ -36,7 +36,10 @@ tap.test('CertProvisioner handles static provisioning', async () => {
|
||||
domainName: domain,
|
||||
publicKey: 'CERT',
|
||||
privateKey: 'KEY',
|
||||
validUntil: Date.now() + 3600 * 1000
|
||||
validUntil: Date.now() + 3600 * 1000,
|
||||
created: Date.now(),
|
||||
csr: 'CSR',
|
||||
id: 'ID',
|
||||
};
|
||||
};
|
||||
const prov = new CertProvisioner(
|
||||
@ -117,7 +120,10 @@ tap.test('CertProvisioner on-demand static provisioning', async () => {
|
||||
domainName: domain,
|
||||
publicKey: 'PKEY',
|
||||
privateKey: 'PRIV',
|
||||
validUntil: Date.now() + 1000
|
||||
validUntil: Date.now() + 1000,
|
||||
created: Date.now(),
|
||||
csr: 'CSR',
|
||||
id: 'ID',
|
||||
});
|
||||
const prov = new CertProvisioner(
|
||||
domainConfigs,
|
||||
|
@ -31,10 +31,10 @@ function createProxyConfig(
|
||||
): tsclass.network.IReverseProxyConfig {
|
||||
return {
|
||||
hostName: hostname,
|
||||
destinationIp,
|
||||
destinationPort: destinationPort.toString(), // Convert to string for IReverseProxyConfig
|
||||
publicKey: 'mock-cert',
|
||||
privateKey: 'mock-key'
|
||||
privateKey: 'mock-key',
|
||||
destinationIps: [destinationIp],
|
||||
destinationPorts: [destinationPort],
|
||||
} as tsclass.network.IReverseProxyConfig;
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartproxy',
|
||||
version: '10.0.1',
|
||||
version: '10.2.0',
|
||||
description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.'
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
|
||||
/**
|
||||
* Shared types for certificate management and domain options
|
||||
*/
|
||||
@ -75,9 +77,9 @@ export interface IDomainForwardConfig {
|
||||
* Unified ACME configuration options used across proxies and handlers
|
||||
*/
|
||||
export interface IAcmeOptions {
|
||||
accountEmail?: string; // Email for Let's Encrypt account
|
||||
enabled?: boolean; // Whether ACME is enabled
|
||||
port?: number; // Port to listen on for ACME challenges (default: 80)
|
||||
contactEmail?: string; // Email for Let's Encrypt account
|
||||
useProduction?: boolean; // Use production environment (default: staging)
|
||||
httpsRedirectPort?: number; // Port to redirect HTTP requests to HTTPS (default: 443)
|
||||
renewThresholdDays?: number; // Days before expiry to renew certificates
|
||||
@ -86,4 +88,4 @@ export interface IAcmeOptions {
|
||||
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 './networkproxy/classes.np.networkproxy.js';
|
||||
export * from './networkproxy/index.js';
|
||||
export * from './port80handler/classes.port80handler.js';
|
||||
export * from './redirect/classes.redirect.js';
|
||||
export * from './smartproxy/classes.smartproxy.js';
|
||||
export * from './smartproxy/classes.pp.snihandler.js';
|
||||
export * from './smartproxy/classes.pp.interfaces.js';
|
||||
|
||||
export * from './common/types.js';
|
||||
|
@ -183,7 +183,6 @@ export class CertificateManager {
|
||||
|
||||
// Check if we have a certificate for this domain
|
||||
const certs = this.certificateCache.get(domain);
|
||||
|
||||
if (certs) {
|
||||
try {
|
||||
// Create TLS context with the cached certificate
|
||||
@ -191,7 +190,6 @@ export class CertificateManager {
|
||||
key: certs.key,
|
||||
cert: certs.cert
|
||||
});
|
||||
|
||||
this.logger.debug(`Using cached certificate for ${domain}`);
|
||||
cb(null, context);
|
||||
return;
|
||||
@ -199,6 +197,19 @@ export class CertificateManager {
|
||||
this.logger.error(`Error creating secure context for ${domain}:`, err);
|
||||
}
|
||||
}
|
||||
// No existing certificate: trigger dynamic provisioning via Port80Handler
|
||||
if (this.port80Handler) {
|
||||
try {
|
||||
this.logger.info(`Triggering on-demand certificate retrieval for ${domain}`);
|
||||
this.port80Handler.addDomain({
|
||||
domainName: domain,
|
||||
sslRedirect: false,
|
||||
acmeMaintenance: true
|
||||
});
|
||||
} catch (err) {
|
||||
this.logger.error(`Error registering domain for on-demand certificate: ${domain}`, err);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we should trigger certificate issuance
|
||||
if (this.options.acme?.enabled && this.port80Handler && !domain.includes('*')) {
|
||||
@ -357,7 +368,7 @@ export class CertificateManager {
|
||||
// Build and configure Port80Handler
|
||||
this.port80Handler = buildPort80Handler({
|
||||
port: this.options.acme.port,
|
||||
contactEmail: this.options.acme.contactEmail,
|
||||
accountEmail: this.options.acme.accountEmail,
|
||||
useProduction: this.options.acme.useProduction,
|
||||
httpsRedirectPort: this.options.port, // Redirect to our HTTPS port
|
||||
enabled: this.options.acme.enabled,
|
||||
|
@ -76,7 +76,7 @@ export class NetworkProxy implements IMetricsTracker {
|
||||
acme: {
|
||||
enabled: optionsArg.acme?.enabled || false,
|
||||
port: optionsArg.acme?.port || 80,
|
||||
contactEmail: optionsArg.acme?.contactEmail || 'admin@example.com',
|
||||
accountEmail: optionsArg.acme?.accountEmail || 'admin@example.com',
|
||||
useProduction: optionsArg.acme?.useProduction || false, // Default to staging for safety
|
||||
renewThresholdDays: optionsArg.acme?.renewThresholdDays || 30,
|
||||
autoRenew: optionsArg.acme?.autoRenew !== false, // Default to true
|
||||
|
@ -10,21 +10,6 @@ import type {
|
||||
IAcmeOptions
|
||||
} from '../common/types.js';
|
||||
// (fs and path I/O moved to CertProvisioner)
|
||||
// ACME HTTP-01 challenge handler storing tokens in memory (diskless)
|
||||
class DisklessHttp01Handler {
|
||||
private storage: Map<string, string>;
|
||||
constructor(storage: Map<string, string>) { this.storage = storage; }
|
||||
public getSupportedTypes(): string[] { return ['http-01']; }
|
||||
public async prepare(ch: any): Promise<void> {
|
||||
this.storage.set(ch.token, ch.keyAuthorization);
|
||||
}
|
||||
public async verify(ch: any): Promise<void> {
|
||||
return;
|
||||
}
|
||||
public async cleanup(ch: any): Promise<void> {
|
||||
this.storage.delete(ch.token);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom error classes for better error handling
|
||||
@ -80,11 +65,11 @@ interface IDomainCertificate {
|
||||
*/
|
||||
export class Port80Handler extends plugins.EventEmitter {
|
||||
private domainCertificates: Map<string, IDomainCertificate>;
|
||||
// In-memory storage for ACME HTTP-01 challenge tokens
|
||||
private acmeHttp01Storage: Map<string, string> = new Map();
|
||||
// SmartAcme instance for certificate management
|
||||
private smartAcme: plugins.smartacme.SmartAcme | null = null;
|
||||
private smartAcmeHttp01Handler!: plugins.smartacme.handlers.Http01MemoryHandler;
|
||||
private server: plugins.http.Server | null = null;
|
||||
|
||||
// Renewal scheduling is handled externally by SmartProxy
|
||||
// (Removed internal renewal timer)
|
||||
private isShuttingDown: boolean = false;
|
||||
@ -101,7 +86,7 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
// Default options
|
||||
this.options = {
|
||||
port: options.port ?? 80,
|
||||
contactEmail: options.contactEmail ?? 'admin@example.com',
|
||||
accountEmail: options.accountEmail ?? 'admin@example.com',
|
||||
useProduction: options.useProduction ?? false, // Safer default: staging
|
||||
httpsRedirectPort: options.httpsRedirectPort ?? 443,
|
||||
enabled: options.enabled ?? true, // Enable by default
|
||||
@ -131,13 +116,14 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
console.log('Port80Handler is disabled, skipping start');
|
||||
return;
|
||||
}
|
||||
// Initialize SmartAcme for ACME challenge management (diskless HTTP handler)
|
||||
// Initialize SmartAcme with in-memory HTTP-01 challenge handler
|
||||
if (this.options.enabled) {
|
||||
this.smartAcmeHttp01Handler = new plugins.smartacme.handlers.Http01MemoryHandler();
|
||||
this.smartAcme = new plugins.smartacme.SmartAcme({
|
||||
accountEmail: this.options.contactEmail,
|
||||
certManager: new plugins.smartacme.MemoryCertManager(),
|
||||
accountEmail: this.options.accountEmail,
|
||||
certManager: new plugins.smartacme.certmanagers.MemoryCertManager(),
|
||||
environment: this.options.useProduction ? 'production' : 'integration',
|
||||
challengeHandlers: [ new DisklessHttp01Handler(this.acmeHttp01Storage) ],
|
||||
challengeHandlers: [ this.smartAcmeHttp01Handler ],
|
||||
challengePriority: ['http-01'],
|
||||
});
|
||||
await this.smartAcme.start();
|
||||
@ -261,66 +247,6 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a certificate for a domain directly (for externally obtained certificates)
|
||||
* @param domain The domain for the certificate
|
||||
* @param certificate The certificate (PEM format)
|
||||
* @param privateKey The private key (PEM format)
|
||||
* @param expiryDate Optional expiry date
|
||||
*/
|
||||
public setCertificate(domain: string, certificate: string, privateKey: string, expiryDate?: Date): void {
|
||||
if (!domain || !certificate || !privateKey) {
|
||||
throw new Port80HandlerError('Domain, certificate and privateKey are required');
|
||||
}
|
||||
|
||||
// Don't allow setting certificates for glob patterns
|
||||
if (this.isGlobPattern(domain)) {
|
||||
throw new Port80HandlerError('Cannot set certificate for glob pattern domains');
|
||||
}
|
||||
|
||||
let domainInfo = this.domainCertificates.get(domain);
|
||||
|
||||
if (!domainInfo) {
|
||||
// Create default domain options if not already configured
|
||||
const defaultOptions: IDomainOptions = {
|
||||
domainName: domain,
|
||||
sslRedirect: true,
|
||||
acmeMaintenance: true
|
||||
};
|
||||
|
||||
domainInfo = {
|
||||
options: defaultOptions,
|
||||
certObtained: false,
|
||||
obtainingInProgress: false
|
||||
};
|
||||
this.domainCertificates.set(domain, domainInfo);
|
||||
}
|
||||
|
||||
domainInfo.certificate = certificate;
|
||||
domainInfo.privateKey = privateKey;
|
||||
domainInfo.certObtained = true;
|
||||
domainInfo.obtainingInProgress = false;
|
||||
|
||||
if (expiryDate) {
|
||||
domainInfo.expiryDate = expiryDate;
|
||||
} else {
|
||||
// Extract expiry date from certificate
|
||||
domainInfo.expiryDate = this.extractExpiryDateFromCertificate(certificate, domain);
|
||||
}
|
||||
|
||||
console.log(`Certificate set for ${domain}`);
|
||||
|
||||
// (Persistence of certificates moved to CertProvisioner)
|
||||
|
||||
// Emit certificate event
|
||||
this.emitCertificateEvent(Port80HandlerEvents.CERTIFICATE_ISSUED, {
|
||||
domain,
|
||||
certificate,
|
||||
privateKey,
|
||||
expiryDate: domainInfo.expiryDate || this.getDefaultExpiryDate()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the certificate for a domain if it exists
|
||||
* @param domain The domain to get the certificate for
|
||||
@ -423,9 +349,19 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
// Extract domain (ignoring any port in the Host header)
|
||||
const domain = hostHeader.split(':')[0];
|
||||
|
||||
// Dynamic provisioning: if domain not yet managed, register for ACME and return 503
|
||||
if (!this.domainCertificates.has(domain)) {
|
||||
try {
|
||||
this.addDomain({ domainName: domain, sslRedirect: false, acmeMaintenance: true });
|
||||
} catch (err) {
|
||||
console.error(`Error registering domain for on-demand provisioning: ${err}`);
|
||||
}
|
||||
res.statusCode = 503;
|
||||
res.end('Certificate issuance in progress');
|
||||
return;
|
||||
}
|
||||
// Get domain config, using glob pattern matching if needed
|
||||
const domainMatch = this.getDomainInfoForRequest(domain);
|
||||
|
||||
if (!domainMatch) {
|
||||
res.statusCode = 404;
|
||||
res.end('Domain not configured');
|
||||
@ -448,17 +384,12 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
res.end('Not found');
|
||||
return;
|
||||
}
|
||||
// Serve challenge response from in-memory storage
|
||||
const token = req.url.split('/').pop() || '';
|
||||
const keyAuth = this.acmeHttp01Storage.get(token);
|
||||
if (keyAuth) {
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', 'text/plain');
|
||||
res.end(keyAuth);
|
||||
console.log(`Served ACME challenge response for ${domain}`);
|
||||
// Delegate to Http01MemoryHandler
|
||||
if (this.smartAcmeHttp01Handler) {
|
||||
this.smartAcmeHttp01Handler.handleRequest(req, res);
|
||||
} else {
|
||||
res.statusCode = 404;
|
||||
res.end('Challenge token not found');
|
||||
res.statusCode = 500;
|
||||
res.end('ACME HTTP-01 handler not initialized');
|
||||
}
|
||||
return;
|
||||
}
|
||||
@ -734,36 +665,6 @@ export class Port80Handler extends plugins.EventEmitter {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets information about managed domains
|
||||
* @returns Array of domain information
|
||||
*/
|
||||
public getManagedDomains(): Array<{
|
||||
domain: string;
|
||||
isGlobPattern: boolean;
|
||||
hasCertificate: boolean;
|
||||
hasForwarding: boolean;
|
||||
sslRedirect: boolean;
|
||||
acmeMaintenance: boolean;
|
||||
}> {
|
||||
return Array.from(this.domainCertificates.entries()).map(([domain, info]) => ({
|
||||
domain,
|
||||
isGlobPattern: this.isGlobPattern(domain),
|
||||
hasCertificate: info.certObtained,
|
||||
hasForwarding: !!info.options.forward,
|
||||
sslRedirect: info.options.sslRedirect,
|
||||
acmeMaintenance: info.options.acmeMaintenance
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets configuration details
|
||||
* @returns Current configuration
|
||||
*/
|
||||
public getConfig(): Required<IAcmeOptions> {
|
||||
return { ...this.options };
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a certificate renewal for a specific domain.
|
||||
* @param domain The domain to renew.
|
||||
|
@ -14,7 +14,7 @@ export class CertProvisioner extends plugins.EventEmitter {
|
||||
private domainConfigs: IDomainConfig[];
|
||||
private port80Handler: Port80Handler;
|
||||
private networkProxyBridge: NetworkProxyBridge;
|
||||
private certProvider?: (domain: string) => Promise<ISmartProxyCertProvisionObject>;
|
||||
private certProvisionFunction?: (domain: string) => Promise<ISmartProxyCertProvisionObject>;
|
||||
private forwardConfigs: Array<{ domain: string; forwardConfig?: { ip: string; port: number }; acmeForwardConfig?: { ip: string; port: number }; sslRedirect: boolean }>;
|
||||
private renewThresholdDays: number;
|
||||
private renewCheckIntervalHours: number;
|
||||
@ -46,7 +46,7 @@ export class CertProvisioner extends plugins.EventEmitter {
|
||||
this.domainConfigs = domainConfigs;
|
||||
this.port80Handler = port80Handler;
|
||||
this.networkProxyBridge = networkProxyBridge;
|
||||
this.certProvider = certProvider;
|
||||
this.certProvisionFunction = certProvider;
|
||||
this.renewThresholdDays = renewThresholdDays;
|
||||
this.renewCheckIntervalHours = renewCheckIntervalHours;
|
||||
this.autoRenew = autoRenew;
|
||||
@ -81,20 +81,28 @@ export class CertProvisioner extends plugins.EventEmitter {
|
||||
// Initial provisioning for all domains
|
||||
const domains = this.domainConfigs.flatMap(cfg => cfg.domains);
|
||||
for (const domain of domains) {
|
||||
// Skip wildcard domains
|
||||
if (domain.includes('*')) continue;
|
||||
const isWildcard = domain.includes('*');
|
||||
let provision: ISmartProxyCertProvisionObject | 'http01' = 'http01';
|
||||
if (this.certProvider) {
|
||||
if (this.certProvisionFunction) {
|
||||
try {
|
||||
provision = await this.certProvider(domain);
|
||||
provision = await this.certProvisionFunction(domain);
|
||||
} catch (err) {
|
||||
console.error(`certProvider error for ${domain}:`, err);
|
||||
}
|
||||
} else if (isWildcard) {
|
||||
// No certProvider: cannot handle wildcard without DNS-01 support
|
||||
console.warn(`Skipping wildcard domain without certProvisionFunction: ${domain}`);
|
||||
continue;
|
||||
}
|
||||
if (provision === 'http01') {
|
||||
if (isWildcard) {
|
||||
console.warn(`Skipping HTTP-01 for wildcard domain: ${domain}`);
|
||||
continue;
|
||||
}
|
||||
this.provisionMap.set(domain, 'http01');
|
||||
this.port80Handler.addDomain({ domainName: domain, sslRedirect: true, acmeMaintenance: true });
|
||||
} else {
|
||||
// Static certificate (e.g., DNS-01 provisioned or user-provided) supports wildcard domains
|
||||
this.provisionMap.set(domain, 'static');
|
||||
const certObj = provision as plugins.tsclass.network.ICert;
|
||||
const certData: ICertificateData = {
|
||||
@ -120,8 +128,8 @@ export class CertProvisioner extends plugins.EventEmitter {
|
||||
try {
|
||||
if (type === 'http01') {
|
||||
await this.port80Handler.renewCertificate(domain);
|
||||
} else if (type === 'static' && this.certProvider) {
|
||||
const provision2 = await this.certProvider(domain);
|
||||
} else if (type === 'static' && this.certProvisionFunction) {
|
||||
const provision2 = await this.certProvisionFunction(domain);
|
||||
if (provision2 !== 'http01') {
|
||||
const certObj = provision2 as plugins.tsclass.network.ICert;
|
||||
const certData: ICertificateData = {
|
||||
@ -162,18 +170,22 @@ export class CertProvisioner extends plugins.EventEmitter {
|
||||
* @param domain Domain name to provision
|
||||
*/
|
||||
public async requestCertificate(domain: string): Promise<void> {
|
||||
// Skip wildcard domains
|
||||
if (domain.includes('*')) {
|
||||
throw new Error(`Cannot request certificate for wildcard domain: ${domain}`);
|
||||
}
|
||||
const isWildcard = domain.includes('*');
|
||||
// Determine provisioning method
|
||||
let provision: ISmartProxyCertProvisionObject | 'http01' = 'http01';
|
||||
if (this.certProvider) {
|
||||
provision = await this.certProvider(domain);
|
||||
if (this.certProvisionFunction) {
|
||||
provision = await this.certProvisionFunction(domain);
|
||||
} else if (isWildcard) {
|
||||
// Cannot perform HTTP-01 on wildcard without certProvider
|
||||
throw new Error(`Cannot request certificate for wildcard domain without certProvisionFunction: ${domain}`);
|
||||
}
|
||||
if (provision === 'http01') {
|
||||
if (isWildcard) {
|
||||
throw new Error(`Cannot request HTTP-01 certificate for wildcard domain: ${domain}`);
|
||||
}
|
||||
await this.port80Handler.renewCertificate(domain);
|
||||
} else {
|
||||
// Static certificate (e.g., DNS-01 provisioned) supports wildcards
|
||||
const certObj = provision as plugins.tsclass.network.ICert;
|
||||
const certData: ICertificateData = {
|
||||
domain: certObj.domainName,
|
||||
|
@ -2,7 +2,7 @@ import * as plugins from '../plugins.js';
|
||||
import type {
|
||||
IConnectionRecord,
|
||||
IDomainConfig,
|
||||
IPortProxySettings,
|
||||
ISmartProxyOptions,
|
||||
} from './classes.pp.interfaces.js';
|
||||
import { ConnectionManager } from './classes.pp.connectionmanager.js';
|
||||
import { SecurityManager } from './classes.pp.securitymanager.js';
|
||||
@ -17,7 +17,7 @@ import { PortRangeManager } from './classes.pp.portrangemanager.js';
|
||||
*/
|
||||
export class ConnectionHandler {
|
||||
constructor(
|
||||
private settings: IPortProxySettings,
|
||||
private settings: ISmartProxyOptions,
|
||||
private connectionManager: ConnectionManager,
|
||||
private securityManager: SecurityManager,
|
||||
private domainConfigManager: DomainConfigManager,
|
||||
@ -557,13 +557,41 @@ export class ConnectionHandler {
|
||||
this.tlsManager.isClientHello(chunk) &&
|
||||
!serverName
|
||||
) {
|
||||
// Block ClientHello without SNI when allowSessionTicket is false
|
||||
console.log(
|
||||
`[${connectionId}] No SNI detected in ClientHello and allowSessionTicket=false. ` +
|
||||
`Sending warning unrecognized_name alert to encourage immediate retry with SNI.`
|
||||
// Missing SNI: forward to NetworkProxy if available
|
||||
const proxyInstance = this.networkProxyBridge.getNetworkProxy();
|
||||
if (proxyInstance) {
|
||||
if (this.settings.enableDetailedLogging) {
|
||||
console.log(
|
||||
`[${connectionId}] No SNI in ClientHello; forwarding to NetworkProxy.`
|
||||
);
|
||||
}
|
||||
this.networkProxyBridge.forwardToNetworkProxy(
|
||||
connectionId,
|
||||
socket,
|
||||
record,
|
||||
chunk,
|
||||
undefined,
|
||||
(_reason) => {
|
||||
// On proxy failure, send TLS unrecognized_name alert and cleanup
|
||||
if (record.incomingTerminationReason === null) {
|
||||
record.incomingTerminationReason = 'session_ticket_blocked_no_sni';
|
||||
this.connectionManager.incrementTerminationStat(
|
||||
'incoming',
|
||||
'session_ticket_blocked_no_sni'
|
||||
);
|
||||
}
|
||||
const alert = Buffer.from([0x15, 0x03, 0x03, 0x00, 0x02, 0x01, 0x70]);
|
||||
try { socket.cork(); socket.write(alert); socket.uncork(); socket.end(); }
|
||||
catch { socket.end(); }
|
||||
this.connectionManager.initiateCleanupOnce(record, 'session_ticket_blocked_no_sni');
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Fallback: send TLS unrecognized_name alert and terminate
|
||||
console.log(
|
||||
`[${connectionId}] No SNI detected and proxy unavailable; sending TLS alert.`
|
||||
);
|
||||
|
||||
// Set the termination reason first
|
||||
if (record.incomingTerminationReason === null) {
|
||||
record.incomingTerminationReason = 'session_ticket_blocked_no_sni';
|
||||
this.connectionManager.incrementTerminationStat(
|
||||
@ -571,54 +599,10 @@ export class ConnectionHandler {
|
||||
'session_ticket_blocked_no_sni'
|
||||
);
|
||||
}
|
||||
|
||||
// Create a warning-level alert for unrecognized_name
|
||||
// This encourages Chrome to retry immediately with SNI
|
||||
const serverNameUnknownAlertData = Buffer.from([
|
||||
0x15, // Alert record type
|
||||
0x03,
|
||||
0x03, // TLS 1.2 version
|
||||
0x00,
|
||||
0x02, // Length
|
||||
0x01, // Warning alert level (not fatal)
|
||||
0x70, // unrecognized_name alert (code 112)
|
||||
]);
|
||||
|
||||
try {
|
||||
// Use cork/uncork to ensure the alert is sent as a single packet
|
||||
socket.cork();
|
||||
const writeSuccessful = socket.write(serverNameUnknownAlertData);
|
||||
socket.uncork();
|
||||
socket.end();
|
||||
|
||||
// Function to handle the clean socket termination - but more gradually
|
||||
const finishConnection = () => {
|
||||
this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
|
||||
};
|
||||
|
||||
if (writeSuccessful) {
|
||||
// Wait longer before ending connection to ensure alert is processed by client
|
||||
setTimeout(finishConnection, 200); // Increased from 50ms to 200ms
|
||||
} else {
|
||||
// If the kernel buffer was full, wait for the drain event
|
||||
socket.once('drain', () => {
|
||||
// Wait longer after drain as well
|
||||
setTimeout(finishConnection, 200);
|
||||
});
|
||||
|
||||
// Safety timeout is increased too
|
||||
setTimeout(() => {
|
||||
socket.removeAllListeners('drain');
|
||||
finishConnection();
|
||||
}, 400); // Increased from 250ms to 400ms
|
||||
}
|
||||
} catch (err) {
|
||||
// If we can't send the alert, fall back to immediate termination
|
||||
console.log(`[${connectionId}] Error sending TLS alert: ${err.message}`);
|
||||
socket.end();
|
||||
this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
|
||||
}
|
||||
|
||||
const alert = Buffer.from([0x15, 0x03, 0x03, 0x00, 0x02, 0x01, 0x70]);
|
||||
try { socket.cork(); socket.write(alert); socket.uncork(); socket.end(); }
|
||||
catch { socket.end(); }
|
||||
this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import type { IConnectionRecord, IPortProxySettings } from './classes.pp.interfaces.js';
|
||||
import type { IConnectionRecord, ISmartProxyOptions } from './classes.pp.interfaces.js';
|
||||
import { SecurityManager } from './classes.pp.securitymanager.js';
|
||||
import { TimeoutManager } from './classes.pp.timeoutmanager.js';
|
||||
|
||||
@ -14,7 +14,7 @@ export class ConnectionManager {
|
||||
} = { incoming: {}, outgoing: {} };
|
||||
|
||||
constructor(
|
||||
private settings: IPortProxySettings,
|
||||
private settings: ISmartProxyOptions,
|
||||
private securityManager: SecurityManager,
|
||||
private timeoutManager: TimeoutManager
|
||||
) {}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import type { IDomainConfig, IPortProxySettings } from './classes.pp.interfaces.js';
|
||||
import type { IDomainConfig, ISmartProxyOptions } from './classes.pp.interfaces.js';
|
||||
|
||||
/**
|
||||
* Manages domain configurations and target selection
|
||||
@ -8,7 +8,7 @@ export class DomainConfigManager {
|
||||
// Track round-robin indices for domain configs
|
||||
private domainTargetIndices: Map<IDomainConfig, number> = new Map();
|
||||
|
||||
constructor(private settings: IPortProxySettings) {}
|
||||
constructor(private settings: ISmartProxyOptions) {}
|
||||
|
||||
/**
|
||||
* Updates the domain configurations
|
||||
|
@ -22,7 +22,7 @@ export interface IDomainConfig {
|
||||
|
||||
/** Port proxy settings including global allowed port ranges */
|
||||
import type { IAcmeOptions } from '../common/types.js';
|
||||
export interface IPortProxySettings {
|
||||
export interface ISmartProxyOptions {
|
||||
fromPort: number;
|
||||
toPort: number;
|
||||
targetIP?: string; // Global target host to proxy to, defaults to 'localhost'
|
||||
@ -91,7 +91,7 @@ export interface IPortProxySettings {
|
||||
* Optional certificate provider callback. Return 'http01' to use HTTP-01 challenges,
|
||||
* or a static certificate object for immediate provisioning.
|
||||
*/
|
||||
certProvider?: (domain: string) => Promise<ISmartProxyCertProvisionObject>;
|
||||
certProvisionFunction?: (domain: string) => Promise<ISmartProxyCertProvisionObject>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -4,7 +4,7 @@ import { Port80Handler } from '../port80handler/classes.port80handler.js';
|
||||
import { Port80HandlerEvents } from '../common/types.js';
|
||||
import { subscribeToPort80Handler } from '../common/eventUtils.js';
|
||||
import type { ICertificateData } from '../common/types.js';
|
||||
import type { IConnectionRecord, IPortProxySettings, IDomainConfig } from './classes.pp.interfaces.js';
|
||||
import type { IConnectionRecord, ISmartProxyOptions, IDomainConfig } from './classes.pp.interfaces.js';
|
||||
|
||||
/**
|
||||
* Manages NetworkProxy integration for TLS termination
|
||||
@ -13,7 +13,7 @@ export class NetworkProxyBridge {
|
||||
private networkProxy: NetworkProxy | null = null;
|
||||
private port80Handler: Port80Handler | null = null;
|
||||
|
||||
constructor(private settings: IPortProxySettings) {}
|
||||
constructor(private settings: ISmartProxyOptions) {}
|
||||
|
||||
/**
|
||||
* Set the Port80Handler to use for certificate management
|
||||
|
@ -1,10 +1,10 @@
|
||||
import type{ IPortProxySettings } from './classes.pp.interfaces.js';
|
||||
import type{ ISmartProxyOptions } from './classes.pp.interfaces.js';
|
||||
|
||||
/**
|
||||
* Manages port ranges and port-based configuration
|
||||
*/
|
||||
export class PortRangeManager {
|
||||
constructor(private settings: IPortProxySettings) {}
|
||||
constructor(private settings: ISmartProxyOptions) {}
|
||||
|
||||
/**
|
||||
* Get all ports that should be listened on
|
||||
|
@ -1,5 +1,5 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import type { IPortProxySettings } from './classes.pp.interfaces.js';
|
||||
import type { ISmartProxyOptions } from './classes.pp.interfaces.js';
|
||||
|
||||
/**
|
||||
* Handles security aspects like IP tracking, rate limiting, and authorization
|
||||
@ -8,7 +8,7 @@ export class SecurityManager {
|
||||
private connectionsByIP: Map<string, Set<string>> = new Map();
|
||||
private connectionRateByIP: Map<string, number[]> = new Map();
|
||||
|
||||
constructor(private settings: IPortProxySettings) {}
|
||||
constructor(private settings: ISmartProxyOptions) {}
|
||||
|
||||
/**
|
||||
* Get connections count by IP
|
||||
|
@ -1,10 +1,10 @@
|
||||
import type { IConnectionRecord, IPortProxySettings } from './classes.pp.interfaces.js';
|
||||
import type { IConnectionRecord, ISmartProxyOptions } from './classes.pp.interfaces.js';
|
||||
|
||||
/**
|
||||
* Manages timeouts and inactivity tracking for connections
|
||||
*/
|
||||
export class TimeoutManager {
|
||||
constructor(private settings: IPortProxySettings) {}
|
||||
constructor(private settings: ISmartProxyOptions) {}
|
||||
|
||||
/**
|
||||
* Ensure timeout values don't exceed Node.js max safe integer
|
||||
|
@ -1,4 +1,4 @@
|
||||
import * as net from 'net';
|
||||
import * as plugins from '../plugins.js';
|
||||
|
||||
/**
|
||||
* TlsAlert class for managing TLS alert messages
|
||||
@ -99,7 +99,7 @@ export class TlsAlert {
|
||||
* @returns Promise that resolves when the alert has been sent
|
||||
*/
|
||||
static async send(
|
||||
socket: net.Socket,
|
||||
socket: plugins.net.Socket,
|
||||
level: number,
|
||||
description: number,
|
||||
closeAfterSend: boolean = false,
|
||||
@ -183,7 +183,7 @@ export class TlsAlert {
|
||||
* @param socket The socket to send the alert to
|
||||
* @returns Promise that resolves when the alert has been sent
|
||||
*/
|
||||
static async sendSniRequired(socket: net.Socket): Promise<void> {
|
||||
static async sendSniRequired(socket: plugins.net.Socket): Promise<void> {
|
||||
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)
|
||||
* @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);
|
||||
}
|
||||
|
||||
@ -208,7 +208,7 @@ export class TlsAlert {
|
||||
* @returns Promise that resolves when the alert has been sent
|
||||
*/
|
||||
static async sendCertificateExpired(
|
||||
socket: net.Socket,
|
||||
socket: plugins.net.Socket,
|
||||
fatal: boolean = false,
|
||||
closeAfterSend: boolean = true,
|
||||
closeDelay: number = 200
|
||||
@ -224,7 +224,7 @@ export class TlsAlert {
|
||||
* @param socket The socket to send the alerts to
|
||||
* @returns Promise that resolves when all alerts have been sent
|
||||
*/
|
||||
static async sendForceSniSequence(socket: net.Socket): Promise<void> {
|
||||
static async sendForceSniSequence(socket: plugins.net.Socket): Promise<void> {
|
||||
try {
|
||||
// Send unrecognized_name (warning)
|
||||
socket.cork();
|
||||
@ -249,7 +249,7 @@ export class TlsAlert {
|
||||
* @returns Promise that resolves when the alert has been sent and the connection closed
|
||||
*/
|
||||
static async sendFatalAndClose(
|
||||
socket: net.Socket,
|
||||
socket: plugins.net.Socket,
|
||||
description: number,
|
||||
closeDelay: number = 100
|
||||
): Promise<void> {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import type { IPortProxySettings } from './classes.pp.interfaces.js';
|
||||
import type { ISmartProxyOptions } from './classes.pp.interfaces.js';
|
||||
import { SniHandler } from './classes.pp.snihandler.js';
|
||||
|
||||
/**
|
||||
@ -16,7 +16,7 @@ interface IConnectionInfo {
|
||||
* Manages TLS-related operations including SNI extraction and validation
|
||||
*/
|
||||
export class TlsManager {
|
||||
constructor(private settings: IPortProxySettings) {}
|
||||
constructor(private settings: ISmartProxyOptions) {}
|
||||
|
||||
/**
|
||||
* Check if a data chunk appears to be a TLS handshake
|
||||
|
@ -1,5 +1,5 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import type { IPortProxySettings, IDomainConfig } from './classes.pp.interfaces.js';
|
||||
|
||||
import { ConnectionManager } from './classes.pp.connectionmanager.js';
|
||||
import { SecurityManager } from './classes.pp.securitymanager.js';
|
||||
import { DomainConfigManager } from './classes.pp.domainconfigmanager.js';
|
||||
@ -13,6 +13,9 @@ import { CertProvisioner } from './classes.pp.certprovisioner.js';
|
||||
import type { ICertificateData } from '../common/types.js';
|
||||
import { buildPort80Handler } from '../common/acmeFactory.js';
|
||||
|
||||
import type { ISmartProxyOptions, IDomainConfig } from './classes.pp.interfaces.js';
|
||||
export type { ISmartProxyOptions as IPortProxySettings, IDomainConfig };
|
||||
|
||||
/**
|
||||
* SmartProxy - Main class that coordinates all components
|
||||
*/
|
||||
@ -36,7 +39,7 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
// CertProvisioner for unified certificate workflows
|
||||
private certProvisioner?: CertProvisioner;
|
||||
|
||||
constructor(settingsArg: IPortProxySettings) {
|
||||
constructor(settingsArg: ISmartProxyOptions) {
|
||||
super();
|
||||
// Set reasonable defaults for all settings
|
||||
this.settings = {
|
||||
@ -75,7 +78,7 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
this.settings.acme = {
|
||||
enabled: false,
|
||||
port: 80,
|
||||
contactEmail: 'admin@example.com',
|
||||
accountEmail: 'admin@example.com',
|
||||
useProduction: false,
|
||||
renewThresholdDays: 30,
|
||||
autoRenew: true,
|
||||
@ -116,7 +119,7 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
/**
|
||||
* The settings for the port proxy
|
||||
*/
|
||||
public settings: IPortProxySettings;
|
||||
public settings: ISmartProxyOptions;
|
||||
|
||||
/**
|
||||
* Initialize the Port80Handler for ACME certificate management
|
||||
@ -162,7 +165,7 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
this.settings.domainConfigs,
|
||||
this.port80Handler,
|
||||
this.networkProxyBridge,
|
||||
this.settings.certProvider,
|
||||
this.settings.certProvisionFunction,
|
||||
acme.renewThresholdDays!,
|
||||
acme.renewCheckIntervalHours!,
|
||||
acme.autoRenew!,
|
||||
@ -388,16 +391,23 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
if (this.port80Handler && this.settings.acme?.enabled) {
|
||||
for (const domainConfig of newDomainConfigs) {
|
||||
for (const domain of domainConfig.domains) {
|
||||
if (domain.includes('*')) continue;
|
||||
let provision = 'http01' as string | plugins.tsclass.network.ICert;
|
||||
if (this.settings.certProvider) {
|
||||
const isWildcard = domain.includes('*');
|
||||
let provision: string | plugins.tsclass.network.ICert = 'http01';
|
||||
if (this.settings.certProvisionFunction) {
|
||||
try {
|
||||
provision = await this.settings.certProvider(domain);
|
||||
provision = await this.settings.certProvisionFunction(domain);
|
||||
} catch (err) {
|
||||
console.log(`certProvider error for ${domain}: ${err}`);
|
||||
}
|
||||
} else if (isWildcard) {
|
||||
console.warn(`Skipping wildcard domain without certProvisionFunction: ${domain}`);
|
||||
continue;
|
||||
}
|
||||
if (provision === 'http01') {
|
||||
if (isWildcard) {
|
||||
console.warn(`Skipping HTTP-01 for wildcard domain: ${domain}`);
|
||||
continue;
|
||||
}
|
||||
this.port80Handler.addDomain({
|
||||
domainName: domain,
|
||||
sslRedirect: true,
|
||||
@ -405,6 +415,7 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
});
|
||||
console.log(`Registered domain ${domain} with Port80Handler for HTTP-01`);
|
||||
} else {
|
||||
// Static certificate (e.g., DNS-01 provisioned) supports wildcards
|
||||
const certObj = provision as plugins.tsclass.network.ICert;
|
||||
const certData: ICertificateData = {
|
||||
domain: certObj.domainName,
|
||||
|
Loading…
x
Reference in New Issue
Block a user