Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e0af82c1ef | |||
| efe3d80713 | |||
| 6b04bc612b | |||
| e774ec87ca | |||
| cbde778f09 | |||
| bc2bc874a5 | |||
| fdabf807b0 | |||
| 81e0e6b4d8 | |||
| 28fa69bf59 | |||
| 5019658032 | |||
| a9fe365c78 | |||
| 32e0410227 |
47
changelog.md
47
changelog.md
@@ -1,5 +1,52 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-02-13 - 23.1.6 - fix(smart-proxy)
|
||||||
|
disable built-in Rust ACME when a certProvisionFunction is provided and improve certificate provisioning flow
|
||||||
|
|
||||||
|
- Pass an optional ACME override into buildRustConfig so Rust ACME can be disabled per-run
|
||||||
|
- Disable Rust ACME when certProvisionFunction is configured to avoid provisioning race conditions
|
||||||
|
- Normalize routing glob patterns into concrete domain identifiers for certificate provisioning (expand leading-star globs and warn on unsupported patterns)
|
||||||
|
- Deduplicate domains during provisioning to avoid repeated attempts
|
||||||
|
- When the callback returns 'http01', explicitly trigger Rust ACME for the route via bridge.provisionCertificate and log success/failure
|
||||||
|
|
||||||
|
## 2026-02-13 - 23.1.5 - fix(smart-proxy)
|
||||||
|
provision certificates for wildcard domains instead of skipping them
|
||||||
|
|
||||||
|
- Removed early continue that skipped domains containing '*' in the domain loop
|
||||||
|
- Now calls provisionFn for wildcard domains so certificate provisioning can proceed for wildcard hosts
|
||||||
|
- Fixes cases where wildcard domains never had certificates requested
|
||||||
|
|
||||||
|
## 2026-02-12 - 23.1.4 - fix(tests)
|
||||||
|
make tests more robust and bump small dependencies
|
||||||
|
|
||||||
|
- Bump dependencies: @push.rocks/smartrust ^1.2.1 and minimatch ^10.2.0
|
||||||
|
- Replace hardcoded ports with named constants (ECHO_PORT, PROXY_PORT, PROXY_PORT_1/2) to avoid collisions between tests
|
||||||
|
- Add server 'error' handlers and reject listen promises on server errors to prevent silent hangs
|
||||||
|
- Reduce test timeouts and intervals (shorter test durations, more frequent pings) to speed up test runs
|
||||||
|
- Ensure proxy is stopped between tests and remove forced process.exit; export tap.start() consistently
|
||||||
|
- Adjust assertions to match the new shorter ping/response counts
|
||||||
|
|
||||||
|
## 2026-02-12 - 23.1.3 - fix(rustproxy)
|
||||||
|
install default rustls crypto provider early; detect and skip raw fast-path for HTTP connections and return proper HTTP 502 when no route matches
|
||||||
|
|
||||||
|
- Install ring-based rustls crypto provider at startup to prevent panics from instant-acme/hyper-rustls calling ClientConfig::builder() before TLS listeners are initialized
|
||||||
|
- Add a non-blocking 10ms peek to detect HTTP traffic in the TCP passthrough fast-path to avoid misrouting HTTP and ensure HTTP proxy handles CORS, errors, and request-level routing
|
||||||
|
- Skip the fast-path and fall back to the HTTP proxy when HTTP is detected (with a debug log)
|
||||||
|
- When no route matches for detected HTTP connections, send an HTTP 502 Bad Gateway response and close the connection instead of silently dropping it
|
||||||
|
|
||||||
|
## 2026-02-11 - 23.1.2 - fix(core)
|
||||||
|
use node: scoped builtin imports and add route unit tests
|
||||||
|
|
||||||
|
- Replaced bare Node built-in imports (events, fs, http, https, net, path, tls, url, http2, buffer, crypto) with 'node:' specifiers for ESM/bundler compatibility (files updated include ts/plugins.ts, ts/core/models/socket-types.ts, ts/core/utils/enhanced-connection-pool.ts, ts/core/utils/socket-tracker.ts, ts/protocols/common/fragment-handler.ts, ts/protocols/tls/sni/client-hello-parser.ts, ts/protocols/tls/sni/sni-extraction.ts, ts/protocols/websocket/utils.ts, ts/tls/sni/sni-handler.ts).
|
||||||
|
- Added new unit tests (test/test.bun.ts and test/test.deno.ts) covering route helpers, validators, matching, merging and cloning to improve test coverage.
|
||||||
|
|
||||||
|
## 2026-02-11 - 23.1.1 - fix(rust-proxy)
|
||||||
|
increase rust proxy bridge maxPayloadSize to 100 MB and bump dependencies
|
||||||
|
|
||||||
|
- Set maxPayloadSize to 100 * 1024 * 1024 (100 MB) in ts/proxies/smart-proxy/rust-proxy-bridge.ts to support large route configs
|
||||||
|
- Bump devDependency @types/node from ^25.2.2 to ^25.2.3
|
||||||
|
- Bump dependency @push.rocks/smartrust from ^1.1.1 to ^1.2.0
|
||||||
|
|
||||||
## 2026-02-10 - 23.1.0 - feat(rust-bridge)
|
## 2026-02-10 - 23.1.0 - feat(rust-bridge)
|
||||||
integrate tsrust to build and locate cross-compiled Rust binaries; refactor rust-proxy bridge to use typed IPC and streamline process handling; add @push.rocks/smartrust and update build/dev dependencies
|
integrate tsrust to build and locate cross-compiled Rust binaries; refactor rust-proxy bridge to use typed IPC and streamline process handling; add @push.rocks/smartrust and update build/dev dependencies
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartproxy",
|
"name": "@push.rocks/smartproxy",
|
||||||
"version": "23.1.0",
|
"version": "23.1.6",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.",
|
"description": "A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.",
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
"@git.zone/tsrust": "^1.3.0",
|
"@git.zone/tsrust": "^1.3.0",
|
||||||
"@git.zone/tstest": "^3.1.8",
|
"@git.zone/tstest": "^3.1.8",
|
||||||
"@push.rocks/smartserve": "^2.0.1",
|
"@push.rocks/smartserve": "^2.0.1",
|
||||||
"@types/node": "^25.2.2",
|
"@types/node": "^25.2.3",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"why-is-node-running": "^3.2.2"
|
"why-is-node-running": "^3.2.2"
|
||||||
},
|
},
|
||||||
@@ -34,14 +34,14 @@
|
|||||||
"@push.rocks/smartnetwork": "^4.4.0",
|
"@push.rocks/smartnetwork": "^4.4.0",
|
||||||
"@push.rocks/smartpromise": "^4.2.3",
|
"@push.rocks/smartpromise": "^4.2.3",
|
||||||
"@push.rocks/smartrequest": "^5.0.1",
|
"@push.rocks/smartrequest": "^5.0.1",
|
||||||
"@push.rocks/smartrust": "^1.1.1",
|
"@push.rocks/smartrust": "^1.2.1",
|
||||||
"@push.rocks/smartrx": "^3.0.10",
|
"@push.rocks/smartrx": "^3.0.10",
|
||||||
"@push.rocks/smartstring": "^4.1.0",
|
"@push.rocks/smartstring": "^4.1.0",
|
||||||
"@push.rocks/taskbuffer": "^4.2.0",
|
"@push.rocks/taskbuffer": "^4.2.0",
|
||||||
"@tsclass/tsclass": "^9.3.0",
|
"@tsclass/tsclass": "^9.3.0",
|
||||||
"@types/minimatch": "^6.0.0",
|
"@types/minimatch": "^6.0.0",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"minimatch": "^10.1.2",
|
"minimatch": "^10.2.0",
|
||||||
"pretty-ms": "^9.3.0",
|
"pretty-ms": "^9.3.0",
|
||||||
"ws": "^8.19.0"
|
"ws": "^8.19.0"
|
||||||
},
|
},
|
||||||
|
|||||||
118
pnpm-lock.yaml
generated
118
pnpm-lock.yaml
generated
@@ -36,8 +36,8 @@ importers:
|
|||||||
specifier: ^5.0.1
|
specifier: ^5.0.1
|
||||||
version: 5.0.1
|
version: 5.0.1
|
||||||
'@push.rocks/smartrust':
|
'@push.rocks/smartrust':
|
||||||
specifier: ^1.1.1
|
specifier: ^1.2.1
|
||||||
version: 1.1.1
|
version: 1.2.1
|
||||||
'@push.rocks/smartrx':
|
'@push.rocks/smartrx':
|
||||||
specifier: ^3.0.10
|
specifier: ^3.0.10
|
||||||
version: 3.0.10
|
version: 3.0.10
|
||||||
@@ -57,8 +57,8 @@ importers:
|
|||||||
specifier: ^8.18.1
|
specifier: ^8.18.1
|
||||||
version: 8.18.1
|
version: 8.18.1
|
||||||
minimatch:
|
minimatch:
|
||||||
specifier: ^10.1.2
|
specifier: ^10.2.0
|
||||||
version: 10.1.2
|
version: 10.2.0
|
||||||
pretty-ms:
|
pretty-ms:
|
||||||
specifier: ^9.3.0
|
specifier: ^9.3.0
|
||||||
version: 9.3.0
|
version: 9.3.0
|
||||||
@@ -82,8 +82,8 @@ importers:
|
|||||||
specifier: ^2.0.1
|
specifier: ^2.0.1
|
||||||
version: 2.0.1
|
version: 2.0.1
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^25.2.2
|
specifier: ^25.2.3
|
||||||
version: 25.2.2
|
version: 25.2.3
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.9.3
|
specifier: ^5.9.3
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
@@ -570,14 +570,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==}
|
resolution: {integrity: sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
'@isaacs/balanced-match@4.0.1':
|
|
||||||
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
|
|
||||||
engines: {node: 20 || >=22}
|
|
||||||
|
|
||||||
'@isaacs/brace-expansion@5.0.1':
|
|
||||||
resolution: {integrity: sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==}
|
|
||||||
engines: {node: 20 || >=22}
|
|
||||||
|
|
||||||
'@isaacs/cliui@9.0.0':
|
'@isaacs/cliui@9.0.0':
|
||||||
resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==}
|
resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -883,8 +875,8 @@ packages:
|
|||||||
'@push.rocks/smartrouter@1.3.3':
|
'@push.rocks/smartrouter@1.3.3':
|
||||||
resolution: {integrity: sha512-1+xZEnWlhzqLWAaJ1zFNhQ0zgbfCWQl1DBT72LygLxTs+P0K8AwJKgqo/IX6CT55kGCFnPAZIYSbVJlGsgrB0w==}
|
resolution: {integrity: sha512-1+xZEnWlhzqLWAaJ1zFNhQ0zgbfCWQl1DBT72LygLxTs+P0K8AwJKgqo/IX6CT55kGCFnPAZIYSbVJlGsgrB0w==}
|
||||||
|
|
||||||
'@push.rocks/smartrust@1.1.1':
|
'@push.rocks/smartrust@1.2.1':
|
||||||
resolution: {integrity: sha512-NtfTOrVpw0K+z/jW24OmunvZBqkJHfe1tJhTMPFYUb4a5Yt5mtTc3oUvlX+bHarn94Jq0oh0HCLh8xcPQ2Sd7w==}
|
resolution: {integrity: sha512-ANwXXibUwoHNWF1hhXhXVVrfzYlhgHYRa2205Jkd/s/wXzcWHftYZthilJj+52B7nkzSB76umfxKfK5eBYY2Ug==}
|
||||||
|
|
||||||
'@push.rocks/smartrx@3.0.10':
|
'@push.rocks/smartrx@3.0.10':
|
||||||
resolution: {integrity: sha512-USjIYcsSfzn14cwOsxgq/bBmWDTTzy3ouWAnW5NdMyRRzEbmeNrvmy6TRqNeDlJ2PsYNTt1rr/zGUqvIy72ITg==}
|
resolution: {integrity: sha512-USjIYcsSfzn14cwOsxgq/bBmWDTTzy3ouWAnW5NdMyRRzEbmeNrvmy6TRqNeDlJ2PsYNTt1rr/zGUqvIy72ITg==}
|
||||||
@@ -1497,11 +1489,11 @@ packages:
|
|||||||
'@types/node@18.19.130':
|
'@types/node@18.19.130':
|
||||||
resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==}
|
resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==}
|
||||||
|
|
||||||
'@types/node@22.19.10':
|
'@types/node@22.19.11':
|
||||||
resolution: {integrity: sha512-tF5VOugLS/EuDlTBijk0MqABfP8UxgYazTLo3uIn3b4yJgg26QRbVYJYsDtHrjdDUIRfP70+VfhTTc+CE1yskw==}
|
resolution: {integrity: sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==}
|
||||||
|
|
||||||
'@types/node@25.2.2':
|
'@types/node@25.2.3':
|
||||||
resolution: {integrity: sha512-BkmoP5/FhRYek5izySdkOneRyXYN35I860MFAGupTdebyE66uZaR+bXLHq8k4DirE5DwQi3NuhvRU1jqTVwUrQ==}
|
resolution: {integrity: sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==}
|
||||||
|
|
||||||
'@types/ping@0.4.4':
|
'@types/ping@0.4.4':
|
||||||
resolution: {integrity: sha512-ifvo6w2f5eJYlXm+HiVx67iJe8WZp87sfa683nlqED5Vnt9Z93onkokNoWqOG21EaE8fMxyKPobE+mkPEyxsdw==}
|
resolution: {integrity: sha512-ifvo6w2f5eJYlXm+HiVx67iJe8WZp87sfa683nlqED5Vnt9Z93onkokNoWqOG21EaE8fMxyKPobE+mkPEyxsdw==}
|
||||||
@@ -1649,6 +1641,10 @@ packages:
|
|||||||
balanced-match@1.0.2:
|
balanced-match@1.0.2:
|
||||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||||
|
|
||||||
|
balanced-match@4.0.2:
|
||||||
|
resolution: {integrity: sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==}
|
||||||
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
bare-events@2.8.2:
|
bare-events@2.8.2:
|
||||||
resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==}
|
resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1714,6 +1710,10 @@ packages:
|
|||||||
brace-expansion@2.0.2:
|
brace-expansion@2.0.2:
|
||||||
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
|
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
|
||||||
|
|
||||||
|
brace-expansion@5.0.2:
|
||||||
|
resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==}
|
||||||
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
broadcast-channel@7.2.0:
|
broadcast-channel@7.2.0:
|
||||||
resolution: {integrity: sha512-JgraikEriG/TxBUi2W/w2O0jhHjXZUtXAvCZH0Yr3whjxYVgAg0hSe6r/teM+I5H5Q/q6RhyuKdC2pHNlFyepQ==}
|
resolution: {integrity: sha512-JgraikEriG/TxBUi2W/w2O0jhHjXZUtXAvCZH0Yr3whjxYVgAg0hSe6r/teM+I5H5Q/q6RhyuKdC2pHNlFyepQ==}
|
||||||
|
|
||||||
@@ -2750,8 +2750,8 @@ packages:
|
|||||||
minimalistic-crypto-utils@1.0.1:
|
minimalistic-crypto-utils@1.0.1:
|
||||||
resolution: {integrity: sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=}
|
resolution: {integrity: sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=}
|
||||||
|
|
||||||
minimatch@10.1.2:
|
minimatch@10.2.0:
|
||||||
resolution: {integrity: sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==}
|
resolution: {integrity: sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
minimatch@3.1.2:
|
minimatch@3.1.2:
|
||||||
@@ -4576,7 +4576,7 @@ snapshots:
|
|||||||
'@inquirer/figures': 1.0.15
|
'@inquirer/figures': 1.0.15
|
||||||
'@inquirer/type': 2.0.0
|
'@inquirer/type': 2.0.0
|
||||||
'@types/mute-stream': 0.0.4
|
'@types/mute-stream': 0.0.4
|
||||||
'@types/node': 22.19.10
|
'@types/node': 22.19.11
|
||||||
'@types/wrap-ansi': 3.0.0
|
'@types/wrap-ansi': 3.0.0
|
||||||
ansi-escapes: 4.3.2
|
ansi-escapes: 4.3.2
|
||||||
cli-width: 4.1.0
|
cli-width: 4.1.0
|
||||||
@@ -4654,12 +4654,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
mute-stream: 1.0.0
|
mute-stream: 1.0.0
|
||||||
|
|
||||||
'@isaacs/balanced-match@4.0.1': {}
|
|
||||||
|
|
||||||
'@isaacs/brace-expansion@5.0.1':
|
|
||||||
dependencies:
|
|
||||||
'@isaacs/balanced-match': 4.0.1
|
|
||||||
|
|
||||||
'@isaacs/cliui@9.0.0': {}
|
'@isaacs/cliui@9.0.0': {}
|
||||||
|
|
||||||
'@leichtgewicht/ip-codec@2.0.5': {}
|
'@leichtgewicht/ip-codec@2.0.5': {}
|
||||||
@@ -5034,7 +5028,7 @@ snapshots:
|
|||||||
'@push.rocks/smartstring': 4.1.0
|
'@push.rocks/smartstring': 4.1.0
|
||||||
'@push.rocks/smartunique': 3.0.9
|
'@push.rocks/smartunique': 3.0.9
|
||||||
'@tsclass/tsclass': 9.3.0
|
'@tsclass/tsclass': 9.3.0
|
||||||
minimatch: 10.1.2
|
minimatch: 10.2.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- aws-crt
|
- aws-crt
|
||||||
|
|
||||||
@@ -5127,7 +5121,7 @@ snapshots:
|
|||||||
acme-client: 5.4.0
|
acme-client: 5.4.0
|
||||||
dns-packet: 5.6.1
|
dns-packet: 5.6.1
|
||||||
elliptic: 6.6.1
|
elliptic: 6.6.1
|
||||||
minimatch: 10.1.2
|
minimatch: 10.2.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -5143,7 +5137,7 @@ snapshots:
|
|||||||
acme-client: 5.4.0
|
acme-client: 5.4.0
|
||||||
dns-packet: 5.6.1
|
dns-packet: 5.6.1
|
||||||
elliptic: 6.6.1
|
elliptic: 6.6.1
|
||||||
minimatch: 10.1.2
|
minimatch: 10.2.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -5497,7 +5491,7 @@ snapshots:
|
|||||||
'@push.rocks/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
path-to-regexp: 8.3.0
|
path-to-regexp: 8.3.0
|
||||||
|
|
||||||
'@push.rocks/smartrust@1.1.1':
|
'@push.rocks/smartrust@1.2.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
|
|
||||||
@@ -6239,27 +6233,27 @@ snapshots:
|
|||||||
|
|
||||||
'@types/bn.js@5.2.0':
|
'@types/bn.js@5.2.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.2.2
|
'@types/node': 25.2.3
|
||||||
|
|
||||||
'@types/body-parser@1.19.6':
|
'@types/body-parser@1.19.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/connect': 3.4.38
|
'@types/connect': 3.4.38
|
||||||
'@types/node': 25.2.2
|
'@types/node': 25.2.3
|
||||||
|
|
||||||
'@types/buffer-json@2.0.3': {}
|
'@types/buffer-json@2.0.3': {}
|
||||||
|
|
||||||
'@types/clean-css@4.2.11':
|
'@types/clean-css@4.2.11':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.2.2
|
'@types/node': 25.2.3
|
||||||
source-map: 0.6.1
|
source-map: 0.6.1
|
||||||
|
|
||||||
'@types/connect@3.4.38':
|
'@types/connect@3.4.38':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.2.2
|
'@types/node': 25.2.3
|
||||||
|
|
||||||
'@types/cors@2.8.19':
|
'@types/cors@2.8.19':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.2.2
|
'@types/node': 25.2.3
|
||||||
|
|
||||||
'@types/debug@4.1.12':
|
'@types/debug@4.1.12':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6267,7 +6261,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/dns-packet@5.6.5':
|
'@types/dns-packet@5.6.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.2.2
|
'@types/node': 25.2.3
|
||||||
|
|
||||||
'@types/elliptic@6.4.18':
|
'@types/elliptic@6.4.18':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6275,7 +6269,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/express-serve-static-core@5.1.0':
|
'@types/express-serve-static-core@5.1.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.2.2
|
'@types/node': 25.2.3
|
||||||
'@types/qs': 6.14.0
|
'@types/qs': 6.14.0
|
||||||
'@types/range-parser': 1.2.7
|
'@types/range-parser': 1.2.7
|
||||||
'@types/send': 1.2.1
|
'@types/send': 1.2.1
|
||||||
@@ -6289,7 +6283,7 @@ snapshots:
|
|||||||
'@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': 25.2.2
|
'@types/node': 25.2.3
|
||||||
|
|
||||||
'@types/hast@3.0.4':
|
'@types/hast@3.0.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6311,7 +6305,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/jsonfile@6.1.4':
|
'@types/jsonfile@6.1.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.2.2
|
'@types/node': 25.2.3
|
||||||
|
|
||||||
'@types/mdast@4.0.4':
|
'@types/mdast@4.0.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6323,32 +6317,32 @@ snapshots:
|
|||||||
|
|
||||||
'@types/minimatch@6.0.0':
|
'@types/minimatch@6.0.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
minimatch: 10.1.2
|
minimatch: 10.2.0
|
||||||
|
|
||||||
'@types/ms@2.1.0': {}
|
'@types/ms@2.1.0': {}
|
||||||
|
|
||||||
'@types/mute-stream@0.0.4':
|
'@types/mute-stream@0.0.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.2.2
|
'@types/node': 25.2.3
|
||||||
|
|
||||||
'@types/node-fetch@2.6.13':
|
'@types/node-fetch@2.6.13':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.2.2
|
'@types/node': 25.2.3
|
||||||
form-data: 4.0.5
|
form-data: 4.0.5
|
||||||
|
|
||||||
'@types/node-forge@1.3.14':
|
'@types/node-forge@1.3.14':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.2.2
|
'@types/node': 25.2.3
|
||||||
|
|
||||||
'@types/node@18.19.130':
|
'@types/node@18.19.130':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 5.26.5
|
undici-types: 5.26.5
|
||||||
|
|
||||||
'@types/node@22.19.10':
|
'@types/node@22.19.11':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.21.0
|
undici-types: 6.21.0
|
||||||
|
|
||||||
'@types/node@25.2.2':
|
'@types/node@25.2.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 7.16.0
|
undici-types: 7.16.0
|
||||||
|
|
||||||
@@ -6366,22 +6360,22 @@ snapshots:
|
|||||||
|
|
||||||
'@types/send@1.2.1':
|
'@types/send@1.2.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.2.2
|
'@types/node': 25.2.3
|
||||||
|
|
||||||
'@types/serve-static@2.2.0':
|
'@types/serve-static@2.2.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/http-errors': 2.0.5
|
'@types/http-errors': 2.0.5
|
||||||
'@types/node': 25.2.2
|
'@types/node': 25.2.3
|
||||||
|
|
||||||
'@types/symbol-tree@3.2.5': {}
|
'@types/symbol-tree@3.2.5': {}
|
||||||
|
|
||||||
'@types/tar-stream@3.1.4':
|
'@types/tar-stream@3.1.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.2.2
|
'@types/node': 25.2.3
|
||||||
|
|
||||||
'@types/through2@2.0.41':
|
'@types/through2@2.0.41':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.2.2
|
'@types/node': 25.2.3
|
||||||
|
|
||||||
'@types/trusted-types@2.0.7': {}
|
'@types/trusted-types@2.0.7': {}
|
||||||
|
|
||||||
@@ -6407,11 +6401,11 @@ snapshots:
|
|||||||
|
|
||||||
'@types/ws@8.18.1':
|
'@types/ws@8.18.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.2.2
|
'@types/node': 25.2.3
|
||||||
|
|
||||||
'@types/yauzl@2.10.3':
|
'@types/yauzl@2.10.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.2.2
|
'@types/node': 25.2.3
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@ungap/structured-clone@1.3.0': {}
|
'@ungap/structured-clone@1.3.0': {}
|
||||||
@@ -6494,6 +6488,10 @@ snapshots:
|
|||||||
|
|
||||||
balanced-match@1.0.2: {}
|
balanced-match@1.0.2: {}
|
||||||
|
|
||||||
|
balanced-match@4.0.2:
|
||||||
|
dependencies:
|
||||||
|
jackspeak: 4.2.3
|
||||||
|
|
||||||
bare-events@2.8.2: {}
|
bare-events@2.8.2: {}
|
||||||
|
|
||||||
bare-fs@4.5.3:
|
bare-fs@4.5.3:
|
||||||
@@ -6564,6 +6562,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
balanced-match: 1.0.2
|
balanced-match: 1.0.2
|
||||||
|
|
||||||
|
brace-expansion@5.0.2:
|
||||||
|
dependencies:
|
||||||
|
balanced-match: 4.0.2
|
||||||
|
|
||||||
broadcast-channel@7.2.0:
|
broadcast-channel@7.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.28.4
|
'@babel/runtime': 7.28.4
|
||||||
@@ -6832,7 +6834,7 @@ snapshots:
|
|||||||
engine.io@6.6.4:
|
engine.io@6.6.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/cors': 2.8.19
|
'@types/cors': 2.8.19
|
||||||
'@types/node': 25.2.2
|
'@types/node': 25.2.3
|
||||||
accepts: 1.3.8
|
accepts: 1.3.8
|
||||||
base64id: 2.0.0
|
base64id: 2.0.0
|
||||||
cookie: 0.7.2
|
cookie: 0.7.2
|
||||||
@@ -7157,7 +7159,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
foreground-child: 3.3.1
|
foreground-child: 3.3.1
|
||||||
jackspeak: 4.2.3
|
jackspeak: 4.2.3
|
||||||
minimatch: 10.1.2
|
minimatch: 10.2.0
|
||||||
minipass: 7.1.2
|
minipass: 7.1.2
|
||||||
package-json-from-dist: 1.0.1
|
package-json-from-dist: 1.0.1
|
||||||
path-scurry: 2.0.1
|
path-scurry: 2.0.1
|
||||||
@@ -7862,9 +7864,9 @@ snapshots:
|
|||||||
|
|
||||||
minimalistic-crypto-utils@1.0.1: {}
|
minimalistic-crypto-utils@1.0.1: {}
|
||||||
|
|
||||||
minimatch@10.1.2:
|
minimatch@10.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@isaacs/brace-expansion': 5.0.1
|
brace-expansion: 5.0.2
|
||||||
|
|
||||||
minimatch@3.1.2:
|
minimatch@3.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
@@ -364,6 +364,10 @@ impl TcpListenerManager {
|
|||||||
// doesn't send initial data (e.g., SMTP, greeting-based protocols).
|
// doesn't send initial data (e.g., SMTP, greeting-based protocols).
|
||||||
// If a route matches by port alone and doesn't need domain/path/TLS info,
|
// If a route matches by port alone and doesn't need domain/path/TLS info,
|
||||||
// we can forward immediately without waiting for client data.
|
// we can forward immediately without waiting for client data.
|
||||||
|
//
|
||||||
|
// IMPORTANT: HTTP connections must NOT use this path — they need the HTTP
|
||||||
|
// proxy for proper error responses, CORS handling, and request-level routing.
|
||||||
|
// We detect HTTP via a non-blocking peek before committing to raw forwarding.
|
||||||
{
|
{
|
||||||
let quick_ctx = rustproxy_routing::MatchContext {
|
let quick_ctx = rustproxy_routing::MatchContext {
|
||||||
port,
|
port,
|
||||||
@@ -384,7 +388,28 @@ impl TcpListenerManager {
|
|||||||
|
|
||||||
// Only use fast path for simple port-only forward routes with no TLS
|
// Only use fast path for simple port-only forward routes with no TLS
|
||||||
if has_no_domain && has_no_path && is_forward && has_no_tls {
|
if has_no_domain && has_no_path && is_forward && has_no_tls {
|
||||||
if let Some(target) = quick_match.target {
|
// Non-blocking peek: if client has already sent data that looks
|
||||||
|
// like HTTP, skip fast path and let the normal path handle it
|
||||||
|
// through the HTTP proxy (for CORS, error responses, path routing).
|
||||||
|
let is_likely_http = {
|
||||||
|
let mut probe = [0u8; 16];
|
||||||
|
// Brief peek: HTTP clients send data immediately after connect.
|
||||||
|
// Server-speaks-first protocols (SMTP etc.) send nothing initially.
|
||||||
|
// 10ms is ample for any HTTP client while negligible for
|
||||||
|
// server-speaks-first protocols (which wait seconds for greeting).
|
||||||
|
match tokio::time::timeout(
|
||||||
|
std::time::Duration::from_millis(10),
|
||||||
|
stream.peek(&mut probe),
|
||||||
|
).await {
|
||||||
|
Ok(Ok(n)) if n > 0 => sni_parser::is_http(&probe[..n]),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if is_likely_http {
|
||||||
|
debug!("Fast-path skipped: HTTP detected from {}, using HTTP proxy", peer_addr);
|
||||||
|
// Fall through to normal path for HTTP proxy handling
|
||||||
|
} else if let Some(target) = quick_match.target {
|
||||||
let target_host = target.host.first().to_string();
|
let target_host = target.host.first().to_string();
|
||||||
let target_port = target.port.resolve(port);
|
let target_port = target.port.resolve(port);
|
||||||
let route_id = quick_match.route.id.as_deref();
|
let route_id = quick_match.route.id.as_deref();
|
||||||
@@ -562,6 +587,17 @@ impl TcpListenerManager {
|
|||||||
Some(rm) => rm,
|
Some(rm) => rm,
|
||||||
None => {
|
None => {
|
||||||
debug!("No route matched for port {} domain {:?}", port, domain);
|
debug!("No route matched for port {} domain {:?}", port, domain);
|
||||||
|
if is_http {
|
||||||
|
// Send a proper HTTP error instead of dropping the connection
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
|
let body = "No route matched";
|
||||||
|
let resp = format!(
|
||||||
|
"HTTP/1.1 502 Bad Gateway\r\nContent-Type: text/plain\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
||||||
|
body.len(), body
|
||||||
|
);
|
||||||
|
let _ = stream.write_all(resp.as_bytes()).await;
|
||||||
|
let _ = stream.shutdown().await;
|
||||||
|
}
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ struct Cli {
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
|
// Install the default CryptoProvider early, before any TLS or ACME code runs.
|
||||||
|
// This prevents panics from instant-acme/hyper-rustls calling ClientConfig::builder()
|
||||||
|
// before TLS listeners have started. Idempotent — later calls harmlessly return Err.
|
||||||
|
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||||
|
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
// Initialize tracing - write to stderr so stdout is reserved for management IPC
|
// Initialize tracing - write to stderr so stdout is reserved for management IPC
|
||||||
|
|||||||
123
test/test.bun.ts
Normal file
123
test/test.bun.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createHttpsTerminateRoute,
|
||||||
|
createCompleteHttpsServer,
|
||||||
|
createHttpRoute,
|
||||||
|
} from '../ts/proxies/smart-proxy/utils/route-helpers.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
mergeRouteConfigs,
|
||||||
|
cloneRoute,
|
||||||
|
routeMatchesPath,
|
||||||
|
} from '../ts/proxies/smart-proxy/utils/route-utils.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
validateRoutes,
|
||||||
|
validateRouteConfig,
|
||||||
|
} from '../ts/proxies/smart-proxy/utils/route-validator.js';
|
||||||
|
|
||||||
|
import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js';
|
||||||
|
|
||||||
|
tap.test('route creation - createHttpsTerminateRoute produces correct structure', async () => {
|
||||||
|
const route = createHttpsTerminateRoute('secure.example.com', { host: '127.0.0.1', port: 8443 });
|
||||||
|
expect(route).toHaveProperty('match');
|
||||||
|
expect(route).toHaveProperty('action');
|
||||||
|
expect(route.action.type).toEqual('forward');
|
||||||
|
expect(route.action.tls).toBeDefined();
|
||||||
|
expect(route.action.tls!.mode).toEqual('terminate');
|
||||||
|
expect(route.match.domains).toEqual('secure.example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('route creation - createCompleteHttpsServer returns redirect and main route', async () => {
|
||||||
|
const routes = createCompleteHttpsServer('app.example.com', { host: '127.0.0.1', port: 3000 });
|
||||||
|
expect(routes).toBeArray();
|
||||||
|
expect(routes.length).toBeGreaterThanOrEqual(2);
|
||||||
|
// Should have an HTTP→HTTPS redirect and an HTTPS route
|
||||||
|
const hasRedirect = routes.some((r) => r.action.type === 'forward' && r.action.redirect !== undefined);
|
||||||
|
const hasHttps = routes.some((r) => r.action.tls?.mode === 'terminate');
|
||||||
|
expect(hasRedirect || hasHttps).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('route validation - validateRoutes on a set of routes', async () => {
|
||||||
|
const routes: IRouteConfig[] = [
|
||||||
|
createHttpRoute('a.com', { host: '127.0.0.1', port: 3000 }),
|
||||||
|
createHttpRoute('b.com', { host: '127.0.0.1', port: 4000 }),
|
||||||
|
];
|
||||||
|
const result = validateRoutes(routes);
|
||||||
|
expect(result.valid).toBeTrue();
|
||||||
|
expect(result.errors).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('route validation - validateRoutes catches invalid route in set', async () => {
|
||||||
|
const routes: any[] = [
|
||||||
|
createHttpRoute('valid.com', { host: '127.0.0.1', port: 3000 }),
|
||||||
|
{ match: { ports: 80 } }, // missing action
|
||||||
|
];
|
||||||
|
const result = validateRoutes(routes);
|
||||||
|
expect(result.valid).toBeFalse();
|
||||||
|
expect(result.errors.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('path matching - routeMatchesPath with exact path', async () => {
|
||||||
|
const route = createHttpRoute('example.com', { host: '127.0.0.1', port: 3000 });
|
||||||
|
route.match.path = '/api';
|
||||||
|
expect(routeMatchesPath(route, '/api')).toBeTrue();
|
||||||
|
expect(routeMatchesPath(route, '/other')).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('path matching - route without path matches everything', async () => {
|
||||||
|
const route = createHttpRoute('example.com', { host: '127.0.0.1', port: 3000 });
|
||||||
|
// No path set, should match any path
|
||||||
|
expect(routeMatchesPath(route, '/anything')).toBeTrue();
|
||||||
|
expect(routeMatchesPath(route, '/')).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('route merging - mergeRouteConfigs combines routes', async () => {
|
||||||
|
const base = createHttpRoute('example.com', { host: '127.0.0.1', port: 3000 });
|
||||||
|
base.priority = 10;
|
||||||
|
base.name = 'base-route';
|
||||||
|
|
||||||
|
const merged = mergeRouteConfigs(base, {
|
||||||
|
priority: 50,
|
||||||
|
name: 'merged-route',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(merged.priority).toEqual(50);
|
||||||
|
expect(merged.name).toEqual('merged-route');
|
||||||
|
// Original route fields should be preserved
|
||||||
|
expect(merged.match.domains).toEqual('example.com');
|
||||||
|
expect(merged.action.targets![0].host).toEqual('127.0.0.1');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('route merging - mergeRouteConfigs does not mutate original', async () => {
|
||||||
|
const base = createHttpRoute('example.com', { host: '127.0.0.1', port: 3000 });
|
||||||
|
base.name = 'original';
|
||||||
|
|
||||||
|
const merged = mergeRouteConfigs(base, { name: 'changed' });
|
||||||
|
expect(base.name).toEqual('original');
|
||||||
|
expect(merged.name).toEqual('changed');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('route cloning - cloneRoute produces independent copy', async () => {
|
||||||
|
const original = createHttpRoute('example.com', { host: '127.0.0.1', port: 3000 });
|
||||||
|
original.priority = 42;
|
||||||
|
original.name = 'original-route';
|
||||||
|
|
||||||
|
const cloned = cloneRoute(original);
|
||||||
|
|
||||||
|
// Should be equal in value
|
||||||
|
expect(cloned.match.domains).toEqual('example.com');
|
||||||
|
expect(cloned.priority).toEqual(42);
|
||||||
|
expect(cloned.name).toEqual('original-route');
|
||||||
|
expect(cloned.action.targets![0].host).toEqual('127.0.0.1');
|
||||||
|
expect(cloned.action.targets![0].port).toEqual(3000);
|
||||||
|
|
||||||
|
// Should be independent - modifying clone shouldn't affect original
|
||||||
|
cloned.name = 'cloned-route';
|
||||||
|
cloned.priority = 99;
|
||||||
|
expect(original.name).toEqual('original-route');
|
||||||
|
expect(original.priority).toEqual(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
||||||
111
test/test.deno.ts
Normal file
111
test/test.deno.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createHttpRoute,
|
||||||
|
createHttpsTerminateRoute,
|
||||||
|
createLoadBalancerRoute,
|
||||||
|
} from '../ts/proxies/smart-proxy/utils/route-helpers.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
findMatchingRoutes,
|
||||||
|
findBestMatchingRoute,
|
||||||
|
routeMatchesDomain,
|
||||||
|
routeMatchesPort,
|
||||||
|
routeMatchesPath,
|
||||||
|
} from '../ts/proxies/smart-proxy/utils/route-utils.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
validateRouteConfig,
|
||||||
|
isValidDomain,
|
||||||
|
isValidPort,
|
||||||
|
} from '../ts/proxies/smart-proxy/utils/route-validator.js';
|
||||||
|
|
||||||
|
import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js';
|
||||||
|
|
||||||
|
tap.test('route creation - createHttpRoute produces correct structure', async () => {
|
||||||
|
const route = createHttpRoute('example.com', { host: '127.0.0.1', port: 3000 });
|
||||||
|
expect(route).toHaveProperty('match');
|
||||||
|
expect(route).toHaveProperty('action');
|
||||||
|
expect(route.match.domains).toEqual('example.com');
|
||||||
|
expect(route.action.type).toEqual('forward');
|
||||||
|
expect(route.action.targets).toBeArray();
|
||||||
|
expect(route.action.targets![0].host).toEqual('127.0.0.1');
|
||||||
|
expect(route.action.targets![0].port).toEqual(3000);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('route creation - createHttpRoute with array of domains', async () => {
|
||||||
|
const route = createHttpRoute(['a.com', 'b.com'], { host: 'localhost', port: 8080 });
|
||||||
|
expect(route.match.domains).toEqual(['a.com', 'b.com']);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('route validation - validateRouteConfig accepts valid route', async () => {
|
||||||
|
const route = createHttpRoute('valid.example.com', { host: '10.0.0.1', port: 8080 });
|
||||||
|
const result = validateRouteConfig(route);
|
||||||
|
expect(result.valid).toBeTrue();
|
||||||
|
expect(result.errors).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('route validation - validateRouteConfig rejects missing action', async () => {
|
||||||
|
const badRoute = { match: { ports: 80 } } as any;
|
||||||
|
const result = validateRouteConfig(badRoute);
|
||||||
|
expect(result.valid).toBeFalse();
|
||||||
|
expect(result.errors.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('route validation - isValidDomain checks correctly', async () => {
|
||||||
|
expect(isValidDomain('example.com')).toBeTrue();
|
||||||
|
expect(isValidDomain('*.example.com')).toBeTrue();
|
||||||
|
expect(isValidDomain('')).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('route validation - isValidPort checks correctly', async () => {
|
||||||
|
expect(isValidPort(80)).toBeTrue();
|
||||||
|
expect(isValidPort(443)).toBeTrue();
|
||||||
|
expect(isValidPort(0)).toBeFalse();
|
||||||
|
expect(isValidPort(70000)).toBeFalse();
|
||||||
|
expect(isValidPort(-1)).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('domain matching - exact domain', async () => {
|
||||||
|
const route = createHttpRoute('example.com', { host: '127.0.0.1', port: 3000 });
|
||||||
|
expect(routeMatchesDomain(route, 'example.com')).toBeTrue();
|
||||||
|
expect(routeMatchesDomain(route, 'other.com')).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('domain matching - wildcard domain', async () => {
|
||||||
|
const route = createHttpRoute('*.example.com', { host: '127.0.0.1', port: 3000 });
|
||||||
|
expect(routeMatchesDomain(route, 'sub.example.com')).toBeTrue();
|
||||||
|
expect(routeMatchesDomain(route, 'example.com')).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('port matching - single port', async () => {
|
||||||
|
const route = createHttpRoute('example.com', { host: '127.0.0.1', port: 3000 });
|
||||||
|
// createHttpRoute defaults to port 80
|
||||||
|
expect(routeMatchesPort(route, 80)).toBeTrue();
|
||||||
|
expect(routeMatchesPort(route, 443)).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('route finding - findBestMatchingRoute selects by priority', async () => {
|
||||||
|
const lowPriority = createHttpRoute('example.com', { host: '127.0.0.1', port: 3000 });
|
||||||
|
lowPriority.priority = 10;
|
||||||
|
|
||||||
|
const highPriority = createHttpRoute('example.com', { host: '127.0.0.1', port: 4000 });
|
||||||
|
highPriority.priority = 100;
|
||||||
|
|
||||||
|
const routes: IRouteConfig[] = [lowPriority, highPriority];
|
||||||
|
const best = findBestMatchingRoute(routes, { domain: 'example.com', port: 80 });
|
||||||
|
expect(best).toBeDefined();
|
||||||
|
expect(best!.priority).toEqual(100);
|
||||||
|
expect(best!.action.targets![0].port).toEqual(4000);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('route finding - findMatchingRoutes returns all matches', async () => {
|
||||||
|
const route1 = createHttpRoute('example.com', { host: '127.0.0.1', port: 3000 });
|
||||||
|
const route2 = createHttpRoute('example.com', { host: '127.0.0.1', port: 4000 });
|
||||||
|
const route3 = createHttpRoute('other.com', { host: '127.0.0.1', port: 5000 });
|
||||||
|
|
||||||
|
const matches = findMatchingRoutes([route1, route2, route3], { domain: 'example.com', port: 80 });
|
||||||
|
expect(matches).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
||||||
@@ -6,42 +6,49 @@ import { SmartProxy } from '../ts/index.js';
|
|||||||
let testProxy: SmartProxy;
|
let testProxy: SmartProxy;
|
||||||
let targetServer: net.Server;
|
let targetServer: net.Server;
|
||||||
|
|
||||||
|
const ECHO_PORT = 47200;
|
||||||
|
const PROXY_PORT = 47201;
|
||||||
|
|
||||||
// Create a simple echo server as target
|
// Create a simple echo server as target
|
||||||
tap.test('setup test environment', async () => {
|
tap.test('setup test environment', async () => {
|
||||||
// Create target server that echoes data back
|
// Create target server that echoes data back
|
||||||
targetServer = net.createServer((socket) => {
|
targetServer = net.createServer((socket) => {
|
||||||
console.log('Target server: client connected');
|
console.log('Target server: client connected');
|
||||||
|
|
||||||
// Echo data back
|
// Echo data back
|
||||||
socket.on('data', (data) => {
|
socket.on('data', (data) => {
|
||||||
console.log(`Target server received: ${data.toString().trim()}`);
|
console.log(`Target server received: ${data.toString().trim()}`);
|
||||||
socket.write(data);
|
socket.write(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('close', () => {
|
socket.on('close', () => {
|
||||||
console.log('Target server: client disconnected');
|
console.log('Target server: client disconnected');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
targetServer.listen(9876, () => {
|
targetServer.on('error', (err) => {
|
||||||
console.log('Target server listening on port 9876');
|
console.error(`Echo server error: ${err.message}`);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
targetServer.listen(ECHO_PORT, () => {
|
||||||
|
console.log(`Target server listening on port ${ECHO_PORT}`);
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create proxy with simple TCP forwarding (no TLS)
|
// Create proxy with simple TCP forwarding (no TLS)
|
||||||
testProxy = new SmartProxy({
|
testProxy = new SmartProxy({
|
||||||
routes: [{
|
routes: [{
|
||||||
name: 'tcp-forward-test',
|
name: 'tcp-forward-test',
|
||||||
match: {
|
match: {
|
||||||
ports: 8888 // Plain TCP port
|
ports: PROXY_PORT // Plain TCP port
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
targets: [{
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 9876
|
port: ECHO_PORT
|
||||||
}]
|
}]
|
||||||
// No TLS configuration - just plain TCP forwarding
|
// No TLS configuration - just plain TCP forwarding
|
||||||
}
|
}
|
||||||
@@ -49,7 +56,7 @@ tap.test('setup test environment', async () => {
|
|||||||
defaults: {
|
defaults: {
|
||||||
target: {
|
target: {
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 9876
|
port: ECHO_PORT
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
enableDetailedLogging: true,
|
enableDetailedLogging: true,
|
||||||
@@ -59,72 +66,72 @@ tap.test('setup test environment', async () => {
|
|||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
keepAliveInitialDelay: 1000
|
keepAliveInitialDelay: 1000
|
||||||
});
|
});
|
||||||
|
|
||||||
await testProxy.start();
|
await testProxy.start();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should keep WebSocket-like connection open for extended period', async (tools) => {
|
tap.test('should keep WebSocket-like connection open for extended period', async (tools) => {
|
||||||
tools.timeout(60000); // 60 second test timeout
|
tools.timeout(15000); // 15 second test timeout
|
||||||
|
|
||||||
const client = new net.Socket();
|
const client = new net.Socket();
|
||||||
let messagesReceived = 0;
|
let messagesReceived = 0;
|
||||||
let connectionClosed = false;
|
let connectionClosed = false;
|
||||||
|
|
||||||
// Connect to proxy
|
// Connect to proxy
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
client.connect(8888, 'localhost', () => {
|
client.connect(PROXY_PORT, 'localhost', () => {
|
||||||
console.log('Client connected to proxy');
|
console.log('Client connected to proxy');
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on('error', reject);
|
client.on('error', reject);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set up data handler
|
// Set up data handler
|
||||||
client.on('data', (data) => {
|
client.on('data', (data) => {
|
||||||
console.log(`Client received: ${data.toString().trim()}`);
|
console.log(`Client received: ${data.toString().trim()}`);
|
||||||
messagesReceived++;
|
messagesReceived++;
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on('close', () => {
|
client.on('close', () => {
|
||||||
console.log('Client connection closed');
|
console.log('Client connection closed');
|
||||||
connectionClosed = true;
|
connectionClosed = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Send initial handshake-like data
|
// Send initial handshake-like data
|
||||||
client.write('HELLO\n');
|
client.write('HELLO\n');
|
||||||
|
|
||||||
// Wait for response
|
// Wait for response
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
expect(messagesReceived).toEqual(1);
|
expect(messagesReceived).toEqual(1);
|
||||||
|
|
||||||
// Simulate WebSocket-like keep-alive pattern
|
// Simulate WebSocket-like keep-alive pattern
|
||||||
// Send periodic messages over 60 seconds
|
// Send periodic messages over 5 seconds
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
const pingInterval = setInterval(() => {
|
const pingInterval = setInterval(() => {
|
||||||
if (!connectionClosed && Date.now() - startTime < 60000) {
|
if (!connectionClosed && Date.now() - startTime < 5000) {
|
||||||
console.log('Sending ping...');
|
console.log('Sending ping...');
|
||||||
client.write('PING\n');
|
client.write('PING\n');
|
||||||
} else {
|
} else {
|
||||||
clearInterval(pingInterval);
|
clearInterval(pingInterval);
|
||||||
}
|
}
|
||||||
}, 10000); // Every 10 seconds
|
}, 1000); // Every 1 second
|
||||||
|
|
||||||
// Wait for 55 seconds (must complete within 60s runner timeout)
|
// Wait for 5 seconds — sufficient to verify the connection stays open
|
||||||
await new Promise(resolve => setTimeout(resolve, 55000));
|
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||||
|
|
||||||
// Clean up interval
|
// Clean up interval
|
||||||
clearInterval(pingInterval);
|
clearInterval(pingInterval);
|
||||||
|
|
||||||
// Connection should still be open
|
// Connection should still be open
|
||||||
expect(connectionClosed).toEqual(false);
|
expect(connectionClosed).toEqual(false);
|
||||||
|
|
||||||
// Should have received responses (1 hello + 6 pings)
|
// Should have received responses (1 hello + ~5 pings)
|
||||||
expect(messagesReceived).toBeGreaterThan(5);
|
expect(messagesReceived).toBeGreaterThan(3);
|
||||||
|
|
||||||
// Close connection gracefully
|
// Close connection gracefully
|
||||||
client.end();
|
client.end();
|
||||||
|
|
||||||
// Wait for close
|
// Wait for close
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
expect(connectionClosed).toEqual(true);
|
expect(connectionClosed).toEqual(true);
|
||||||
@@ -134,7 +141,7 @@ tap.test('should keep WebSocket-like connection open for extended period', async
|
|||||||
|
|
||||||
tap.test('cleanup', async () => {
|
tap.test('cleanup', async () => {
|
||||||
await testProxy.stop();
|
await testProxy.stop();
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
targetServer.close(() => {
|
targetServer.close(() => {
|
||||||
console.log('Target server closed');
|
console.log('Target server closed');
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import * as net from 'net';
|
|||||||
|
|
||||||
let smartProxyInstance: SmartProxy;
|
let smartProxyInstance: SmartProxy;
|
||||||
let echoServer: net.Server;
|
let echoServer: net.Server;
|
||||||
const echoServerPort = 9876;
|
const echoServerPort = 47300;
|
||||||
const proxyPort = 8080;
|
const proxyPort = 47301;
|
||||||
|
|
||||||
// Create an echo server for testing
|
// Create an echo server for testing
|
||||||
tap.test('should create echo server for testing', async () => {
|
tap.test('should create echo server for testing', async () => {
|
||||||
@@ -16,7 +16,11 @@ tap.test('should create echo server for testing', async () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
echoServer.on('error', (err) => {
|
||||||
|
console.error(`Echo server error: ${err.message}`);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
echoServer.listen(echoServerPort, () => {
|
echoServer.listen(echoServerPort, () => {
|
||||||
console.log(`Echo server listening on port ${echoServerPort}`);
|
console.log(`Echo server listening on port ${echoServerPort}`);
|
||||||
resolve();
|
resolve();
|
||||||
@@ -265,4 +269,4 @@ tap.test('should clean up resources', async () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
export default tap.start();
|
export default tap.start();
|
||||||
@@ -5,19 +5,27 @@ import { SmartProxy } from '../ts/proxies/smart-proxy/smart-proxy.js';
|
|||||||
let echoServer: net.Server;
|
let echoServer: net.Server;
|
||||||
let proxy: SmartProxy;
|
let proxy: SmartProxy;
|
||||||
|
|
||||||
|
const ECHO_PORT = 47400;
|
||||||
|
const PROXY_PORT_1 = 47401;
|
||||||
|
const PROXY_PORT_2 = 47402;
|
||||||
|
|
||||||
tap.test('port forwarding should not immediately close connections', async (tools) => {
|
tap.test('port forwarding should not immediately close connections', async (tools) => {
|
||||||
// Set a timeout for this test
|
// Set a timeout for this test
|
||||||
tools.timeout(10000); // 10 seconds
|
tools.timeout(10000); // 10 seconds
|
||||||
// Create an echo server
|
// Create an echo server
|
||||||
echoServer = await new Promise<net.Server>((resolve) => {
|
echoServer = await new Promise<net.Server>((resolve, reject) => {
|
||||||
const server = net.createServer((socket) => {
|
const server = net.createServer((socket) => {
|
||||||
socket.on('data', (data) => {
|
socket.on('data', (data) => {
|
||||||
socket.write(`ECHO: ${data}`);
|
socket.write(`ECHO: ${data}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
server.listen(8888, () => {
|
server.on('error', (err) => {
|
||||||
console.log('Echo server listening on port 8888');
|
console.error(`Echo server error: ${err.message}`);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
server.listen(ECHO_PORT, () => {
|
||||||
|
console.log(`Echo server listening on port ${ECHO_PORT}`);
|
||||||
resolve(server);
|
resolve(server);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -26,10 +34,10 @@ tap.test('port forwarding should not immediately close connections', async (tool
|
|||||||
proxy = new SmartProxy({
|
proxy = new SmartProxy({
|
||||||
routes: [{
|
routes: [{
|
||||||
name: 'test-forward',
|
name: 'test-forward',
|
||||||
match: { ports: 9999 },
|
match: { ports: PROXY_PORT_1 },
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
targets: [{ host: 'localhost', port: 8888 }]
|
targets: [{ host: 'localhost', port: ECHO_PORT }]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
@@ -37,21 +45,24 @@ tap.test('port forwarding should not immediately close connections', async (tool
|
|||||||
await proxy.start();
|
await proxy.start();
|
||||||
|
|
||||||
// Test connection through proxy
|
// Test connection through proxy
|
||||||
const client = net.createConnection(9999, 'localhost');
|
const client = net.createConnection(PROXY_PORT_1, 'localhost');
|
||||||
|
|
||||||
const result = await new Promise<string>((resolve, reject) => {
|
const result = await new Promise<string>((resolve, reject) => {
|
||||||
client.on('data', (data) => {
|
client.on('data', (data) => {
|
||||||
const response = data.toString();
|
const response = data.toString();
|
||||||
client.end(); // Close the connection after receiving data
|
client.end(); // Close the connection after receiving data
|
||||||
resolve(response);
|
resolve(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on('error', reject);
|
client.on('error', reject);
|
||||||
|
|
||||||
client.write('Hello');
|
client.write('Hello');
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toEqual('ECHO: Hello');
|
expect(result).toEqual('ECHO: Hello');
|
||||||
|
|
||||||
|
// Stop proxy from test 1 before test 2 reassigns the variable
|
||||||
|
await proxy.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('TLS passthrough should work correctly', async () => {
|
tap.test('TLS passthrough should work correctly', async () => {
|
||||||
@@ -59,7 +70,7 @@ tap.test('TLS passthrough should work correctly', async () => {
|
|||||||
proxy = new SmartProxy({
|
proxy = new SmartProxy({
|
||||||
routes: [{
|
routes: [{
|
||||||
name: 'tls-test',
|
name: 'tls-test',
|
||||||
match: { ports: 8443, domains: 'test.example.com' },
|
match: { ports: PROXY_PORT_2, domains: 'test.example.com' },
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
tls: { mode: 'passthrough' },
|
tls: { mode: 'passthrough' },
|
||||||
@@ -85,16 +96,6 @@ tap.test('cleanup', async () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (proxy) {
|
|
||||||
await proxy.stop();
|
|
||||||
console.log('Proxy stopped');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default tap.start().then(() => {
|
export default tap.start();
|
||||||
// Force exit after tests complete
|
|
||||||
setTimeout(() => {
|
|
||||||
console.log('Forcing process exit');
|
|
||||||
process.exit(0);
|
|
||||||
}, 1000);
|
|
||||||
});
|
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartproxy',
|
name: '@push.rocks/smartproxy',
|
||||||
version: '23.1.0',
|
version: '23.1.6',
|
||||||
description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.'
|
description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as net from 'net';
|
import * as net from 'node:net';
|
||||||
import { WrappedSocket } from './wrapped-socket.js';
|
import { WrappedSocket } from './wrapped-socket.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { LifecycleComponent } from './lifecycle-component.js';
|
import { LifecycleComponent } from './lifecycle-component.js';
|
||||||
import { BinaryHeap } from './binary-heap.js';
|
import { BinaryHeap } from './binary-heap.js';
|
||||||
import { AsyncMutex } from './async-utils.js';
|
import { AsyncMutex } from './async-utils.js';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'node:events';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for pooled connection
|
* Interface for pooled connection
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Provides standardized socket cleanup with proper listener and timer management
|
* Provides standardized socket cleanup with proper listener and timer management
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Socket } from 'net';
|
import type { Socket } from 'node:net';
|
||||||
|
|
||||||
export type SocketTracked = {
|
export type SocketTracked = {
|
||||||
cleanup: () => void;
|
cleanup: () => void;
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
// node native scope
|
// node native scope
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'node:events';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'node:fs';
|
||||||
import * as http from 'http';
|
import * as http from 'node:http';
|
||||||
import * as https from 'https';
|
import * as https from 'node:https';
|
||||||
import * as net from 'net';
|
import * as net from 'node:net';
|
||||||
import * as path from 'path';
|
import * as path from 'node:path';
|
||||||
import * as tls from 'tls';
|
import * as tls from 'node:tls';
|
||||||
import * as url from 'url';
|
import * as url from 'node:url';
|
||||||
import * as http2 from 'http2';
|
import * as http2 from 'node:http2';
|
||||||
|
|
||||||
export { EventEmitter, fs, http, https, net, path, tls, url, http2 };
|
export { EventEmitter, fs, http, https, net, path, tls, url, http2 };
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* that may span multiple TCP packets.
|
* that may span multiple TCP packets.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Buffer } from 'buffer';
|
import { Buffer } from 'node:buffer';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fragment tracking information
|
* Fragment tracking information
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Buffer } from 'buffer';
|
import { Buffer } from 'node:buffer';
|
||||||
import {
|
import {
|
||||||
TlsRecordType,
|
TlsRecordType,
|
||||||
TlsHandshakeType,
|
TlsHandshakeType,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Buffer } from 'buffer';
|
import { Buffer } from 'node:buffer';
|
||||||
import { TlsExtensionType, TlsUtils } from '../utils/tls-utils.js';
|
import { TlsExtensionType, TlsUtils } from '../utils/tls-utils.js';
|
||||||
import {
|
import {
|
||||||
ClientHelloParser,
|
ClientHelloParser,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* WebSocket Protocol Utilities
|
* WebSocket Protocol Utilities
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as crypto from 'crypto';
|
import * as crypto from 'node:crypto';
|
||||||
import { WEBSOCKET_MAGIC_STRING } from './constants.js';
|
import { WEBSOCKET_MAGIC_STRING } from './constants.js';
|
||||||
import type { RawData } from './types.js';
|
import type { RawData } from './types.js';
|
||||||
|
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ export class RustProxyBridge extends plugins.EventEmitter {
|
|||||||
envVarName: 'SMARTPROXY_RUST_BINARY',
|
envVarName: 'SMARTPROXY_RUST_BINARY',
|
||||||
platformPackagePrefix: '@push.rocks/smartproxy',
|
platformPackagePrefix: '@push.rocks/smartproxy',
|
||||||
localPaths: buildLocalPaths(),
|
localPaths: buildLocalPaths(),
|
||||||
|
maxPayloadSize: 100 * 1024 * 1024, // 100 MB – route configs with many entries can be large
|
||||||
logger: {
|
logger: {
|
||||||
log: (level: string, message: string, data?: Record<string, any>) => {
|
log: (level: string, message: string, data?: Record<string, any>) => {
|
||||||
logger.log(level as any, message, data);
|
logger.log(level as any, message, data);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { RouteValidator } from './utils/route-validator.js';
|
|||||||
import { Mutex } from './utils/mutex.js';
|
import { Mutex } from './utils/mutex.js';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import type { ISmartProxyOptions, TSmartProxyCertProvisionObject } from './models/interfaces.js';
|
import type { ISmartProxyOptions, TSmartProxyCertProvisionObject, IAcmeOptions } from './models/interfaces.js';
|
||||||
import type { IRouteConfig } from './models/route-types.js';
|
import type { IRouteConfig } from './models/route-types.js';
|
||||||
import type { IMetrics } from './models/metrics-types.js';
|
import type { IMetrics } from './models/metrics-types.js';
|
||||||
|
|
||||||
@@ -146,8 +146,16 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
// Preprocess routes (strip JS functions, convert socket-handler routes)
|
// Preprocess routes (strip JS functions, convert socket-handler routes)
|
||||||
const rustRoutes = this.preprocessor.preprocessForRust(this.settings.routes);
|
const rustRoutes = this.preprocessor.preprocessForRust(this.settings.routes);
|
||||||
|
|
||||||
|
// When certProvisionFunction handles cert provisioning,
|
||||||
|
// disable Rust's built-in ACME to prevent race condition.
|
||||||
|
let acmeForRust = this.settings.acme;
|
||||||
|
if (this.settings.certProvisionFunction && acmeForRust?.enabled) {
|
||||||
|
acmeForRust = { ...acmeForRust, enabled: false };
|
||||||
|
logger.log('info', 'Rust ACME disabled — certProvisionFunction will handle certificate provisioning', { component: 'smart-proxy' });
|
||||||
|
}
|
||||||
|
|
||||||
// Build Rust config
|
// Build Rust config
|
||||||
const config = this.buildRustConfig(rustRoutes);
|
const config = this.buildRustConfig(rustRoutes, acmeForRust);
|
||||||
|
|
||||||
// Start the Rust proxy
|
// Start the Rust proxy
|
||||||
await this.bridge.startProxy(config);
|
await this.bridge.startProxy(config);
|
||||||
@@ -334,20 +342,21 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* Build the Rust configuration object from TS settings.
|
* Build the Rust configuration object from TS settings.
|
||||||
*/
|
*/
|
||||||
private buildRustConfig(routes: IRouteConfig[]): any {
|
private buildRustConfig(routes: IRouteConfig[], acmeOverride?: IAcmeOptions): any {
|
||||||
|
const acme = acmeOverride !== undefined ? acmeOverride : this.settings.acme;
|
||||||
return {
|
return {
|
||||||
routes,
|
routes,
|
||||||
defaults: this.settings.defaults,
|
defaults: this.settings.defaults,
|
||||||
acme: this.settings.acme
|
acme: acme
|
||||||
? {
|
? {
|
||||||
enabled: this.settings.acme.enabled,
|
enabled: acme.enabled,
|
||||||
email: this.settings.acme.email,
|
email: acme.email,
|
||||||
useProduction: this.settings.acme.useProduction,
|
useProduction: acme.useProduction,
|
||||||
port: this.settings.acme.port,
|
port: acme.port,
|
||||||
renewThresholdDays: this.settings.acme.renewThresholdDays,
|
renewThresholdDays: acme.renewThresholdDays,
|
||||||
autoRenew: this.settings.acme.autoRenew,
|
autoRenew: acme.autoRenew,
|
||||||
certificateStore: this.settings.acme.certificateStore,
|
certificateStore: acme.certificateStore,
|
||||||
renewCheckIntervalHours: this.settings.acme.renewCheckIntervalHours,
|
renewCheckIntervalHours: acme.renewCheckIntervalHours,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
connectionTimeout: this.settings.connectionTimeout,
|
connectionTimeout: this.settings.connectionTimeout,
|
||||||
@@ -374,20 +383,31 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
const provisionFn = this.settings.certProvisionFunction;
|
const provisionFn = this.settings.certProvisionFunction;
|
||||||
if (!provisionFn) return;
|
if (!provisionFn) return;
|
||||||
|
|
||||||
|
const provisionedDomains = new Set<string>();
|
||||||
|
|
||||||
for (const route of this.settings.routes) {
|
for (const route of this.settings.routes) {
|
||||||
if (route.action.tls?.certificate !== 'auto') continue;
|
if (route.action.tls?.certificate !== 'auto') continue;
|
||||||
if (!route.match.domains) continue;
|
if (!route.match.domains) continue;
|
||||||
|
|
||||||
const domains = Array.isArray(route.match.domains) ? route.match.domains : [route.match.domains];
|
const rawDomains = Array.isArray(route.match.domains) ? route.match.domains : [route.match.domains];
|
||||||
|
const certDomains = this.normalizeDomainsForCertProvisioning(rawDomains);
|
||||||
for (const domain of domains) {
|
|
||||||
if (domain.includes('*')) continue;
|
|
||||||
|
|
||||||
|
for (const domain of certDomains) {
|
||||||
|
if (provisionedDomains.has(domain)) continue;
|
||||||
|
provisionedDomains.add(domain);
|
||||||
try {
|
try {
|
||||||
const result: TSmartProxyCertProvisionObject = await provisionFn(domain);
|
const result: TSmartProxyCertProvisionObject = await provisionFn(domain);
|
||||||
|
|
||||||
if (result === 'http01') {
|
if (result === 'http01') {
|
||||||
// Rust handles ACME for this domain
|
// Callback wants HTTP-01 for this domain — trigger Rust ACME explicitly
|
||||||
|
if (route.name) {
|
||||||
|
try {
|
||||||
|
await this.bridge.provisionCertificate(route.name);
|
||||||
|
logger.log('info', `Triggered Rust ACME for ${domain} (route: ${route.name})`, { component: 'smart-proxy' });
|
||||||
|
} catch (provisionErr: any) {
|
||||||
|
logger.log('warn', `Cannot provision cert for ${domain} — callback returned 'http01' but Rust ACME failed: ${provisionErr.message}`, { component: 'smart-proxy' });
|
||||||
|
}
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,6 +433,43 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize routing glob patterns into valid domain identifiers for cert provisioning.
|
||||||
|
* - `*nevermind.cloud` → `['nevermind.cloud', '*.nevermind.cloud']`
|
||||||
|
* - `*.lossless.digital` → `['*.lossless.digital']` (already valid wildcard)
|
||||||
|
* - `code.foss.global` → `['code.foss.global']` (plain domain)
|
||||||
|
* - `*mid*.example.com` → skipped with warning (unsupported glob)
|
||||||
|
*/
|
||||||
|
private normalizeDomainsForCertProvisioning(rawDomains: string[]): string[] {
|
||||||
|
const result: string[] = [];
|
||||||
|
for (const raw of rawDomains) {
|
||||||
|
// Plain domain — no glob characters
|
||||||
|
if (!raw.includes('*')) {
|
||||||
|
result.push(raw);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid wildcard: *.example.com
|
||||||
|
if (raw.startsWith('*.') && !raw.slice(2).includes('*')) {
|
||||||
|
result.push(raw);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Routing glob like *example.com (leading star, no dot after it)
|
||||||
|
// Convert to bare domain + wildcard pair
|
||||||
|
if (raw.startsWith('*') && !raw.startsWith('*.') && !raw.slice(1).includes('*')) {
|
||||||
|
const baseDomain = raw.slice(1); // Remove leading *
|
||||||
|
result.push(baseDomain);
|
||||||
|
result.push(`*.${baseDomain}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsupported glob pattern (e.g. *mid*.example.com)
|
||||||
|
logger.log('warn', `Skipping unsupported glob pattern for cert provisioning: ${raw}`, { component: 'smart-proxy' });
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private isValidDomain(domain: string): boolean {
|
private isValidDomain(domain: string): boolean {
|
||||||
if (!domain || domain.length === 0) return false;
|
if (!domain || domain.length === 0) return false;
|
||||||
if (domain.includes('*')) return false;
|
if (domain.includes('*')) return false;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Buffer } from 'buffer';
|
import { Buffer } from 'node:buffer';
|
||||||
import {
|
import {
|
||||||
TlsRecordType,
|
TlsRecordType,
|
||||||
TlsHandshakeType,
|
TlsHandshakeType,
|
||||||
|
|||||||
Reference in New Issue
Block a user