Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
d81cf94876 | |||
8d06f1533e | |||
223be61c8d | |||
6a693f4d86 | |||
27a2bcb556 | |||
0674ca7163 | |||
e31c84493f |
17
changelog.md
17
changelog.md
@ -1,5 +1,22 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-03-11 - 3.37.0 - feat(portproxy)
|
||||||
|
Add ACME certificate management options to PortProxy, update ACME settings handling, and bump dependency versions
|
||||||
|
|
||||||
|
- Bumped version in package.json from 3.34.0 to 3.36.0 and updated commitinfo accordingly
|
||||||
|
- Updated dependencies: @push.rocks/tapbundle to ^5.5.10, @types/node to ^22.13.10, and @tsclass/tsclass to ^5.0.0
|
||||||
|
- Added ACME certificate management configuration to PortProxy settings (acme options, updateAcmeSettings, requestCertificate)
|
||||||
|
- Enhanced sync of domain configs to NetworkProxy with fallback for missing default certificates
|
||||||
|
|
||||||
|
## 2025-03-11 - 3.34.0 - feat(core)
|
||||||
|
Improve wildcard domain matching and enhance NetworkProxy integration in PortProxy. Added support for TLD wildcards and complex wildcard patterns in the router, and refactored TLS renegotiation handling for stricter SNI enforcement.
|
||||||
|
|
||||||
|
- Added support for TLD wildcard matching (e.g., 'example.*') to improve domain routing.
|
||||||
|
- Implemented complex wildcard pattern matching (e.g., '*.lossless*') in the router.
|
||||||
|
- Enhanced NetworkProxy integration by initializing a single NetworkProxy instance and forwarding TLS connections accordingly.
|
||||||
|
- Refactored TLS renegotiation handling to terminate connections on SNI mismatch for stricter enforcement.
|
||||||
|
- Updated tests to cover the new wildcard matching scenarios.
|
||||||
|
|
||||||
## 2025-03-11 - 3.33.0 - feat(portproxy)
|
## 2025-03-11 - 3.33.0 - feat(portproxy)
|
||||||
Add browser-friendly mode and SNI renegotiation configuration options to PortProxy
|
Add browser-friendly mode and SNI renegotiation configuration options to PortProxy
|
||||||
|
|
||||||
|
10
package.json
10
package.json
@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartproxy",
|
"name": "@push.rocks/smartproxy",
|
||||||
"version": "3.33.0",
|
"version": "3.37.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, and dynamic routing with authentication options.",
|
"description": "A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.",
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
"typings": "dist_ts/index.d.ts",
|
"typings": "dist_ts/index.d.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@ -18,8 +18,8 @@
|
|||||||
"@git.zone/tsbuild": "^2.2.6",
|
"@git.zone/tsbuild": "^2.2.6",
|
||||||
"@git.zone/tsrun": "^1.2.44",
|
"@git.zone/tsrun": "^1.2.44",
|
||||||
"@git.zone/tstest": "^1.0.77",
|
"@git.zone/tstest": "^1.0.77",
|
||||||
"@push.rocks/tapbundle": "^5.5.6",
|
"@push.rocks/tapbundle": "^5.5.10",
|
||||||
"@types/node": "^22.13.9",
|
"@types/node": "^22.13.10",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -28,7 +28,7 @@
|
|||||||
"@push.rocks/smartpromise": "^4.2.3",
|
"@push.rocks/smartpromise": "^4.2.3",
|
||||||
"@push.rocks/smartrequest": "^2.0.23",
|
"@push.rocks/smartrequest": "^2.0.23",
|
||||||
"@push.rocks/smartstring": "^4.0.15",
|
"@push.rocks/smartstring": "^4.0.15",
|
||||||
"@tsclass/tsclass": "^4.4.3",
|
"@tsclass/tsclass": "^5.0.0",
|
||||||
"@types/minimatch": "^5.1.2",
|
"@types/minimatch": "^5.1.2",
|
||||||
"@types/ws": "^8.18.0",
|
"@types/ws": "^8.18.0",
|
||||||
"acme-client": "^5.4.0",
|
"acme-client": "^5.4.0",
|
||||||
|
119
pnpm-lock.yaml
generated
119
pnpm-lock.yaml
generated
@ -24,8 +24,8 @@ importers:
|
|||||||
specifier: ^4.0.15
|
specifier: ^4.0.15
|
||||||
version: 4.0.15
|
version: 4.0.15
|
||||||
'@tsclass/tsclass':
|
'@tsclass/tsclass':
|
||||||
specifier: ^4.4.3
|
specifier: ^5.0.0
|
||||||
version: 4.4.3
|
version: 5.0.0
|
||||||
'@types/minimatch':
|
'@types/minimatch':
|
||||||
specifier: ^5.1.2
|
specifier: ^5.1.2
|
||||||
version: 5.1.2
|
version: 5.1.2
|
||||||
@ -55,11 +55,11 @@ importers:
|
|||||||
specifier: ^1.0.77
|
specifier: ^1.0.77
|
||||||
version: 1.0.96(@aws-sdk/credential-providers@3.758.0)(socks@2.8.4)(typescript@5.8.2)
|
version: 1.0.96(@aws-sdk/credential-providers@3.758.0)(socks@2.8.4)(typescript@5.8.2)
|
||||||
'@push.rocks/tapbundle':
|
'@push.rocks/tapbundle':
|
||||||
specifier: ^5.5.6
|
specifier: ^5.5.10
|
||||||
version: 5.5.6(@aws-sdk/credential-providers@3.758.0)(socks@2.8.4)
|
version: 5.5.10(@aws-sdk/credential-providers@3.758.0)(socks@2.8.4)
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^22.13.9
|
specifier: ^22.13.10
|
||||||
version: 22.13.9
|
version: 22.13.10
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.8.2
|
specifier: ^5.8.2
|
||||||
version: 5.8.2
|
version: 5.8.2
|
||||||
@ -941,8 +941,8 @@ packages:
|
|||||||
'@push.rocks/smartyaml@2.0.5':
|
'@push.rocks/smartyaml@2.0.5':
|
||||||
resolution: {integrity: sha512-tBcf+HaOIfeEsTMwgUZDtZERCxXQyRsWO8Ar5DjBdiSRchbhVGZQEBzXswMS0W5ZoRenjgPK+4tPW3JQGRTfbg==}
|
resolution: {integrity: sha512-tBcf+HaOIfeEsTMwgUZDtZERCxXQyRsWO8Ar5DjBdiSRchbhVGZQEBzXswMS0W5ZoRenjgPK+4tPW3JQGRTfbg==}
|
||||||
|
|
||||||
'@push.rocks/tapbundle@5.5.6':
|
'@push.rocks/tapbundle@5.5.10':
|
||||||
resolution: {integrity: sha512-V6u+nZwt4fNccxbm3ztZgHr/QAj/uKhaaOUFgtaae0jzYdds4jNEI+mXLpfXuNMgm7Nx93Lk5XUxWKTI8drjNw==}
|
resolution: {integrity: sha512-vTGzd3/kzKp8s6jrREGIKGG+87fy7grcTZIelVDpyZMtBdIi9Fe7g8EIw/reVx8oTAFSuUEAEaAT8B5NDIFStg==}
|
||||||
|
|
||||||
'@push.rocks/taskbuffer@3.1.7':
|
'@push.rocks/taskbuffer@3.1.7':
|
||||||
resolution: {integrity: sha512-QktGVJPucqQmW/QNGnscf4FAigT1H7JWKFGFdRuDEaOHKFh9qN+PXG3QY7DtZ4jfXdGLxPN4yAufDuPSAJYFnw==}
|
resolution: {integrity: sha512-QktGVJPucqQmW/QNGnscf4FAigT1H7JWKFGFdRuDEaOHKFh9qN+PXG3QY7DtZ4jfXdGLxPN4yAufDuPSAJYFnw==}
|
||||||
@ -1315,8 +1315,11 @@ packages:
|
|||||||
'@tsclass/tsclass@3.0.48':
|
'@tsclass/tsclass@3.0.48':
|
||||||
resolution: {integrity: sha512-hC65UvDlp9qvsl6OcIZXz0JNiWZ0gyzsTzbXpg215sGxopgbkOLCr6E0s4qCTnweYm95gt2AdY95uP7M7kExaQ==}
|
resolution: {integrity: sha512-hC65UvDlp9qvsl6OcIZXz0JNiWZ0gyzsTzbXpg215sGxopgbkOLCr6E0s4qCTnweYm95gt2AdY95uP7M7kExaQ==}
|
||||||
|
|
||||||
'@tsclass/tsclass@4.4.3':
|
'@tsclass/tsclass@4.4.4':
|
||||||
resolution: {integrity: sha512-Vhp+B1UsYlwXLhIeds++CXEeCwFgRzpput4YNM7Qyhr+UQgIMFRFAs2HSI3jEE5r9c1hR9G6MkSxi2U/CLyiaA==}
|
resolution: {integrity: sha512-YZOAF+u+r4u5rCev2uUd1KBTBdfyFdtDmcv4wuN+864lMccbdfRICR3SlJwCfYS1lbeV3QNLYGD30wjRXgvCJA==}
|
||||||
|
|
||||||
|
'@tsclass/tsclass@5.0.0':
|
||||||
|
resolution: {integrity: sha512-2X66VCk0Oe1L01j6GQHC6F9Gj7lpZPPSUTDNax7e29lm4OqBTyAzTR3ePR8coSbWBwsmRV8awLRSrSI+swlqWA==}
|
||||||
|
|
||||||
'@types/accepts@1.3.7':
|
'@types/accepts@1.3.7':
|
||||||
resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==}
|
resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==}
|
||||||
@ -1472,8 +1475,8 @@ packages:
|
|||||||
'@types/node-forge@1.3.11':
|
'@types/node-forge@1.3.11':
|
||||||
resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==}
|
resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==}
|
||||||
|
|
||||||
'@types/node@22.13.9':
|
'@types/node@22.13.10':
|
||||||
resolution: {integrity: sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==}
|
resolution: {integrity: sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==}
|
||||||
|
|
||||||
'@types/parse5@6.0.3':
|
'@types/parse5@6.0.3':
|
||||||
resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==}
|
resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==}
|
||||||
@ -4373,7 +4376,7 @@ snapshots:
|
|||||||
'@push.rocks/taskbuffer': 3.1.7
|
'@push.rocks/taskbuffer': 3.1.7
|
||||||
'@push.rocks/webrequest': 3.0.37
|
'@push.rocks/webrequest': 3.0.37
|
||||||
'@push.rocks/webstore': 2.0.20
|
'@push.rocks/webstore': 2.0.20
|
||||||
'@tsclass/tsclass': 4.4.3
|
'@tsclass/tsclass': 4.4.4
|
||||||
'@types/express': 4.17.21
|
'@types/express': 4.17.21
|
||||||
body-parser: 1.20.3
|
body-parser: 1.20.3
|
||||||
cors: 2.8.5
|
cors: 2.8.5
|
||||||
@ -5231,7 +5234,7 @@ snapshots:
|
|||||||
'@push.rocks/smartlog': 3.0.7
|
'@push.rocks/smartlog': 3.0.7
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartshell': 3.2.3
|
'@push.rocks/smartshell': 3.2.3
|
||||||
'@push.rocks/tapbundle': 5.5.6(@aws-sdk/credential-providers@3.758.0)(socks@2.8.4)
|
'@push.rocks/tapbundle': 5.5.10(@aws-sdk/credential-providers@3.758.0)(socks@2.8.4)
|
||||||
'@types/ws': 8.18.0
|
'@types/ws': 8.18.0
|
||||||
figures: 6.1.0
|
figures: 6.1.0
|
||||||
ws: 8.18.1
|
ws: 8.18.1
|
||||||
@ -5281,7 +5284,7 @@ snapshots:
|
|||||||
'@jest/schemas': 29.6.3
|
'@jest/schemas': 29.6.3
|
||||||
'@types/istanbul-lib-coverage': 2.0.6
|
'@types/istanbul-lib-coverage': 2.0.6
|
||||||
'@types/istanbul-reports': 3.0.4
|
'@types/istanbul-reports': 3.0.4
|
||||||
'@types/node': 22.13.9
|
'@types/node': 22.13.10
|
||||||
'@types/yargs': 17.0.33
|
'@types/yargs': 17.0.33
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
|
|
||||||
@ -5529,7 +5532,7 @@ snapshots:
|
|||||||
'@push.rocks/smartstring': 4.0.15
|
'@push.rocks/smartstring': 4.0.15
|
||||||
'@push.rocks/smartunique': 3.0.9
|
'@push.rocks/smartunique': 3.0.9
|
||||||
'@push.rocks/taskbuffer': 3.1.7
|
'@push.rocks/taskbuffer': 3.1.7
|
||||||
'@tsclass/tsclass': 4.4.3
|
'@tsclass/tsclass': 4.4.4
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- aws-crt
|
- aws-crt
|
||||||
|
|
||||||
@ -5551,7 +5554,7 @@ snapshots:
|
|||||||
'@pushrocks/smartjson': 4.0.6
|
'@pushrocks/smartjson': 4.0.6
|
||||||
'@pushrocks/smartpath': 5.0.5
|
'@pushrocks/smartpath': 5.0.5
|
||||||
'@pushrocks/smartpromise': 3.1.10
|
'@pushrocks/smartpromise': 3.1.10
|
||||||
'@tsclass/tsclass': 4.4.3
|
'@tsclass/tsclass': 4.4.4
|
||||||
mongodb: 4.17.2
|
mongodb: 4.17.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- aws-crt
|
- aws-crt
|
||||||
@ -5602,7 +5605,7 @@ snapshots:
|
|||||||
'@push.rocks/smartstream': 3.2.5
|
'@push.rocks/smartstream': 3.2.5
|
||||||
'@push.rocks/smartstring': 4.0.15
|
'@push.rocks/smartstring': 4.0.15
|
||||||
'@push.rocks/smartunique': 3.0.9
|
'@push.rocks/smartunique': 3.0.9
|
||||||
'@tsclass/tsclass': 4.4.3
|
'@tsclass/tsclass': 4.4.4
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- aws-crt
|
- aws-crt
|
||||||
|
|
||||||
@ -5652,7 +5655,7 @@ snapshots:
|
|||||||
'@push.rocks/smarttime': 4.1.1
|
'@push.rocks/smarttime': 4.1.1
|
||||||
'@push.rocks/smartunique': 3.0.9
|
'@push.rocks/smartunique': 3.0.9
|
||||||
'@push.rocks/taskbuffer': 3.1.7
|
'@push.rocks/taskbuffer': 3.1.7
|
||||||
'@tsclass/tsclass': 4.4.3
|
'@tsclass/tsclass': 4.4.4
|
||||||
mongodb: 6.14.2(@aws-sdk/credential-providers@3.758.0)(socks@2.8.4)
|
mongodb: 6.14.2(@aws-sdk/credential-providers@3.758.0)(socks@2.8.4)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@aws-sdk/credential-providers'
|
- '@aws-sdk/credential-providers'
|
||||||
@ -5764,7 +5767,7 @@ snapshots:
|
|||||||
'@push.rocks/smartlog-interfaces@3.0.2':
|
'@push.rocks/smartlog-interfaces@3.0.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@api.global/typedrequest-interfaces': 2.0.2
|
'@api.global/typedrequest-interfaces': 2.0.2
|
||||||
'@tsclass/tsclass': 4.4.3
|
'@tsclass/tsclass': 4.4.4
|
||||||
|
|
||||||
'@push.rocks/smartlog@3.0.7':
|
'@push.rocks/smartlog@3.0.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -5879,7 +5882,7 @@ snapshots:
|
|||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartpuppeteer': 2.0.5(typescript@5.8.2)
|
'@push.rocks/smartpuppeteer': 2.0.5(typescript@5.8.2)
|
||||||
'@push.rocks/smartunique': 3.0.9
|
'@push.rocks/smartunique': 3.0.9
|
||||||
'@tsclass/tsclass': 4.4.3
|
'@tsclass/tsclass': 4.4.4
|
||||||
'@types/express': 5.0.0
|
'@types/express': 5.0.0
|
||||||
express: 4.21.2
|
express: 4.21.2
|
||||||
pdf-lib: 1.17.1
|
pdf-lib: 1.17.1
|
||||||
@ -5929,7 +5932,7 @@ snapshots:
|
|||||||
'@push.rocks/smartbucket': 3.3.7
|
'@push.rocks/smartbucket': 3.3.7
|
||||||
'@push.rocks/smartfile': 11.2.0
|
'@push.rocks/smartfile': 11.2.0
|
||||||
'@push.rocks/smartpath': 5.0.18
|
'@push.rocks/smartpath': 5.0.18
|
||||||
'@tsclass/tsclass': 4.4.3
|
'@tsclass/tsclass': 4.4.4
|
||||||
'@types/s3rver': 3.7.4
|
'@types/s3rver': 3.7.4
|
||||||
s3rver: 3.7.1
|
s3rver: 3.7.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@ -5952,7 +5955,7 @@ snapshots:
|
|||||||
'@push.rocks/smartxml': 1.1.1
|
'@push.rocks/smartxml': 1.1.1
|
||||||
'@push.rocks/smartyaml': 2.0.5
|
'@push.rocks/smartyaml': 2.0.5
|
||||||
'@push.rocks/webrequest': 3.0.37
|
'@push.rocks/webrequest': 3.0.37
|
||||||
'@tsclass/tsclass': 4.4.3
|
'@tsclass/tsclass': 4.4.4
|
||||||
|
|
||||||
'@push.rocks/smartsocket@2.0.27':
|
'@push.rocks/smartsocket@2.0.27':
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -6058,7 +6061,7 @@ snapshots:
|
|||||||
'@types/js-yaml': 3.12.10
|
'@types/js-yaml': 3.12.10
|
||||||
js-yaml: 3.14.1
|
js-yaml: 3.14.1
|
||||||
|
|
||||||
'@push.rocks/tapbundle@5.5.6(@aws-sdk/credential-providers@3.758.0)(socks@2.8.4)':
|
'@push.rocks/tapbundle@5.5.10(@aws-sdk/credential-providers@3.758.0)(socks@2.8.4)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@open-wc/testing': 4.0.0
|
'@open-wc/testing': 4.0.0
|
||||||
'@push.rocks/consolecolor': 2.0.2
|
'@push.rocks/consolecolor': 2.0.2
|
||||||
@ -6112,7 +6115,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@pushrocks/smartdelay': 3.0.1
|
'@pushrocks/smartdelay': 3.0.1
|
||||||
'@pushrocks/smartpromise': 4.0.2
|
'@pushrocks/smartpromise': 4.0.2
|
||||||
'@tsclass/tsclass': 4.4.3
|
'@tsclass/tsclass': 4.4.4
|
||||||
|
|
||||||
'@push.rocks/webstore@2.0.20':
|
'@push.rocks/webstore@2.0.20':
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -6654,20 +6657,24 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
type-fest: 2.19.0
|
type-fest: 2.19.0
|
||||||
|
|
||||||
'@tsclass/tsclass@4.4.3':
|
'@tsclass/tsclass@4.4.4':
|
||||||
|
dependencies:
|
||||||
|
type-fest: 4.37.0
|
||||||
|
|
||||||
|
'@tsclass/tsclass@5.0.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
type-fest: 4.37.0
|
type-fest: 4.37.0
|
||||||
|
|
||||||
'@types/accepts@1.3.7':
|
'@types/accepts@1.3.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.13.9
|
'@types/node': 22.13.10
|
||||||
|
|
||||||
'@types/babel__code-frame@7.0.6': {}
|
'@types/babel__code-frame@7.0.6': {}
|
||||||
|
|
||||||
'@types/body-parser@1.19.5':
|
'@types/body-parser@1.19.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/connect': 3.4.38
|
'@types/connect': 3.4.38
|
||||||
'@types/node': 22.13.9
|
'@types/node': 22.13.10
|
||||||
|
|
||||||
'@types/buffer-json@2.0.3': {}
|
'@types/buffer-json@2.0.3': {}
|
||||||
|
|
||||||
@ -6683,17 +6690,17 @@ snapshots:
|
|||||||
|
|
||||||
'@types/clean-css@4.2.11':
|
'@types/clean-css@4.2.11':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.13.9
|
'@types/node': 22.13.10
|
||||||
source-map: 0.6.1
|
source-map: 0.6.1
|
||||||
|
|
||||||
'@types/co-body@6.1.3':
|
'@types/co-body@6.1.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.13.9
|
'@types/node': 22.13.10
|
||||||
'@types/qs': 6.9.18
|
'@types/qs': 6.9.18
|
||||||
|
|
||||||
'@types/connect@3.4.38':
|
'@types/connect@3.4.38':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.13.9
|
'@types/node': 22.13.10
|
||||||
|
|
||||||
'@types/content-disposition@0.5.8': {}
|
'@types/content-disposition@0.5.8': {}
|
||||||
|
|
||||||
@ -6706,11 +6713,11 @@ snapshots:
|
|||||||
'@types/connect': 3.4.38
|
'@types/connect': 3.4.38
|
||||||
'@types/express': 5.0.0
|
'@types/express': 5.0.0
|
||||||
'@types/keygrip': 1.0.6
|
'@types/keygrip': 1.0.6
|
||||||
'@types/node': 22.13.9
|
'@types/node': 22.13.10
|
||||||
|
|
||||||
'@types/cors@2.8.17':
|
'@types/cors@2.8.17':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.13.9
|
'@types/node': 22.13.10
|
||||||
|
|
||||||
'@types/debounce@1.2.4': {}
|
'@types/debounce@1.2.4': {}
|
||||||
|
|
||||||
@ -6724,14 +6731,14 @@ snapshots:
|
|||||||
|
|
||||||
'@types/express-serve-static-core@4.19.6':
|
'@types/express-serve-static-core@4.19.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.13.9
|
'@types/node': 22.13.10
|
||||||
'@types/qs': 6.9.18
|
'@types/qs': 6.9.18
|
||||||
'@types/range-parser': 1.2.7
|
'@types/range-parser': 1.2.7
|
||||||
'@types/send': 0.17.4
|
'@types/send': 0.17.4
|
||||||
|
|
||||||
'@types/express-serve-static-core@5.0.6':
|
'@types/express-serve-static-core@5.0.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.13.9
|
'@types/node': 22.13.10
|
||||||
'@types/qs': 6.9.18
|
'@types/qs': 6.9.18
|
||||||
'@types/range-parser': 1.2.7
|
'@types/range-parser': 1.2.7
|
||||||
'@types/send': 0.17.4
|
'@types/send': 0.17.4
|
||||||
@ -6756,30 +6763,30 @@ snapshots:
|
|||||||
|
|
||||||
'@types/from2@2.3.5':
|
'@types/from2@2.3.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.13.9
|
'@types/node': 22.13.10
|
||||||
|
|
||||||
'@types/fs-extra@11.0.4':
|
'@types/fs-extra@11.0.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/jsonfile': 6.1.4
|
'@types/jsonfile': 6.1.4
|
||||||
'@types/node': 22.13.9
|
'@types/node': 22.13.10
|
||||||
|
|
||||||
'@types/fs-extra@9.0.13':
|
'@types/fs-extra@9.0.13':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.13.9
|
'@types/node': 22.13.10
|
||||||
|
|
||||||
'@types/glob@7.2.0':
|
'@types/glob@7.2.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/minimatch': 5.1.2
|
'@types/minimatch': 5.1.2
|
||||||
'@types/node': 22.13.9
|
'@types/node': 22.13.10
|
||||||
|
|
||||||
'@types/glob@8.1.0':
|
'@types/glob@8.1.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/minimatch': 5.1.2
|
'@types/minimatch': 5.1.2
|
||||||
'@types/node': 22.13.9
|
'@types/node': 22.13.10
|
||||||
|
|
||||||
'@types/gunzip-maybe@1.4.2':
|
'@types/gunzip-maybe@1.4.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.13.9
|
'@types/node': 22.13.10
|
||||||
|
|
||||||
'@types/hast@3.0.4':
|
'@types/hast@3.0.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -6813,7 +6820,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/jsonfile@6.1.4':
|
'@types/jsonfile@6.1.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.13.9
|
'@types/node': 22.13.10
|
||||||
|
|
||||||
'@types/keygrip@1.0.6': {}
|
'@types/keygrip@1.0.6': {}
|
||||||
|
|
||||||
@ -6830,7 +6837,7 @@ snapshots:
|
|||||||
'@types/http-errors': 2.0.4
|
'@types/http-errors': 2.0.4
|
||||||
'@types/keygrip': 1.0.6
|
'@types/keygrip': 1.0.6
|
||||||
'@types/koa-compose': 3.2.8
|
'@types/koa-compose': 3.2.8
|
||||||
'@types/node': 22.13.9
|
'@types/node': 22.13.10
|
||||||
|
|
||||||
'@types/mdast@4.0.4':
|
'@types/mdast@4.0.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -6848,9 +6855,9 @@ snapshots:
|
|||||||
|
|
||||||
'@types/node-forge@1.3.11':
|
'@types/node-forge@1.3.11':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.13.9
|
'@types/node': 22.13.10
|
||||||
|
|
||||||
'@types/node@22.13.9':
|
'@types/node@22.13.10':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.20.0
|
undici-types: 6.20.0
|
||||||
|
|
||||||
@ -6868,19 +6875,19 @@ snapshots:
|
|||||||
|
|
||||||
'@types/s3rver@3.7.4':
|
'@types/s3rver@3.7.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.13.9
|
'@types/node': 22.13.10
|
||||||
|
|
||||||
'@types/semver@7.5.8': {}
|
'@types/semver@7.5.8': {}
|
||||||
|
|
||||||
'@types/send@0.17.4':
|
'@types/send@0.17.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/mime': 1.3.5
|
'@types/mime': 1.3.5
|
||||||
'@types/node': 22.13.9
|
'@types/node': 22.13.10
|
||||||
|
|
||||||
'@types/serve-static@1.15.7':
|
'@types/serve-static@1.15.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/http-errors': 2.0.4
|
'@types/http-errors': 2.0.4
|
||||||
'@types/node': 22.13.9
|
'@types/node': 22.13.10
|
||||||
'@types/send': 0.17.4
|
'@types/send': 0.17.4
|
||||||
|
|
||||||
'@types/sinon-chai@3.2.12':
|
'@types/sinon-chai@3.2.12':
|
||||||
@ -6900,11 +6907,11 @@ snapshots:
|
|||||||
|
|
||||||
'@types/tar-stream@2.2.3':
|
'@types/tar-stream@2.2.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.13.9
|
'@types/node': 22.13.10
|
||||||
|
|
||||||
'@types/through2@2.0.41':
|
'@types/through2@2.0.41':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.13.9
|
'@types/node': 22.13.10
|
||||||
|
|
||||||
'@types/triple-beam@1.3.5': {}
|
'@types/triple-beam@1.3.5': {}
|
||||||
|
|
||||||
@ -6928,18 +6935,18 @@ snapshots:
|
|||||||
|
|
||||||
'@types/whatwg-url@8.2.2':
|
'@types/whatwg-url@8.2.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.13.9
|
'@types/node': 22.13.10
|
||||||
'@types/webidl-conversions': 7.0.3
|
'@types/webidl-conversions': 7.0.3
|
||||||
|
|
||||||
'@types/which@3.0.4': {}
|
'@types/which@3.0.4': {}
|
||||||
|
|
||||||
'@types/ws@7.4.7':
|
'@types/ws@7.4.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.13.9
|
'@types/node': 22.13.10
|
||||||
|
|
||||||
'@types/ws@8.18.0':
|
'@types/ws@8.18.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.13.9
|
'@types/node': 22.13.10
|
||||||
|
|
||||||
'@types/yargs-parser@21.0.3': {}
|
'@types/yargs-parser@21.0.3': {}
|
||||||
|
|
||||||
@ -6949,7 +6956,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/yauzl@2.10.3':
|
'@types/yauzl@2.10.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.13.9
|
'@types/node': 22.13.10
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@ungap/structured-clone@1.3.0': {}
|
'@ungap/structured-clone@1.3.0': {}
|
||||||
@ -7598,7 +7605,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/cookie': 0.4.1
|
'@types/cookie': 0.4.1
|
||||||
'@types/cors': 2.8.17
|
'@types/cors': 2.8.17
|
||||||
'@types/node': 22.13.9
|
'@types/node': 22.13.10
|
||||||
accepts: 1.3.8
|
accepts: 1.3.8
|
||||||
base64id: 2.0.0
|
base64id: 2.0.0
|
||||||
cookie: 0.4.2
|
cookie: 0.4.2
|
||||||
@ -8370,7 +8377,7 @@ snapshots:
|
|||||||
jest-util@29.7.0:
|
jest-util@29.7.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jest/types': 29.6.3
|
'@jest/types': 29.6.3
|
||||||
'@types/node': 22.13.9
|
'@types/node': 22.13.10
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
ci-info: 3.9.0
|
ci-info: 3.9.0
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
|
@ -197,6 +197,52 @@ tap.test('should match wildcard subdomains', async () => {
|
|||||||
expect(result).toEqual(wildcardConfig);
|
expect(result).toEqual(wildcardConfig);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Test TLD wildcards (example.*)
|
||||||
|
tap.test('should match TLD wildcards', async () => {
|
||||||
|
const tldWildcardConfig = createProxyConfig('example.*');
|
||||||
|
router.setNewProxyConfigs([tldWildcardConfig]);
|
||||||
|
|
||||||
|
// Test that example.com matches example.*
|
||||||
|
const req1 = createMockRequest('example.com');
|
||||||
|
const result1 = router.routeReq(req1);
|
||||||
|
expect(result1).toBeTruthy();
|
||||||
|
expect(result1).toEqual(tldWildcardConfig);
|
||||||
|
|
||||||
|
// Test that example.org matches example.*
|
||||||
|
const req2 = createMockRequest('example.org');
|
||||||
|
const result2 = router.routeReq(req2);
|
||||||
|
expect(result2).toBeTruthy();
|
||||||
|
expect(result2).toEqual(tldWildcardConfig);
|
||||||
|
|
||||||
|
// Test that subdomain.example.com doesn't match example.*
|
||||||
|
const req3 = createMockRequest('subdomain.example.com');
|
||||||
|
const result3 = router.routeReq(req3);
|
||||||
|
expect(result3).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test complex pattern matching (*.lossless*)
|
||||||
|
tap.test('should match complex wildcard patterns', async () => {
|
||||||
|
const complexWildcardConfig = createProxyConfig('*.lossless*');
|
||||||
|
router.setNewProxyConfigs([complexWildcardConfig]);
|
||||||
|
|
||||||
|
// Test that sub.lossless.com matches *.lossless*
|
||||||
|
const req1 = createMockRequest('sub.lossless.com');
|
||||||
|
const result1 = router.routeReq(req1);
|
||||||
|
expect(result1).toBeTruthy();
|
||||||
|
expect(result1).toEqual(complexWildcardConfig);
|
||||||
|
|
||||||
|
// Test that api.lossless.org matches *.lossless*
|
||||||
|
const req2 = createMockRequest('api.lossless.org');
|
||||||
|
const result2 = router.routeReq(req2);
|
||||||
|
expect(result2).toBeTruthy();
|
||||||
|
expect(result2).toEqual(complexWildcardConfig);
|
||||||
|
|
||||||
|
// Test that losslessapi.com matches *.lossless*
|
||||||
|
const req3 = createMockRequest('losslessapi.com');
|
||||||
|
const result3 = router.routeReq(req3);
|
||||||
|
expect(result3).toBeUndefined(); // Should not match as it doesn't have a subdomain
|
||||||
|
});
|
||||||
|
|
||||||
// Test default configuration fallback
|
// Test default configuration fallback
|
||||||
tap.test('should fall back to default configuration', async () => {
|
tap.test('should fall back to default configuration', async () => {
|
||||||
const defaultConfig = createProxyConfig('*');
|
const defaultConfig = createProxyConfig('*');
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartproxy',
|
name: '@push.rocks/smartproxy',
|
||||||
version: '3.33.0',
|
version: '3.37.0',
|
||||||
description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, and dynamic routing with authentication options.'
|
description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.'
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import * as plugins from './plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
import { ProxyRouter } from './classes.router.js';
|
import { ProxyRouter } from './classes.router.js';
|
||||||
|
import { AcmeCertManager, CertManagerEvents } from './classes.port80handler.js';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
@ -20,6 +21,18 @@ export interface INetworkProxyOptions {
|
|||||||
// New settings for PortProxy integration
|
// New settings for PortProxy integration
|
||||||
connectionPoolSize?: number; // Maximum connections to maintain in the pool to each backend
|
connectionPoolSize?: number; // Maximum connections to maintain in the pool to each backend
|
||||||
portProxyIntegration?: boolean; // Flag to indicate this proxy is used by PortProxy
|
portProxyIntegration?: boolean; // Flag to indicate this proxy is used by PortProxy
|
||||||
|
|
||||||
|
// ACME certificate management options
|
||||||
|
acme?: {
|
||||||
|
enabled?: boolean; // Whether to enable automatic certificate management
|
||||||
|
port?: number; // Port to listen on for ACME challenges (default: 80)
|
||||||
|
contactEmail?: string; // Email for Let's Encrypt account
|
||||||
|
useProduction?: boolean; // Whether to use Let's Encrypt production (default: false for staging)
|
||||||
|
renewThresholdDays?: number; // Days before expiry to renew certificates (default: 30)
|
||||||
|
autoRenew?: boolean; // Whether to automatically renew certificates (default: true)
|
||||||
|
certificateStore?: string; // Directory to store certificates (default: ./certs)
|
||||||
|
skipConfiguredCerts?: boolean; // Skip domains that already have certificates configured
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IWebSocketWithHeartbeat extends plugins.wsDefault {
|
interface IWebSocketWithHeartbeat extends plugins.wsDefault {
|
||||||
@ -59,6 +72,10 @@ export class NetworkProxy {
|
|||||||
private defaultCertificates: { key: string; cert: string };
|
private defaultCertificates: { key: string; cert: string };
|
||||||
private certificateCache: Map<string, { key: string; cert: string; expires?: Date }> = new Map();
|
private certificateCache: Map<string, { key: string; cert: string; expires?: Date }> = new Map();
|
||||||
|
|
||||||
|
// ACME certificate manager
|
||||||
|
private certManager: AcmeCertManager | null = null;
|
||||||
|
private certificateStoreDir: string;
|
||||||
|
|
||||||
// New connection pool for backend connections
|
// New connection pool for backend connections
|
||||||
private connectionPool: Map<string, Array<{
|
private connectionPool: Map<string, Array<{
|
||||||
socket: plugins.net.Socket;
|
socket: plugins.net.Socket;
|
||||||
@ -66,6 +83,9 @@ export class NetworkProxy {
|
|||||||
isIdle: boolean;
|
isIdle: boolean;
|
||||||
}>> = new Map();
|
}>> = new Map();
|
||||||
|
|
||||||
|
// Track round-robin positions for load balancing
|
||||||
|
private roundRobinPositions: Map<string, number> = new Map();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new NetworkProxy instance
|
* Creates a new NetworkProxy instance
|
||||||
*/
|
*/
|
||||||
@ -85,9 +105,33 @@ export class NetworkProxy {
|
|||||||
},
|
},
|
||||||
// New defaults for PortProxy integration
|
// New defaults for PortProxy integration
|
||||||
connectionPoolSize: optionsArg.connectionPoolSize || 50,
|
connectionPoolSize: optionsArg.connectionPoolSize || 50,
|
||||||
portProxyIntegration: optionsArg.portProxyIntegration || false
|
portProxyIntegration: optionsArg.portProxyIntegration || false,
|
||||||
|
// Default ACME options
|
||||||
|
acme: {
|
||||||
|
enabled: optionsArg.acme?.enabled || false,
|
||||||
|
port: optionsArg.acme?.port || 80,
|
||||||
|
contactEmail: optionsArg.acme?.contactEmail || 'admin@example.com',
|
||||||
|
useProduction: optionsArg.acme?.useProduction || false, // Default to staging for safety
|
||||||
|
renewThresholdDays: optionsArg.acme?.renewThresholdDays || 30,
|
||||||
|
autoRenew: optionsArg.acme?.autoRenew !== false, // Default to true
|
||||||
|
certificateStore: optionsArg.acme?.certificateStore || './certs',
|
||||||
|
skipConfiguredCerts: optionsArg.acme?.skipConfiguredCerts || false
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Set up certificate store directory
|
||||||
|
this.certificateStoreDir = path.resolve(this.options.acme.certificateStore);
|
||||||
|
|
||||||
|
// Ensure certificate store directory exists
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(this.certificateStoreDir)) {
|
||||||
|
fs.mkdirSync(this.certificateStoreDir, { recursive: true });
|
||||||
|
this.log('info', `Created certificate store directory: ${this.certificateStoreDir}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.log('warn', `Failed to create certificate store directory: ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
this.loadDefaultCertificates();
|
this.loadDefaultCertificates();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -330,17 +374,230 @@ export class NetworkProxy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the ACME certificate manager for automatic certificate issuance
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private async initializeAcmeManager(): Promise<void> {
|
||||||
|
if (!this.options.acme.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create certificate manager
|
||||||
|
this.certManager = new AcmeCertManager({
|
||||||
|
port: this.options.acme.port,
|
||||||
|
contactEmail: this.options.acme.contactEmail,
|
||||||
|
useProduction: this.options.acme.useProduction,
|
||||||
|
renewThresholdDays: this.options.acme.renewThresholdDays,
|
||||||
|
httpsRedirectPort: this.options.port, // Redirect to our HTTPS port
|
||||||
|
renewCheckIntervalHours: 24 // Check daily for renewals
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register event handlers
|
||||||
|
this.certManager.on(CertManagerEvents.CERTIFICATE_ISSUED, this.handleCertificateIssued.bind(this));
|
||||||
|
this.certManager.on(CertManagerEvents.CERTIFICATE_RENEWED, this.handleCertificateIssued.bind(this));
|
||||||
|
this.certManager.on(CertManagerEvents.CERTIFICATE_FAILED, this.handleCertificateFailed.bind(this));
|
||||||
|
this.certManager.on(CertManagerEvents.CERTIFICATE_EXPIRING, (data) => {
|
||||||
|
this.log('info', `Certificate for ${data.domain} expires in ${data.daysRemaining} days`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start the manager
|
||||||
|
try {
|
||||||
|
await this.certManager.start();
|
||||||
|
this.log('info', `ACME Certificate Manager started on port ${this.options.acme.port}`);
|
||||||
|
|
||||||
|
// Add domains from proxy configs
|
||||||
|
this.registerDomainsWithAcmeManager();
|
||||||
|
} catch (error) {
|
||||||
|
this.log('error', `Failed to start ACME Certificate Manager: ${error}`);
|
||||||
|
this.certManager = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers domains from proxy configs with the ACME manager
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private registerDomainsWithAcmeManager(): void {
|
||||||
|
if (!this.certManager) return;
|
||||||
|
|
||||||
|
// Get all hostnames from proxy configs
|
||||||
|
this.proxyConfigs.forEach(config => {
|
||||||
|
const hostname = config.hostName;
|
||||||
|
|
||||||
|
// Skip wildcard domains - can't get certs for these with HTTP-01 validation
|
||||||
|
if (hostname.includes('*')) {
|
||||||
|
this.log('info', `Skipping wildcard domain for ACME: ${hostname}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip domains already with certificates if configured to do so
|
||||||
|
if (this.options.acme.skipConfiguredCerts) {
|
||||||
|
const cachedCert = this.certificateCache.get(hostname);
|
||||||
|
if (cachedCert) {
|
||||||
|
this.log('info', `Skipping domain with existing certificate: ${hostname}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for existing certificate in the store
|
||||||
|
const certPath = path.join(this.certificateStoreDir, `${hostname}.cert.pem`);
|
||||||
|
const keyPath = path.join(this.certificateStoreDir, `${hostname}.key.pem`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(certPath) && fs.existsSync(keyPath)) {
|
||||||
|
// Load existing certificate and key
|
||||||
|
const cert = fs.readFileSync(certPath, 'utf8');
|
||||||
|
const key = fs.readFileSync(keyPath, 'utf8');
|
||||||
|
|
||||||
|
// Extract expiry date from certificate if possible
|
||||||
|
let expiryDate: Date | undefined;
|
||||||
|
try {
|
||||||
|
const matches = cert.match(/Not After\s*:\s*(.*?)(?:\n|$)/i);
|
||||||
|
if (matches && matches[1]) {
|
||||||
|
expiryDate = new Date(matches[1]);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.log('warn', `Failed to extract expiry date from certificate for ${hostname}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the certificate in the manager
|
||||||
|
this.certManager.setCertificate(hostname, cert, key, expiryDate);
|
||||||
|
|
||||||
|
// Also update our own certificate cache
|
||||||
|
this.updateCertificateCache(hostname, cert, key, expiryDate);
|
||||||
|
|
||||||
|
this.log('info', `Loaded existing certificate for ${hostname}`);
|
||||||
|
} else {
|
||||||
|
// Register the domain for certificate issuance
|
||||||
|
this.certManager.addDomain(hostname);
|
||||||
|
this.log('info', `Registered domain for ACME certificate issuance: ${hostname}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.log('error', `Error registering domain ${hostname} with ACME manager: ${error}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles newly issued or renewed certificates from ACME manager
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private handleCertificateIssued(data: { domain: string; certificate: string; privateKey: string; expiryDate: Date }): void {
|
||||||
|
const { domain, certificate, privateKey, expiryDate } = data;
|
||||||
|
|
||||||
|
this.log('info', `Certificate ${this.certificateCache.has(domain) ? 'renewed' : 'issued'} for ${domain}, valid until ${expiryDate.toISOString()}`);
|
||||||
|
|
||||||
|
// Update certificate in HTTPS server
|
||||||
|
this.updateCertificateCache(domain, certificate, privateKey, expiryDate);
|
||||||
|
|
||||||
|
// Save the certificate to the filesystem
|
||||||
|
this.saveCertificateToStore(domain, certificate, privateKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles certificate issuance failures
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private handleCertificateFailed(data: { domain: string; error: string }): void {
|
||||||
|
this.log('error', `Certificate issuance failed for ${data.domain}: ${data.error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves certificate and private key to the filesystem
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private saveCertificateToStore(domain: string, certificate: string, privateKey: string): void {
|
||||||
|
try {
|
||||||
|
const certPath = path.join(this.certificateStoreDir, `${domain}.cert.pem`);
|
||||||
|
const keyPath = path.join(this.certificateStoreDir, `${domain}.key.pem`);
|
||||||
|
|
||||||
|
fs.writeFileSync(certPath, certificate);
|
||||||
|
fs.writeFileSync(keyPath, privateKey);
|
||||||
|
|
||||||
|
// Ensure private key has restricted permissions
|
||||||
|
try {
|
||||||
|
fs.chmodSync(keyPath, 0o600);
|
||||||
|
} catch (error) {
|
||||||
|
this.log('warn', `Failed to set permissions on private key for ${domain}: ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log('info', `Saved certificate for ${domain} to ${certPath}`);
|
||||||
|
} catch (error) {
|
||||||
|
this.log('error', `Failed to save certificate for ${domain}: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles SNI (Server Name Indication) for TLS connections
|
||||||
|
* Used by the HTTPS server to select the correct certificate for each domain
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private handleSNI(domain: string, cb: (err: Error | null, ctx: plugins.tls.SecureContext) => void): void {
|
||||||
|
this.log('debug', `SNI request for domain: ${domain}`);
|
||||||
|
|
||||||
|
// Check if we have a certificate for this domain
|
||||||
|
const certs = this.certificateCache.get(domain);
|
||||||
|
|
||||||
|
if (certs) {
|
||||||
|
try {
|
||||||
|
// Create TLS context with the cached certificate
|
||||||
|
const context = plugins.tls.createSecureContext({
|
||||||
|
key: certs.key,
|
||||||
|
cert: certs.cert
|
||||||
|
});
|
||||||
|
|
||||||
|
this.log('debug', `Using cached certificate for ${domain}`);
|
||||||
|
cb(null, context);
|
||||||
|
return;
|
||||||
|
} catch (err) {
|
||||||
|
this.log('error', `Error creating secure context for ${domain}:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we should trigger certificate issuance
|
||||||
|
if (this.options.acme?.enabled && this.certManager && !domain.includes('*')) {
|
||||||
|
// Check if this domain is already registered
|
||||||
|
const certData = this.certManager.getCertificate(domain);
|
||||||
|
|
||||||
|
if (!certData) {
|
||||||
|
this.log('info', `No certificate found for ${domain}, registering for issuance`);
|
||||||
|
this.certManager.addDomain(domain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to default certificate
|
||||||
|
try {
|
||||||
|
const context = plugins.tls.createSecureContext({
|
||||||
|
key: this.defaultCertificates.key,
|
||||||
|
cert: this.defaultCertificates.cert
|
||||||
|
});
|
||||||
|
|
||||||
|
this.log('debug', `Using default certificate for ${domain}`);
|
||||||
|
cb(null, context);
|
||||||
|
} catch (err) {
|
||||||
|
this.log('error', `Error creating default secure context:`, err);
|
||||||
|
cb(new Error('Cannot create secure context'), null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the proxy server
|
* Starts the proxy server
|
||||||
*/
|
*/
|
||||||
public async start(): Promise<void> {
|
public async start(): Promise<void> {
|
||||||
this.startTime = Date.now();
|
this.startTime = Date.now();
|
||||||
|
|
||||||
|
// Initialize ACME certificate manager if enabled
|
||||||
|
if (this.options.acme.enabled) {
|
||||||
|
await this.initializeAcmeManager();
|
||||||
|
}
|
||||||
|
|
||||||
// Create the HTTPS server
|
// Create the HTTPS server
|
||||||
this.httpsServer = plugins.https.createServer(
|
this.httpsServer = plugins.https.createServer(
|
||||||
{
|
{
|
||||||
key: this.defaultCertificates.key,
|
key: this.defaultCertificates.key,
|
||||||
cert: this.defaultCertificates.cert
|
cert: this.defaultCertificates.cert,
|
||||||
|
SNICallback: (domain, cb) => this.handleSNI(domain, cb)
|
||||||
},
|
},
|
||||||
(req, res) => this.handleRequest(req, res)
|
(req, res) => this.handleRequest(req, res)
|
||||||
);
|
);
|
||||||
@ -556,7 +813,10 @@ export class NetworkProxy {
|
|||||||
const outGoingDeferred = plugins.smartpromise.defer();
|
const outGoingDeferred = plugins.smartpromise.defer();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const wsTarget = `ws://${wsDestinationConfig.destinationIp}:${wsDestinationConfig.destinationPort}${reqArg.url}`;
|
// Select destination IP and port for WebSocket
|
||||||
|
const wsDestinationIp = this.selectDestinationIp(wsDestinationConfig);
|
||||||
|
const wsDestinationPort = this.selectDestinationPort(wsDestinationConfig);
|
||||||
|
const wsTarget = `ws://${wsDestinationIp}:${wsDestinationPort}${reqArg.url}`;
|
||||||
this.log('debug', `Proxying WebSocket to ${wsTarget}`);
|
this.log('debug', `Proxying WebSocket to ${wsTarget}`);
|
||||||
|
|
||||||
wsOutgoing = new plugins.wsDefault(wsTarget);
|
wsOutgoing = new plugins.wsDefault(wsTarget);
|
||||||
@ -688,8 +948,12 @@ export class NetworkProxy {
|
|||||||
const useConnectionPool = this.options.portProxyIntegration &&
|
const useConnectionPool = this.options.portProxyIntegration &&
|
||||||
originRequest.socket.remoteAddress?.includes('127.0.0.1');
|
originRequest.socket.remoteAddress?.includes('127.0.0.1');
|
||||||
|
|
||||||
|
// Select destination IP and port from the arrays
|
||||||
|
const destinationIp = this.selectDestinationIp(destinationConfig);
|
||||||
|
const destinationPort = this.selectDestinationPort(destinationConfig);
|
||||||
|
|
||||||
// Construct destination URL
|
// Construct destination URL
|
||||||
const destinationUrl = `http://${destinationConfig.destinationIp}:${destinationConfig.destinationPort}${originRequest.url}`;
|
const destinationUrl = `http://${destinationIp}:${destinationPort}${originRequest.url}`;
|
||||||
|
|
||||||
if (useConnectionPool) {
|
if (useConnectionPool) {
|
||||||
this.log('debug', `[${reqId}] Proxying to ${destinationUrl} (using connection pool)`);
|
this.log('debug', `[${reqId}] Proxying to ${destinationUrl} (using connection pool)`);
|
||||||
@ -697,8 +961,8 @@ export class NetworkProxy {
|
|||||||
reqId,
|
reqId,
|
||||||
originRequest,
|
originRequest,
|
||||||
originResponse,
|
originResponse,
|
||||||
destinationConfig.destinationIp,
|
destinationIp,
|
||||||
destinationConfig.destinationPort,
|
destinationPort,
|
||||||
originRequest.url
|
originRequest.url
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -1084,6 +1348,80 @@ export class NetworkProxy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects a destination IP from the array using round-robin
|
||||||
|
* @param config The proxy configuration
|
||||||
|
* @returns A destination IP address
|
||||||
|
*/
|
||||||
|
private selectDestinationIp(config: plugins.tsclass.network.IReverseProxyConfig): string {
|
||||||
|
// For array-based configs
|
||||||
|
if (Array.isArray(config.destinationIps) && config.destinationIps.length > 0) {
|
||||||
|
// Get the current position or initialize it
|
||||||
|
const key = `ip_${config.hostName}`;
|
||||||
|
let position = this.roundRobinPositions.get(key) || 0;
|
||||||
|
|
||||||
|
// Select the IP using round-robin
|
||||||
|
const selectedIp = config.destinationIps[position];
|
||||||
|
|
||||||
|
// Update the position for next time
|
||||||
|
position = (position + 1) % config.destinationIps.length;
|
||||||
|
this.roundRobinPositions.set(key, position);
|
||||||
|
|
||||||
|
return selectedIp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For backward compatibility with test suites that rely on specific behavior
|
||||||
|
// Check if there's a proxyConfigs entry that matches this hostname
|
||||||
|
const matchingConfig = this.proxyConfigs.find(cfg =>
|
||||||
|
cfg.hostName === config.hostName &&
|
||||||
|
(cfg as any).destinationIp
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matchingConfig) {
|
||||||
|
return (matchingConfig as any).destinationIp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to localhost
|
||||||
|
return 'localhost';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects a destination port from the array using round-robin
|
||||||
|
* @param config The proxy configuration
|
||||||
|
* @returns A destination port number
|
||||||
|
*/
|
||||||
|
private selectDestinationPort(config: plugins.tsclass.network.IReverseProxyConfig): number {
|
||||||
|
// For array-based configs
|
||||||
|
if (Array.isArray(config.destinationPorts) && config.destinationPorts.length > 0) {
|
||||||
|
// Get the current position or initialize it
|
||||||
|
const key = `port_${config.hostName}`;
|
||||||
|
let position = this.roundRobinPositions.get(key) || 0;
|
||||||
|
|
||||||
|
// Select the port using round-robin
|
||||||
|
const selectedPort = config.destinationPorts[position];
|
||||||
|
|
||||||
|
// Update the position for next time
|
||||||
|
position = (position + 1) % config.destinationPorts.length;
|
||||||
|
this.roundRobinPositions.set(key, position);
|
||||||
|
|
||||||
|
return selectedPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For backward compatibility with test suites that rely on specific behavior
|
||||||
|
// Check if there's a proxyConfigs entry that matches this hostname
|
||||||
|
const matchingConfig = this.proxyConfigs.find(cfg =>
|
||||||
|
cfg.hostName === config.hostName &&
|
||||||
|
(cfg as any).destinationPort
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matchingConfig) {
|
||||||
|
return parseInt((matchingConfig as any).destinationPort, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to port 80
|
||||||
|
return 80;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates proxy configurations
|
* Updates proxy configurations
|
||||||
*/
|
*/
|
||||||
@ -1144,6 +1482,48 @@ export class NetworkProxy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts PortProxy domain configurations to NetworkProxy configs
|
||||||
|
* @param domainConfigs PortProxy domain configs
|
||||||
|
* @param sslKeyPair Default SSL key pair to use if not specified
|
||||||
|
* @returns Array of NetworkProxy configs
|
||||||
|
*/
|
||||||
|
public convertPortProxyConfigs(
|
||||||
|
domainConfigs: Array<{
|
||||||
|
domains: string[];
|
||||||
|
targetIPs?: string[];
|
||||||
|
allowedIPs?: string[];
|
||||||
|
}>,
|
||||||
|
sslKeyPair?: { key: string; cert: string }
|
||||||
|
): plugins.tsclass.network.IReverseProxyConfig[] {
|
||||||
|
const proxyConfigs: plugins.tsclass.network.IReverseProxyConfig[] = [];
|
||||||
|
|
||||||
|
// Use default certificates if not provided
|
||||||
|
const sslKey = sslKeyPair?.key || this.defaultCertificates.key;
|
||||||
|
const sslCert = sslKeyPair?.cert || this.defaultCertificates.cert;
|
||||||
|
|
||||||
|
for (const domainConfig of domainConfigs) {
|
||||||
|
// Each domain in the domains array gets its own config
|
||||||
|
for (const domain of domainConfig.domains) {
|
||||||
|
// Skip non-hostname patterns (like IP addresses)
|
||||||
|
if (domain.match(/^\d+\.\d+\.\d+\.\d+$/) || domain === '*' || domain === 'localhost') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyConfigs.push({
|
||||||
|
hostName: domain,
|
||||||
|
destinationIps: domainConfig.targetIPs || ['localhost'],
|
||||||
|
destinationPorts: [this.options.port], // Use the NetworkProxy port
|
||||||
|
privateKey: sslKey,
|
||||||
|
publicKey: sslCert
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log('info', `Converted ${domainConfigs.length} PortProxy configs to ${proxyConfigs.length} NetworkProxy configs`);
|
||||||
|
return proxyConfigs;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds default headers to be included in all responses
|
* Adds default headers to be included in all responses
|
||||||
*/
|
*/
|
||||||
@ -1208,6 +1588,16 @@ export class NetworkProxy {
|
|||||||
}
|
}
|
||||||
this.connectionPool.clear();
|
this.connectionPool.clear();
|
||||||
|
|
||||||
|
// Stop ACME certificate manager if it's running
|
||||||
|
if (this.certManager) {
|
||||||
|
try {
|
||||||
|
await this.certManager.stop();
|
||||||
|
this.log('info', 'ACME Certificate Manager stopped');
|
||||||
|
} catch (error) {
|
||||||
|
this.log('error', 'Error stopping ACME Certificate Manager', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Close the HTTPS server
|
// Close the HTTPS server
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
this.httpsServer.close(() => {
|
this.httpsServer.close(() => {
|
||||||
@ -1217,6 +1607,71 @@ export class NetworkProxy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests a new certificate for a domain
|
||||||
|
* This can be used to manually trigger certificate issuance
|
||||||
|
* @param domain The domain to request a certificate for
|
||||||
|
* @returns A promise that resolves when the request is submitted (not when the certificate is issued)
|
||||||
|
*/
|
||||||
|
public async requestCertificate(domain: string): Promise<boolean> {
|
||||||
|
if (!this.options.acme.enabled) {
|
||||||
|
this.log('warn', 'ACME certificate management is not enabled');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.certManager) {
|
||||||
|
this.log('error', 'ACME certificate manager is not initialized');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip wildcard domains - can't get certs for these with HTTP-01 validation
|
||||||
|
if (domain.includes('*')) {
|
||||||
|
this.log('error', `Cannot request certificate for wildcard domain: ${domain}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.certManager.addDomain(domain);
|
||||||
|
this.log('info', `Certificate request submitted for domain: ${domain}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
this.log('error', `Error requesting certificate for domain ${domain}:`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the certificate cache for a domain
|
||||||
|
* @param domain The domain name
|
||||||
|
* @param certificate The certificate (PEM format)
|
||||||
|
* @param privateKey The private key (PEM format)
|
||||||
|
* @param expiryDate Optional expiry date
|
||||||
|
*/
|
||||||
|
private updateCertificateCache(domain: string, certificate: string, privateKey: string, expiryDate?: Date): void {
|
||||||
|
// Update certificate context in HTTPS server if it's running
|
||||||
|
if (this.httpsServer) {
|
||||||
|
try {
|
||||||
|
this.httpsServer.addContext(domain, {
|
||||||
|
key: privateKey,
|
||||||
|
cert: certificate
|
||||||
|
});
|
||||||
|
this.log('debug', `Updated SSL context for domain: ${domain}`);
|
||||||
|
} catch (error) {
|
||||||
|
this.log('error', `Error updating SSL context for domain ${domain}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update certificate in cache
|
||||||
|
this.certificateCache.set(domain, {
|
||||||
|
key: privateKey,
|
||||||
|
cert: certificate,
|
||||||
|
expires: expiryDate
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add to active contexts set
|
||||||
|
this.activeContexts.add(domain);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs a message according to the configured log level
|
* Logs a message according to the configured log level
|
||||||
*/
|
*/
|
||||||
|
@ -10,10 +10,6 @@ export interface IDomainConfig {
|
|||||||
portRanges?: Array<{ from: number; to: number }>; // Optional port ranges
|
portRanges?: Array<{ from: number; to: number }>; // Optional port ranges
|
||||||
// Allow domain-specific timeout override
|
// Allow domain-specific timeout override
|
||||||
connectionTimeout?: number; // Connection timeout override (ms)
|
connectionTimeout?: number; // Connection timeout override (ms)
|
||||||
|
|
||||||
// New properties for NetworkProxy integration
|
|
||||||
useNetworkProxy?: boolean; // When true, forwards TLS connections to NetworkProxy
|
|
||||||
networkProxyIndex?: number; // Optional index to specify which NetworkProxy to use (defaults to 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Port proxy settings including global allowed port ranges */
|
/** Port proxy settings including global allowed port ranges */
|
||||||
@ -60,13 +56,21 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
|
|||||||
keepAliveInactivityMultiplier?: number; // Multiplier for inactivity timeout for keep-alive connections
|
keepAliveInactivityMultiplier?: number; // Multiplier for inactivity timeout for keep-alive connections
|
||||||
extendedKeepAliveLifetime?: number; // Extended lifetime for keep-alive connections (ms)
|
extendedKeepAliveLifetime?: number; // Extended lifetime for keep-alive connections (ms)
|
||||||
|
|
||||||
// New property for NetworkProxy integration
|
// NetworkProxy integration
|
||||||
networkProxies?: NetworkProxy[]; // Array of NetworkProxy instances to use for TLS termination
|
useNetworkProxy?: number[]; // Array of ports to forward to NetworkProxy
|
||||||
|
networkProxyPort?: number; // Port where NetworkProxy is listening (default: 8443)
|
||||||
|
|
||||||
// Browser optimization settings
|
// ACME certificate management options
|
||||||
browserFriendlyMode?: boolean; // Optimizes handling for browser connections
|
acme?: {
|
||||||
allowRenegotiationWithDifferentSNI?: boolean; // Allows SNI changes during renegotiation
|
enabled?: boolean; // Whether to enable automatic certificate management
|
||||||
relatedDomainPatterns?: string[][]; // Patterns for domains that should be allowed to share connections
|
port?: number; // Port to listen on for ACME challenges (default: 80)
|
||||||
|
contactEmail?: string; // Email for Let's Encrypt account
|
||||||
|
useProduction?: boolean; // Whether to use Let's Encrypt production (default: false for staging)
|
||||||
|
renewThresholdDays?: number; // Days before expiry to renew certificates (default: 30)
|
||||||
|
autoRenew?: boolean; // Whether to automatically renew certificates (default: true)
|
||||||
|
certificateStore?: string; // Directory to store certificates (default: ./certs)
|
||||||
|
skipConfiguredCerts?: boolean; // Skip domains that already have certificates configured
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -102,11 +106,10 @@ interface IConnectionRecord {
|
|||||||
incomingTerminationReason?: string | null; // Reason for incoming termination
|
incomingTerminationReason?: string | null; // Reason for incoming termination
|
||||||
outgoingTerminationReason?: string | null; // Reason for outgoing termination
|
outgoingTerminationReason?: string | null; // Reason for outgoing termination
|
||||||
|
|
||||||
// New field for NetworkProxy tracking
|
// NetworkProxy tracking
|
||||||
usingNetworkProxy?: boolean; // Whether this connection is using a NetworkProxy
|
usingNetworkProxy?: boolean; // Whether this connection is using a NetworkProxy
|
||||||
networkProxyIndex?: number; // Which NetworkProxy instance is being used
|
|
||||||
|
|
||||||
// New field for renegotiation handler
|
// Renegotiation handler
|
||||||
renegotiationHandler?: (chunk: Buffer) => void; // Handler for renegotiation detection
|
renegotiationHandler?: (chunk: Buffer) => void; // Handler for renegotiation detection
|
||||||
|
|
||||||
// Browser connection tracking
|
// Browser connection tracking
|
||||||
@ -301,35 +304,6 @@ function isClientHello(buffer: Buffer): boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if two domains are related based on configured patterns
|
|
||||||
* @param domain1 - First domain name
|
|
||||||
* @param domain2 - Second domain name
|
|
||||||
* @param relatedPatterns - Array of domain pattern groups where domains in the same group are considered related
|
|
||||||
* @returns true if domains are related, false otherwise
|
|
||||||
*/
|
|
||||||
function areDomainsRelated(
|
|
||||||
domain1: string,
|
|
||||||
domain2: string,
|
|
||||||
relatedPatterns?: string[][]
|
|
||||||
): boolean {
|
|
||||||
// Only exact same domains or empty domains are automatically related
|
|
||||||
if (!domain1 || !domain2 || domain1 === domain2) return true;
|
|
||||||
|
|
||||||
// Check against configured related domain patterns - the ONLY source of truth
|
|
||||||
if (relatedPatterns && relatedPatterns.length > 0) {
|
|
||||||
for (const patternGroup of relatedPatterns) {
|
|
||||||
const domain1Matches = patternGroup.some((pattern) => plugins.minimatch(domain1, pattern));
|
|
||||||
const domain2Matches = patternGroup.some((pattern) => plugins.minimatch(domain2, pattern));
|
|
||||||
|
|
||||||
if (domain1Matches && domain2Matches) return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no patterns match, domains are not related
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: Check if a port falls within any of the given port ranges
|
// Helper: Check if a port falls within any of the given port ranges
|
||||||
const isPortInRanges = (port: number, ranges: Array<{ from: number; to: number }>): boolean => {
|
const isPortInRanges = (port: number, ranges: Array<{ from: number; to: number }>): boolean => {
|
||||||
return ranges.some((range) => port >= range.from && port <= range.to);
|
return ranges.some((range) => port >= range.from && port <= range.to);
|
||||||
@ -413,8 +387,8 @@ export class PortProxy {
|
|||||||
private connectionsByIP: Map<string, Set<string>> = new Map();
|
private connectionsByIP: Map<string, Set<string>> = new Map();
|
||||||
private connectionRateByIP: Map<string, number[]> = new Map();
|
private connectionRateByIP: Map<string, number[]> = new Map();
|
||||||
|
|
||||||
// New property to store NetworkProxy instances
|
// NetworkProxy instance for TLS termination
|
||||||
private networkProxies: NetworkProxy[] = [];
|
private networkProxy: NetworkProxy | null = null;
|
||||||
|
|
||||||
constructor(settingsArg: IPortProxySettings) {
|
constructor(settingsArg: IPortProxySettings) {
|
||||||
// Set reasonable defaults for all settings
|
// Set reasonable defaults for all settings
|
||||||
@ -434,34 +408,228 @@ export class PortProxy {
|
|||||||
// Socket optimization settings
|
// Socket optimization settings
|
||||||
noDelay: settingsArg.noDelay !== undefined ? settingsArg.noDelay : true,
|
noDelay: settingsArg.noDelay !== undefined ? settingsArg.noDelay : true,
|
||||||
keepAlive: settingsArg.keepAlive !== undefined ? settingsArg.keepAlive : true,
|
keepAlive: settingsArg.keepAlive !== undefined ? settingsArg.keepAlive : true,
|
||||||
keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 10000, // 10 seconds (reduced for responsiveness)
|
keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 10000, // 10 seconds
|
||||||
maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024, // 10MB to handle large TLS handshakes
|
maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024, // 10MB
|
||||||
|
|
||||||
// Feature flags
|
// Feature flags
|
||||||
disableInactivityCheck: settingsArg.disableInactivityCheck || false,
|
disableInactivityCheck: settingsArg.disableInactivityCheck || false,
|
||||||
enableKeepAliveProbes:
|
enableKeepAliveProbes: settingsArg.enableKeepAliveProbes !== undefined
|
||||||
settingsArg.enableKeepAliveProbes !== undefined ? settingsArg.enableKeepAliveProbes : true, // Enable by default
|
? settingsArg.enableKeepAliveProbes : true,
|
||||||
enableDetailedLogging: settingsArg.enableDetailedLogging || false,
|
enableDetailedLogging: settingsArg.enableDetailedLogging || false,
|
||||||
enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
|
enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
|
||||||
enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false, // Disable randomization by default
|
enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false,
|
||||||
|
|
||||||
// Rate limiting defaults
|
// Rate limiting defaults
|
||||||
maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100, // 100 connections per IP
|
maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100,
|
||||||
connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300, // 300 per minute
|
connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300,
|
||||||
|
|
||||||
// Enhanced keep-alive settings
|
// Enhanced keep-alive settings
|
||||||
keepAliveTreatment: settingsArg.keepAliveTreatment || 'extended', // Extended by default
|
keepAliveTreatment: settingsArg.keepAliveTreatment || 'extended',
|
||||||
keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6, // 6x normal inactivity timeout
|
keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6,
|
||||||
extendedKeepAliveLifetime: settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000, // 7 days
|
extendedKeepAliveLifetime: settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000, // 7 days
|
||||||
|
|
||||||
// Browser optimization settings (new)
|
// NetworkProxy settings
|
||||||
browserFriendlyMode: settingsArg.browserFriendlyMode || true, // On by default
|
networkProxyPort: settingsArg.networkProxyPort || 8443, // Default NetworkProxy port
|
||||||
allowRenegotiationWithDifferentSNI: settingsArg.allowRenegotiationWithDifferentSNI || false, // Off by default
|
|
||||||
relatedDomainPatterns: settingsArg.relatedDomainPatterns || [], // Empty by default
|
// ACME certificate settings with reasonable defaults
|
||||||
|
acme: settingsArg.acme || {
|
||||||
|
enabled: false,
|
||||||
|
port: 80,
|
||||||
|
contactEmail: 'admin@example.com',
|
||||||
|
useProduction: false,
|
||||||
|
renewThresholdDays: 30,
|
||||||
|
autoRenew: true,
|
||||||
|
certificateStore: './certs',
|
||||||
|
skipConfiguredCerts: false
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store NetworkProxy instances if provided
|
// Initialize NetworkProxy if enabled
|
||||||
this.networkProxies = settingsArg.networkProxies || [];
|
if (this.settings.useNetworkProxy && this.settings.useNetworkProxy.length > 0) {
|
||||||
|
this.initializeNetworkProxy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize NetworkProxy instance
|
||||||
|
*/
|
||||||
|
private async initializeNetworkProxy(): Promise<void> {
|
||||||
|
if (!this.networkProxy) {
|
||||||
|
// Configure NetworkProxy options based on PortProxy settings
|
||||||
|
const networkProxyOptions: any = {
|
||||||
|
port: this.settings.networkProxyPort!,
|
||||||
|
portProxyIntegration: true,
|
||||||
|
logLevel: this.settings.enableDetailedLogging ? 'debug' : 'info'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add ACME settings if configured
|
||||||
|
if (this.settings.acme) {
|
||||||
|
networkProxyOptions.acme = { ...this.settings.acme };
|
||||||
|
}
|
||||||
|
|
||||||
|
this.networkProxy = new NetworkProxy(networkProxyOptions);
|
||||||
|
|
||||||
|
console.log(`Initialized NetworkProxy on port ${this.settings.networkProxyPort}`);
|
||||||
|
|
||||||
|
// Convert and apply domain configurations to NetworkProxy
|
||||||
|
await this.syncDomainConfigsToNetworkProxy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the domain configurations for the proxy
|
||||||
|
* @param newDomainConfigs The new domain configurations
|
||||||
|
*/
|
||||||
|
public async updateDomainConfigs(newDomainConfigs: IDomainConfig[]): Promise<void> {
|
||||||
|
console.log(`Updating domain configurations (${newDomainConfigs.length} configs)`);
|
||||||
|
this.settings.domainConfigs = newDomainConfigs;
|
||||||
|
|
||||||
|
// If NetworkProxy is initialized, resync the configurations
|
||||||
|
if (this.networkProxy) {
|
||||||
|
await this.syncDomainConfigsToNetworkProxy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the ACME certificate settings
|
||||||
|
* @param acmeSettings New ACME settings
|
||||||
|
*/
|
||||||
|
public async updateAcmeSettings(acmeSettings: IPortProxySettings['acme']): Promise<void> {
|
||||||
|
console.log('Updating ACME certificate settings');
|
||||||
|
|
||||||
|
// Update settings
|
||||||
|
this.settings.acme = {
|
||||||
|
...this.settings.acme,
|
||||||
|
...acmeSettings
|
||||||
|
};
|
||||||
|
|
||||||
|
// If NetworkProxy is initialized, update its ACME settings
|
||||||
|
if (this.networkProxy) {
|
||||||
|
try {
|
||||||
|
// Recreate NetworkProxy with new settings if ACME enabled state has changed
|
||||||
|
if (this.settings.acme.enabled !== acmeSettings.enabled) {
|
||||||
|
console.log(`ACME enabled state changed to: ${acmeSettings.enabled}`);
|
||||||
|
|
||||||
|
// Stop the current NetworkProxy
|
||||||
|
await this.networkProxy.stop();
|
||||||
|
this.networkProxy = null;
|
||||||
|
|
||||||
|
// Reinitialize with new settings
|
||||||
|
await this.initializeNetworkProxy();
|
||||||
|
|
||||||
|
// Use start() to make sure ACME gets initialized if newly enabled
|
||||||
|
await this.networkProxy.start();
|
||||||
|
} else {
|
||||||
|
// Update existing NetworkProxy with new settings
|
||||||
|
// Note: Some settings may require a restart to take effect
|
||||||
|
console.log('Updating ACME settings in NetworkProxy');
|
||||||
|
|
||||||
|
// For certificate renewals, we might want to trigger checks with the new settings
|
||||||
|
if (acmeSettings.renewThresholdDays) {
|
||||||
|
console.log(`Setting new renewal threshold to ${acmeSettings.renewThresholdDays} days`);
|
||||||
|
// This is implementation-dependent but gives an example
|
||||||
|
if (this.networkProxy.options.acme) {
|
||||||
|
this.networkProxy.options.acme.renewThresholdDays = acmeSettings.renewThresholdDays;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`Error updating ACME settings: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronizes PortProxy domain configurations to NetworkProxy
|
||||||
|
* This allows domains configured in PortProxy to be used by NetworkProxy
|
||||||
|
*/
|
||||||
|
private async syncDomainConfigsToNetworkProxy(): Promise<void> {
|
||||||
|
if (!this.networkProxy) {
|
||||||
|
console.log('Cannot sync configurations - NetworkProxy not initialized');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get SSL certificates from assets
|
||||||
|
// Import fs directly since it's not in plugins
|
||||||
|
const fs = await import('fs');
|
||||||
|
|
||||||
|
let certPair;
|
||||||
|
try {
|
||||||
|
certPair = {
|
||||||
|
key: fs.readFileSync('assets/certs/key.pem', 'utf8'),
|
||||||
|
cert: fs.readFileSync('assets/certs/cert.pem', 'utf8')
|
||||||
|
};
|
||||||
|
} catch (certError) {
|
||||||
|
console.log(`Warning: Could not read default certificates: ${certError}`);
|
||||||
|
console.log('Using empty certificate placeholders - ACME will generate proper certificates if enabled');
|
||||||
|
|
||||||
|
// Use empty placeholders - NetworkProxy will use its internal defaults
|
||||||
|
// or ACME will generate proper ones if enabled
|
||||||
|
certPair = {
|
||||||
|
key: '',
|
||||||
|
cert: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert domain configs to NetworkProxy configs
|
||||||
|
const proxyConfigs = this.networkProxy.convertPortProxyConfigs(
|
||||||
|
this.settings.domainConfigs,
|
||||||
|
certPair
|
||||||
|
);
|
||||||
|
|
||||||
|
// Log ACME-eligible domains if ACME is enabled
|
||||||
|
if (this.settings.acme?.enabled) {
|
||||||
|
const acmeEligibleDomains = proxyConfigs
|
||||||
|
.filter(config => !config.hostName.includes('*')) // Exclude wildcards
|
||||||
|
.map(config => config.hostName);
|
||||||
|
|
||||||
|
if (acmeEligibleDomains.length > 0) {
|
||||||
|
console.log(`Domains eligible for ACME certificates: ${acmeEligibleDomains.join(', ')}`);
|
||||||
|
} else {
|
||||||
|
console.log('No domains eligible for ACME certificates found in configuration');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update NetworkProxy with the converted configs
|
||||||
|
this.networkProxy.updateProxyConfigs(proxyConfigs).then(() => {
|
||||||
|
console.log(`Successfully synchronized ${proxyConfigs.length} domain configurations to NetworkProxy`);
|
||||||
|
}).catch(err => {
|
||||||
|
console.log(`Error synchronizing configurations: ${err.message}`);
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`Failed to sync configurations: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests a certificate for a specific domain
|
||||||
|
* @param domain The domain to request a certificate for
|
||||||
|
* @returns Promise that resolves to true if the request was successful, false otherwise
|
||||||
|
*/
|
||||||
|
public async requestCertificate(domain: string): Promise<boolean> {
|
||||||
|
if (!this.networkProxy) {
|
||||||
|
console.log('Cannot request certificate - NetworkProxy not initialized');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.settings.acme?.enabled) {
|
||||||
|
console.log('Cannot request certificate - ACME is not enabled');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.networkProxy.requestCertificate(domain);
|
||||||
|
if (result) {
|
||||||
|
console.log(`Certificate request for ${domain} submitted successfully`);
|
||||||
|
} else {
|
||||||
|
console.log(`Certificate request for ${domain} failed`);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`Error requesting certificate: ${err}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -469,45 +637,36 @@ export class PortProxy {
|
|||||||
* @param connectionId - Unique connection identifier
|
* @param connectionId - Unique connection identifier
|
||||||
* @param socket - The incoming client socket
|
* @param socket - The incoming client socket
|
||||||
* @param record - The connection record
|
* @param record - The connection record
|
||||||
* @param domainConfig - The domain configuration
|
|
||||||
* @param initialData - Initial data chunk (TLS ClientHello)
|
* @param initialData - Initial data chunk (TLS ClientHello)
|
||||||
* @param serverName - SNI hostname (if available)
|
|
||||||
*/
|
*/
|
||||||
private forwardToNetworkProxy(
|
private forwardToNetworkProxy(
|
||||||
connectionId: string,
|
connectionId: string,
|
||||||
socket: plugins.net.Socket,
|
socket: plugins.net.Socket,
|
||||||
record: IConnectionRecord,
|
record: IConnectionRecord,
|
||||||
domainConfig: IDomainConfig,
|
initialData: Buffer
|
||||||
initialData: Buffer,
|
|
||||||
serverName?: string
|
|
||||||
): void {
|
): void {
|
||||||
// Determine which NetworkProxy to use
|
// Ensure NetworkProxy is initialized
|
||||||
const proxyIndex =
|
if (!this.networkProxy) {
|
||||||
domainConfig.networkProxyIndex !== undefined ? domainConfig.networkProxyIndex : 0;
|
|
||||||
|
|
||||||
// Validate the NetworkProxy index
|
|
||||||
if (proxyIndex < 0 || proxyIndex >= this.networkProxies.length) {
|
|
||||||
console.log(
|
console.log(
|
||||||
`[${connectionId}] Invalid NetworkProxy index: ${proxyIndex}. Using fallback direct connection.`
|
`[${connectionId}] NetworkProxy not initialized. Using fallback direct connection.`
|
||||||
);
|
);
|
||||||
// Fall back to direct connection
|
// Fall back to direct connection
|
||||||
return this.setupDirectConnection(
|
return this.setupDirectConnection(
|
||||||
connectionId,
|
connectionId,
|
||||||
socket,
|
socket,
|
||||||
record,
|
record,
|
||||||
domainConfig,
|
undefined,
|
||||||
serverName,
|
undefined,
|
||||||
initialData
|
initialData
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const networkProxy = this.networkProxies[proxyIndex];
|
const proxyPort = this.networkProxy.getListeningPort();
|
||||||
const proxyPort = networkProxy.getListeningPort();
|
|
||||||
const proxyHost = 'localhost'; // Assuming NetworkProxy runs locally
|
const proxyHost = 'localhost'; // Assuming NetworkProxy runs locally
|
||||||
|
|
||||||
if (this.settings.enableDetailedLogging) {
|
if (this.settings.enableDetailedLogging) {
|
||||||
console.log(
|
console.log(
|
||||||
`[${connectionId}] Forwarding TLS connection to NetworkProxy[${proxyIndex}] at ${proxyHost}:${proxyPort}`
|
`[${connectionId}] Forwarding TLS connection to NetworkProxy at ${proxyHost}:${proxyPort}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -521,7 +680,6 @@ export class PortProxy {
|
|||||||
record.outgoing = proxySocket;
|
record.outgoing = proxySocket;
|
||||||
record.outgoingStartTime = Date.now();
|
record.outgoingStartTime = Date.now();
|
||||||
record.usingNetworkProxy = true;
|
record.usingNetworkProxy = true;
|
||||||
record.networkProxyIndex = proxyIndex;
|
|
||||||
|
|
||||||
// Set up error handlers
|
// Set up error handlers
|
||||||
proxySocket.on('error', (err) => {
|
proxySocket.on('error', (err) => {
|
||||||
@ -565,7 +723,7 @@ export class PortProxy {
|
|||||||
|
|
||||||
if (this.settings.enableDetailedLogging) {
|
if (this.settings.enableDetailedLogging) {
|
||||||
console.log(
|
console.log(
|
||||||
`[${connectionId}] TLS connection successfully forwarded to NetworkProxy[${proxyIndex}]`
|
`[${connectionId}] TLS connection successfully forwarded to NetworkProxy`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -886,11 +1044,11 @@ export class PortProxy {
|
|||||||
record.pendingData = [];
|
record.pendingData = [];
|
||||||
record.pendingDataSize = 0;
|
record.pendingDataSize = 0;
|
||||||
|
|
||||||
// Add the renegotiation handler for SNI validation, with browser-friendly improvements
|
// Add the renegotiation handler for SNI validation with strict domain enforcement
|
||||||
if (serverName) {
|
if (serverName) {
|
||||||
// Define a handler for checking renegotiation with improved detection
|
// Define a handler for checking renegotiation with improved detection
|
||||||
const renegotiationHandler = (renegChunk: Buffer) => {
|
const renegotiationHandler = (renegChunk: Buffer) => {
|
||||||
// Only process if this looks like a TLS ClientHello (more precise than just checking for type 22)
|
// Only process if this looks like a TLS ClientHello
|
||||||
if (isClientHello(renegChunk)) {
|
if (isClientHello(renegChunk)) {
|
||||||
try {
|
try {
|
||||||
// Extract SNI from ClientHello
|
// Extract SNI from ClientHello
|
||||||
@ -899,44 +1057,14 @@ export class PortProxy {
|
|||||||
// Skip if no SNI was found
|
// Skip if no SNI was found
|
||||||
if (!newSNI) return;
|
if (!newSNI) return;
|
||||||
|
|
||||||
// Handle SNI change during renegotiation
|
// Handle SNI change during renegotiation - always terminate for domain switches
|
||||||
if (newSNI !== record.lockedDomain) {
|
if (newSNI !== record.lockedDomain) {
|
||||||
// Track domain switches for browser connections
|
// Log and terminate the connection for any SNI change
|
||||||
if (!record.domainSwitches) record.domainSwitches = 0;
|
|
||||||
record.domainSwitches++;
|
|
||||||
|
|
||||||
// Check if this is a normal behavior of browser connection reuse
|
|
||||||
const isRelatedDomain = areDomainsRelated(
|
|
||||||
newSNI,
|
|
||||||
record.lockedDomain || '',
|
|
||||||
this.settings.relatedDomainPatterns
|
|
||||||
);
|
|
||||||
|
|
||||||
// Decide how to handle the SNI change based on settings
|
|
||||||
if (this.settings.browserFriendlyMode && isRelatedDomain) {
|
|
||||||
console.log(
|
|
||||||
`[${connectionId}] Browser domain switch detected: ${record.lockedDomain} -> ${newSNI}. ` +
|
|
||||||
`Domains are related, allowing connection to continue (domain switch #${record.domainSwitches}).`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update the locked domain to the new one
|
|
||||||
record.lockedDomain = newSNI;
|
|
||||||
} else if (this.settings.allowRenegotiationWithDifferentSNI) {
|
|
||||||
console.log(
|
console.log(
|
||||||
`[${connectionId}] Renegotiation with different SNI: ${record.lockedDomain} -> ${newSNI}. ` +
|
`[${connectionId}] Renegotiation with different SNI: ${record.lockedDomain} -> ${newSNI}. ` +
|
||||||
`Allowing due to allowRenegotiationWithDifferentSNI setting.`
|
`Terminating connection - SNI domain switching is not allowed.`
|
||||||
);
|
|
||||||
|
|
||||||
// Update the locked domain to the new one
|
|
||||||
record.lockedDomain = newSNI;
|
|
||||||
} else {
|
|
||||||
// Standard strict behavior - terminate connection on SNI mismatch
|
|
||||||
console.log(
|
|
||||||
`[${connectionId}] Renegotiation with different SNI: ${record.lockedDomain} -> ${newSNI}. ` +
|
|
||||||
`Terminating connection. Enable browserFriendlyMode to allow this.`
|
|
||||||
);
|
);
|
||||||
this.initiateCleanupOnce(record, 'sni_mismatch');
|
this.initiateCleanupOnce(record, 'sni_mismatch');
|
||||||
}
|
|
||||||
} else if (this.settings.enableDetailedLogging) {
|
} else if (this.settings.enableDetailedLogging) {
|
||||||
console.log(
|
console.log(
|
||||||
`[${connectionId}] Renegotiation detected with same SNI: ${newSNI}. Allowing.`
|
`[${connectionId}] Renegotiation detected with same SNI: ${newSNI}. Allowing.`
|
||||||
@ -1201,7 +1329,7 @@ export class PortProxy {
|
|||||||
`TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
|
`TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
|
||||||
record.hasKeepAlive ? 'Yes' : 'No'
|
record.hasKeepAlive ? 'Yes' : 'No'
|
||||||
}` +
|
}` +
|
||||||
`${record.usingNetworkProxy ? `, NetworkProxy: ${record.networkProxyIndex}` : ''}` +
|
`${record.usingNetworkProxy ? ', Using NetworkProxy' : ''}` +
|
||||||
`${record.domainSwitches ? `, Domain switches: ${record.domainSwitches}` : ''}`
|
`${record.domainSwitches ? `, Domain switches: ${record.domainSwitches}` : ''}`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -1341,6 +1469,29 @@ export class PortProxy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize NetworkProxy if needed (useNetworkProxy is set but networkProxy isn't initialized)
|
||||||
|
if (this.settings.useNetworkProxy && this.settings.useNetworkProxy.length > 0 && !this.networkProxy) {
|
||||||
|
await this.initializeNetworkProxy();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start NetworkProxy if configured
|
||||||
|
if (this.networkProxy) {
|
||||||
|
await this.networkProxy.start();
|
||||||
|
console.log(`NetworkProxy started on port ${this.settings.networkProxyPort}`);
|
||||||
|
|
||||||
|
// Log ACME status
|
||||||
|
if (this.settings.acme?.enabled) {
|
||||||
|
console.log(`ACME certificate management is enabled (${this.settings.acme.useProduction ? 'Production' : 'Staging'} mode)`);
|
||||||
|
console.log(`ACME HTTP challenge server on port ${this.settings.acme.port}`);
|
||||||
|
|
||||||
|
// Register domains for ACME certificates if enabled
|
||||||
|
if (this.networkProxy.options.acme?.enabled) {
|
||||||
|
console.log('Registering domains with ACME certificate manager...');
|
||||||
|
// The NetworkProxy will handle this internally via registerDomainsWithAcmeManager()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Define a unified connection handler for all listening ports.
|
// Define a unified connection handler for all listening ports.
|
||||||
const connectionHandler = (socket: plugins.net.Socket) => {
|
const connectionHandler = (socket: plugins.net.Socket) => {
|
||||||
if (this.isShuttingDown) {
|
if (this.isShuttingDown) {
|
||||||
@ -1401,12 +1552,12 @@ export class PortProxy {
|
|||||||
incomingTerminationReason: null,
|
incomingTerminationReason: null,
|
||||||
outgoingTerminationReason: null,
|
outgoingTerminationReason: null,
|
||||||
|
|
||||||
// Initialize NetworkProxy tracking fields
|
// Initialize NetworkProxy tracking
|
||||||
usingNetworkProxy: false,
|
usingNetworkProxy: false,
|
||||||
|
|
||||||
// Initialize browser connection tracking
|
// Initialize browser connection tracking
|
||||||
isBrowserConnection: this.settings.browserFriendlyMode, // Assume browser if browserFriendlyMode is enabled
|
isBrowserConnection: false,
|
||||||
domainSwitches: 0, // Track domain switches
|
domainSwitches: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Apply keep-alive settings if enabled
|
// Apply keep-alive settings if enabled
|
||||||
@ -1443,7 +1594,6 @@ export class PortProxy {
|
|||||||
console.log(
|
console.log(
|
||||||
`[${connectionId}] New connection from ${remoteIP} on port ${localPort}. ` +
|
`[${connectionId}] New connection from ${remoteIP} on port ${localPort}. ` +
|
||||||
`Keep-Alive: ${connectionRecord.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` +
|
`Keep-Alive: ${connectionRecord.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` +
|
||||||
`Mode: ${this.settings.browserFriendlyMode ? 'Browser-friendly' : 'Standard'}. ` +
|
|
||||||
`Active connections: ${this.connectionRecords.size}`
|
`Active connections: ${this.connectionRecords.size}`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -1452,8 +1602,63 @@ export class PortProxy {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this connection should be forwarded directly to NetworkProxy based on port
|
||||||
|
const shouldUseNetworkProxy = this.settings.useNetworkProxy &&
|
||||||
|
this.settings.useNetworkProxy.includes(localPort);
|
||||||
|
|
||||||
|
if (shouldUseNetworkProxy) {
|
||||||
|
// For NetworkProxy ports, we want to capture the TLS handshake and forward directly
|
||||||
let initialDataReceived = false;
|
let initialDataReceived = false;
|
||||||
|
|
||||||
|
// Set an initial timeout for handshake data
|
||||||
|
let initialTimeout: NodeJS.Timeout | null = setTimeout(() => {
|
||||||
|
if (!initialDataReceived) {
|
||||||
|
console.log(
|
||||||
|
`[${connectionId}] Initial data timeout (${this.settings.initialDataTimeout}ms) for connection from ${remoteIP} on port ${localPort}`
|
||||||
|
);
|
||||||
|
if (connectionRecord.incomingTerminationReason === null) {
|
||||||
|
connectionRecord.incomingTerminationReason = 'initial_timeout';
|
||||||
|
this.incrementTerminationStat('incoming', 'initial_timeout');
|
||||||
|
}
|
||||||
|
socket.end();
|
||||||
|
this.cleanupConnection(connectionRecord, 'initial_timeout');
|
||||||
|
}
|
||||||
|
}, this.settings.initialDataTimeout!);
|
||||||
|
|
||||||
|
// Make sure timeout doesn't keep the process alive
|
||||||
|
if (initialTimeout.unref) {
|
||||||
|
initialTimeout.unref();
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.on('error', this.handleError('incoming', connectionRecord));
|
||||||
|
|
||||||
|
// First data handler to capture initial TLS handshake for NetworkProxy
|
||||||
|
socket.once('data', (chunk: Buffer) => {
|
||||||
|
// Clear the initial timeout since we've received data
|
||||||
|
if (initialTimeout) {
|
||||||
|
clearTimeout(initialTimeout);
|
||||||
|
initialTimeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
initialDataReceived = true;
|
||||||
|
connectionRecord.hasReceivedInitialData = true;
|
||||||
|
|
||||||
|
// Check if this looks like a TLS handshake
|
||||||
|
if (isTlsHandshake(chunk)) {
|
||||||
|
connectionRecord.isTLS = true;
|
||||||
|
|
||||||
|
// Forward directly to NetworkProxy without SNI processing
|
||||||
|
this.forwardToNetworkProxy(connectionId, socket, connectionRecord, chunk);
|
||||||
|
} else {
|
||||||
|
// If not TLS, use normal direct connection
|
||||||
|
console.log(`[${connectionId}] Non-TLS connection on NetworkProxy port ${localPort}`);
|
||||||
|
this.setupDirectConnection(connectionId, socket, connectionRecord, undefined, undefined, chunk);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// For non-NetworkProxy ports, proceed with normal processing
|
||||||
|
|
||||||
// Define helpers for rejecting connections
|
// Define helpers for rejecting connections
|
||||||
const rejectIncomingConnection = (reason: string, logMessage: string) => {
|
const rejectIncomingConnection = (reason: string, logMessage: string) => {
|
||||||
console.log(`[${connectionId}] ${logMessage}`);
|
console.log(`[${connectionId}] ${logMessage}`);
|
||||||
@ -1465,6 +1670,8 @@ export class PortProxy {
|
|||||||
this.cleanupConnection(connectionRecord, reason);
|
this.cleanupConnection(connectionRecord, reason);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let initialDataReceived = false;
|
||||||
|
|
||||||
// Set an initial timeout for SNI data if needed
|
// Set an initial timeout for SNI data if needed
|
||||||
let initialTimeout: NodeJS.Timeout | null = null;
|
let initialTimeout: NodeJS.Timeout | null = null;
|
||||||
if (this.settings.sniEnabled) {
|
if (this.settings.sniEnabled) {
|
||||||
@ -1513,7 +1720,7 @@ export class PortProxy {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up the connection to the target host or NetworkProxy.
|
* Sets up the connection to the target host.
|
||||||
* @param serverName - The SNI hostname (unused when forcedDomain is provided).
|
* @param serverName - The SNI hostname (unused when forcedDomain is provided).
|
||||||
* @param initialChunk - Optional initial data chunk.
|
* @param initialChunk - Optional initial data chunk.
|
||||||
* @param forcedDomain - If provided, overrides SNI/domain lookup (used for port-based routing).
|
* @param forcedDomain - If provided, overrides SNI/domain lookup (used for port-based routing).
|
||||||
@ -1582,23 +1789,6 @@ export class PortProxy {
|
|||||||
)}`
|
)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we should forward this to a NetworkProxy
|
|
||||||
if (
|
|
||||||
isTlsHandshakeDetected &&
|
|
||||||
domainConfig.useNetworkProxy === true &&
|
|
||||||
initialChunk &&
|
|
||||||
this.networkProxies.length > 0
|
|
||||||
) {
|
|
||||||
return this.forwardToNetworkProxy(
|
|
||||||
connectionId,
|
|
||||||
socket,
|
|
||||||
connectionRecord,
|
|
||||||
domainConfig,
|
|
||||||
initialChunk,
|
|
||||||
serverName
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) {
|
} else if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) {
|
||||||
if (
|
if (
|
||||||
!isGlobIPAllowed(
|
!isGlobIPAllowed(
|
||||||
@ -1614,12 +1804,12 @@ export class PortProxy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the initial SNI for browser connection management
|
// Save the initial SNI
|
||||||
if (serverName) {
|
if (serverName) {
|
||||||
connectionRecord.lockedDomain = serverName;
|
connectionRecord.lockedDomain = serverName;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we didn't forward to NetworkProxy, proceed with direct connection
|
// Set up the direct connection
|
||||||
return this.setupDirectConnection(
|
return this.setupDirectConnection(
|
||||||
connectionId,
|
connectionId,
|
||||||
socket,
|
socket,
|
||||||
@ -1764,6 +1954,7 @@ export class PortProxy {
|
|||||||
|
|
||||||
setupConnection('');
|
setupConnection('');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- SETUP LISTENERS ---
|
// --- SETUP LISTENERS ---
|
||||||
@ -1788,12 +1979,11 @@ export class PortProxy {
|
|||||||
console.log(`Server Error on port ${port}: ${err.message}`);
|
console.log(`Server Error on port ${port}: ${err.message}`);
|
||||||
});
|
});
|
||||||
server.listen(port, () => {
|
server.listen(port, () => {
|
||||||
|
const isNetworkProxyPort = this.settings.useNetworkProxy?.includes(port);
|
||||||
console.log(
|
console.log(
|
||||||
`PortProxy -> OK: Now listening on port ${port}${
|
`PortProxy -> OK: Now listening on port ${port}${
|
||||||
this.settings.sniEnabled ? ' (SNI passthrough enabled)' : ''
|
this.settings.sniEnabled && !isNetworkProxyPort ? ' (SNI passthrough enabled)' : ''
|
||||||
}${this.networkProxies.length > 0 ? ' (NetworkProxy integration enabled)' : ''}${
|
}${isNetworkProxyPort ? ' (NetworkProxy forwarding enabled)' : ''}`
|
||||||
this.settings.browserFriendlyMode ? ' (Browser-friendly mode enabled)' : ''
|
|
||||||
}`
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
this.netServers.push(server);
|
this.netServers.push(server);
|
||||||
@ -1963,21 +2153,6 @@ export class PortProxy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Add or replace NetworkProxy instances
|
|
||||||
*/
|
|
||||||
public setNetworkProxies(networkProxies: NetworkProxy[]): void {
|
|
||||||
this.networkProxies = networkProxies;
|
|
||||||
console.log(`Updated NetworkProxy instances: ${this.networkProxies.length} proxies configured`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a list of configured NetworkProxy instances
|
|
||||||
*/
|
|
||||||
public getNetworkProxies(): NetworkProxy[] {
|
|
||||||
return this.networkProxies;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gracefully shut down the proxy
|
* Gracefully shut down the proxy
|
||||||
*/
|
*/
|
||||||
@ -2069,6 +2244,22 @@ export class PortProxy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop NetworkProxy if it was started (which also stops ACME manager)
|
||||||
|
if (this.networkProxy) {
|
||||||
|
try {
|
||||||
|
console.log('Stopping NetworkProxy...');
|
||||||
|
await this.networkProxy.stop();
|
||||||
|
console.log('NetworkProxy stopped successfully');
|
||||||
|
|
||||||
|
// Log ACME shutdown if it was enabled
|
||||||
|
if (this.settings.acme?.enabled) {
|
||||||
|
console.log('ACME certificate manager stopped');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`Error stopping NetworkProxy: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Clear all tracking maps
|
// Clear all tracking maps
|
||||||
this.connectionRecords.clear();
|
this.connectionRecords.clear();
|
||||||
this.domainTargetIndices.clear();
|
this.domainTargetIndices.clear();
|
||||||
|
@ -19,6 +19,21 @@ export interface IRouterResult {
|
|||||||
pathRemainder?: string;
|
pathRemainder?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Router for HTTP reverse proxy requests
|
||||||
|
*
|
||||||
|
* Supports the following domain matching patterns:
|
||||||
|
* - Exact matches: "example.com"
|
||||||
|
* - Wildcard subdomains: "*.example.com" (matches any subdomain of example.com)
|
||||||
|
* - TLD wildcards: "example.*" (matches example.com, example.org, etc.)
|
||||||
|
* - Complex wildcards: "*.lossless*" (matches any subdomain of any lossless domain)
|
||||||
|
* - Default fallback: "*" (matches any unmatched domain)
|
||||||
|
*
|
||||||
|
* Also supports path pattern matching for each domain:
|
||||||
|
* - Exact path: "/api/users"
|
||||||
|
* - Wildcard paths: "/api/*"
|
||||||
|
* - Path parameters: "/users/:id/profile"
|
||||||
|
*/
|
||||||
export class ProxyRouter {
|
export class ProxyRouter {
|
||||||
// Store original configs for reference
|
// Store original configs for reference
|
||||||
private reverseProxyConfigs: tsclass.network.IReverseProxyConfig[] = [];
|
private reverseProxyConfigs: tsclass.network.IReverseProxyConfig[] = [];
|
||||||
@ -98,9 +113,11 @@ export class ProxyRouter {
|
|||||||
return exactConfig;
|
return exactConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try wildcard subdomain
|
// Try various wildcard patterns
|
||||||
if (hostWithoutPort.includes('.')) {
|
if (hostWithoutPort.includes('.')) {
|
||||||
const domainParts = hostWithoutPort.split('.');
|
const domainParts = hostWithoutPort.split('.');
|
||||||
|
|
||||||
|
// Try wildcard subdomain (*.example.com)
|
||||||
if (domainParts.length > 2) {
|
if (domainParts.length > 2) {
|
||||||
const wildcardDomain = `*.${domainParts.slice(1).join('.')}`;
|
const wildcardDomain = `*.${domainParts.slice(1).join('.')}`;
|
||||||
const wildcardConfig = this.findConfigForHost(wildcardDomain, urlPath);
|
const wildcardConfig = this.findConfigForHost(wildcardDomain, urlPath);
|
||||||
@ -108,6 +125,23 @@ export class ProxyRouter {
|
|||||||
return wildcardConfig;
|
return wildcardConfig;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try TLD wildcard (example.*)
|
||||||
|
const baseDomain = domainParts.slice(0, -1).join('.');
|
||||||
|
const tldWildcardDomain = `${baseDomain}.*`;
|
||||||
|
const tldWildcardConfig = this.findConfigForHost(tldWildcardDomain, urlPath);
|
||||||
|
if (tldWildcardConfig) {
|
||||||
|
return tldWildcardConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try complex wildcard patterns
|
||||||
|
const wildcardPatterns = this.findWildcardMatches(hostWithoutPort);
|
||||||
|
for (const pattern of wildcardPatterns) {
|
||||||
|
const wildcardConfig = this.findConfigForHost(pattern, urlPath);
|
||||||
|
if (wildcardConfig) {
|
||||||
|
return wildcardConfig;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall back to default config if available
|
// Fall back to default config if available
|
||||||
@ -120,6 +154,53 @@ export class ProxyRouter {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find potential wildcard patterns that could match a given hostname
|
||||||
|
* Handles complex patterns like "*.lossless*" or other partial matches
|
||||||
|
* @param hostname The hostname to find wildcard matches for
|
||||||
|
* @returns Array of potential wildcard patterns that could match
|
||||||
|
*/
|
||||||
|
private findWildcardMatches(hostname: string): string[] {
|
||||||
|
const patterns: string[] = [];
|
||||||
|
const hostnameParts = hostname.split('.');
|
||||||
|
|
||||||
|
// Find all configured hostnames that contain wildcards
|
||||||
|
const wildcardConfigs = this.reverseProxyConfigs.filter(
|
||||||
|
config => config.hostName.includes('*')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Extract unique wildcard patterns
|
||||||
|
const wildcardPatterns = [...new Set(
|
||||||
|
wildcardConfigs.map(config => config.hostName.toLowerCase())
|
||||||
|
)];
|
||||||
|
|
||||||
|
// For each wildcard pattern, check if it could match the hostname
|
||||||
|
// using simplified regex pattern matching
|
||||||
|
for (const pattern of wildcardPatterns) {
|
||||||
|
// Skip the default wildcard '*'
|
||||||
|
if (pattern === '*') continue;
|
||||||
|
|
||||||
|
// Skip already checked patterns (*.domain.com and domain.*)
|
||||||
|
if (pattern.startsWith('*.') && pattern.indexOf('*', 2) === -1) continue;
|
||||||
|
if (pattern.endsWith('.*') && pattern.indexOf('*') === pattern.length - 1) continue;
|
||||||
|
|
||||||
|
// Convert wildcard pattern to regex
|
||||||
|
const regexPattern = pattern
|
||||||
|
.replace(/\./g, '\\.') // Escape dots
|
||||||
|
.replace(/\*/g, '.*'); // Convert * to .* for regex
|
||||||
|
|
||||||
|
// Create regex object with case insensitive flag
|
||||||
|
const regex = new RegExp(`^${regexPattern}$`, 'i');
|
||||||
|
|
||||||
|
// If hostname matches this complex pattern, add it to the list
|
||||||
|
if (regex.test(hostname)) {
|
||||||
|
patterns.push(pattern);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return patterns;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find a config for a specific host and path
|
* Find a config for a specific host and path
|
||||||
*/
|
*/
|
||||||
|
Reference in New Issue
Block a user