Compare commits

...

10 Commits

Author SHA1 Message Date
6b04bc612b v23.1.5
Some checks failed
Default (tags) / security (push) Successful in 38s
Default (tags) / test (push) Failing after 4m1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-13 12:02:47 +00:00
e774ec87ca fix(smart-proxy): provision certificates for wildcard domains instead of skipping them 2026-02-13 12:02:47 +00:00
cbde778f09 v23.1.4
Some checks failed
Default (tags) / security (push) Successful in 43s
Default (tags) / test (push) Failing after 4m6s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-12 22:35:25 +00:00
bc2bc874a5 fix(tests): make tests more robust and bump small dependencies 2026-02-12 22:35:25 +00:00
fdabf807b0 v23.1.3
Some checks failed
Default (tags) / security (push) Successful in 44s
Default (tags) / test (push) Failing after 4m6s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-12 20:17:32 +00:00
81e0e6b4d8 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 2026-02-12 20:17:32 +00:00
28fa69bf59 v23.1.2
Some checks failed
Default (tags) / security (push) Successful in 36s
Default (tags) / test (push) Failing after 4m1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-11 13:48:30 +00:00
5019658032 fix(core): use node: scoped builtin imports and add route unit tests 2026-02-11 13:48:30 +00:00
a9fe365c78 v23.1.1
Some checks failed
Default (tags) / security (push) Successful in 39s
Default (tags) / test (push) Failing after 4m1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-11 12:52:45 +00:00
32e0410227 fix(rust-proxy): increase rust proxy bridge maxPayloadSize to 100 MB and bump dependencies 2026-02-11 12:52:45 +00:00
23 changed files with 7794 additions and 144 deletions

View File

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

7324
deno.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

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

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

View File

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

View File

@@ -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
View 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
View 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();

View File

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

View File

@@ -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();

View File

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

View File

@@ -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.5',
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.'
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { Buffer } from 'buffer'; import { Buffer } from 'node:buffer';
import { import {
TlsRecordType, TlsRecordType,
TlsHandshakeType, TlsHandshakeType,

View File

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

View File

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

View File

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

View File

@@ -381,8 +381,6 @@ export class SmartProxy extends plugins.EventEmitter {
const domains = Array.isArray(route.match.domains) ? route.match.domains : [route.match.domains]; const domains = Array.isArray(route.match.domains) ? route.match.domains : [route.match.domains];
for (const domain of domains) { for (const domain of domains) {
if (domain.includes('*')) continue;
try { try {
const result: TSmartProxyCertProvisionObject = await provisionFn(domain); const result: TSmartProxyCertProvisionObject = await provisionFn(domain);

View File

@@ -1,4 +1,4 @@
import { Buffer } from 'buffer'; import { Buffer } from 'node:buffer';
import { import {
TlsRecordType, TlsRecordType,
TlsHandshakeType, TlsHandshakeType,