Compare commits

...

29 Commits

Author SHA1 Message Date
479f5160da 10.2.0
Some checks failed
Default (tags) / security (push) Successful in 43s
Default (tags) / test (push) Failing after 1m17s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-05-05 17:03:22 +00:00
0f356c9bbf 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. 2025-05-05 17:03:22 +00:00
036d522048 10.1.0
Some checks failed
Default (tags) / security (push) Successful in 42s
Default (tags) / test (push) Failing after 1m17s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-05-05 15:42:48 +00:00
9c05f71cd6 feat(smartproxy): Implement fallback to NetworkProxy on missing SNI and rename certProvider to certProvisionFunction in CertProvisioner 2025-05-05 15:42:48 +00:00
a9963f3b8a 10.0.12
Some checks failed
Default (tags) / security (push) Successful in 42s
Default (tags) / test (push) Failing after 1m15s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-05-05 15:16:26 +00:00
05c9156458 fix(port80handler): refactor ACME challenge handling to use dedicated Http01MemoryHandler, remove obsolete readme.plan.md, and update version to 10.0.12 2025-05-05 15:16:26 +00:00
47e3c86487 fix(dependencies): Update @push.rocks/smartacme to ^7.3.2; replace DisklessHttp01Handler with Http01MemoryHandler in Port80Handler 2025-05-05 14:47:20 +00:00
1387928938 10.0.11
Some checks failed
Default (tags) / security (push) Successful in 34s
Default (tags) / test (push) Failing after 1m14s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-05-05 10:52:49 +00:00
19578b061e 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() 2025-05-05 10:52:48 +00:00
e8a539829a 10.0.10
Some checks failed
Default (tags) / security (push) Successful in 42s
Default (tags) / test (push) Failing after 1m15s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-05-05 10:46:05 +00:00
a646f4ad28 fix(docs): Update README: rename certProviderFunction to certProvisionFunction in configuration options for consistency. 2025-05-05 10:46:05 +00:00
aa70dcc299 10.0.9
Some checks failed
Default (tags) / security (push) Successful in 25s
Default (tags) / test (push) Failing after 1m13s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-05-05 10:30:08 +00:00
adb85d920f fix(documentation): Update documentation to use certProviderFunction instead of certProvider in SmartProxy settings. 2025-05-05 10:30:08 +00:00
2e4c6312cd 10.0.8
Some checks failed
Default (tags) / security (push) Successful in 32s
Default (tags) / test (push) Failing after 1m17s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-05-05 10:29:00 +00:00
9b773608c7 fix(smartproxy): rename certProvider to certProvisionFunction in certificate provisioning interfaces and SmartProxy 2025-05-05 10:29:00 +00:00
3502807023 10.0.7
Some checks failed
Default (tags) / security (push) Successful in 43s
Default (tags) / test (push) Failing after 1m26s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-05-04 13:49:22 +00:00
c6dff8b78d fix(core): refactor: Rename IPortProxySettings to ISmartProxyOptions in internal modules 2025-05-04 13:49:22 +00:00
12b18373db 10.0.6
Some checks failed
Default (tags) / security (push) Successful in 31s
Default (tags) / test (push) Failing after 1m15s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-05-04 13:05:48 +00:00
30c25ec70c fix(smartproxy): No changes detected in project files. This commit updates commit info without modifying any functionality. 2025-05-04 13:05:48 +00:00
434834fc06 10.0.5
Some checks failed
Default (tags) / security (push) Successful in 38s
Default (tags) / test (push) Failing after 1m15s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-05-04 13:04:35 +00:00
e7243243d0 fix(exports/types): Refactor exports and remove duplicate IReverseProxyConfig interface 2025-05-04 13:04:34 +00:00
cce2aed892 10.0.4
Some checks failed
Default (tags) / security (push) Successful in 42s
Default (tags) / test (push) Failing after 1m16s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-05-04 12:44:35 +00:00
8cd693c063 fix(core): Refactor module exports and update packageManager version in package.json 2025-05-04 12:44:35 +00:00
09ad7644f4 10.0.3
Some checks failed
Default (tags) / security (push) Successful in 46s
Default (tags) / test (push) Failing after 1m19s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-05-04 12:21:02 +00:00
f72f884eda 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. 2025-05-04 12:21:02 +00:00
73f3dfcad4 10.0.2
Some checks failed
Default (tags) / security (push) Successful in 42s
Default (tags) / test (push) Failing after 1m18s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-05-03 19:02:26 +00:00
8291f1f33a fix(tlsalert): Centralize plugin imports in TlsAlert and update plan checklist 2025-05-03 19:02:26 +00:00
f512fb4252 10.0.1
Some checks failed
Default (tags) / security (push) Successful in 37s
Default (tags) / test (push) Failing after 1m18s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-05-03 13:27:59 +00:00
1f3ee1eafc fix(docs): Improve mermaid diagram formatting in readme.md using HTML <br> tags for line breaks 2025-05-03 13:27:59 +00:00
25 changed files with 352 additions and 302 deletions

View File

@ -1,5 +1,110 @@
# 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
- 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

View File

@ -1,6 +1,6 @@
{
"name": "@push.rocks/smartproxy",
"version": "10.0.0",
"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
View File

@ -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:

View File

@ -180,12 +180,12 @@ flowchart TB
subgraph "SmartProxy Components"
direction TB
HTTP80[HTTP Port 80\nRedirect / SslRedirect]
HTTPS443[HTTPS Port 443\nNetworkProxy]
SmartProxy[SmartProxy\n(TCP/SNI Proxy)]
HTTP80["HTTP Port 80<br>Redirect / SslRedirect"]
HTTPS443["HTTPS Port 443<br>NetworkProxy"]
SmartProxy["SmartProxy<br>(TCP/SNI Proxy)"]
NfTables[NfTablesProxy]
Router[ProxyRouter]
ACME[Port80Handler\n(ACME HTTP-01)]
ACME["Port80Handler<br>(ACME HTTP-01)"]
Certs[(SSL Certificates)]
end
@ -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

View File

@ -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.

View File

@ -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,

View File

@ -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;
}

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartproxy',
version: '10.0.0',
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.'
}

View File

@ -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
}
}

View File

@ -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';

View File

@ -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,

View File

@ -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

View File

@ -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.

View File

@ -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,

View File

@ -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;
}
}

View File

@ -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
) {}

View File

@ -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

View File

@ -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>;
}
/**

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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> {

View File

@ -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

View File

@ -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,