Compare commits

...

6 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
11 changed files with 208 additions and 139 deletions

View File

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

73
deno.lock generated
View File

@@ -14,7 +14,7 @@
"npm:@push.rocks/smartnetwork@^4.4.0": "4.4.0",
"npm:@push.rocks/smartpromise@^4.2.3": "4.2.3",
"npm:@push.rocks/smartrequest@^5.0.1": "5.0.1",
"npm:@push.rocks/smartrust@^1.2.0": "1.2.0",
"npm:@push.rocks/smartrust@^1.2.1": "1.2.1",
"npm:@push.rocks/smartrx@^3.0.10": "3.0.10",
"npm:@push.rocks/smartserve@^2.0.1": "2.0.1",
"npm:@push.rocks/smartstring@^4.1.0": "4.1.0",
@@ -23,7 +23,7 @@
"npm:@types/minimatch@6": "6.0.0",
"npm:@types/node@^25.2.3": "25.2.3",
"npm:@types/ws@^8.18.1": "8.18.1",
"npm:minimatch@^10.1.2": "10.1.2",
"npm:minimatch@^10.2.0": "10.2.0",
"npm:pretty-ms@^9.3.0": "9.3.0",
"npm:typescript@^5.9.3": "5.9.3",
"npm:why-is-node-running@^3.2.2": "3.2.2",
@@ -57,17 +57,13 @@
"integrity": "sha512-dcp0oXsjBL+XdFg1wUUP08uJQid5bQ0Yv3V3Y3lnI2QCbat0FU+Tsb0TZRnZ4+P150Vj/ITBqJUgDzFsF34grA==",
"dependencies": [
"@api.global/typedrequest",
"@api.global/typedrequest-interfaces@3.0.19",
"@api.global/typedsocket@3.1.1",
"@cloudflare/workers-types",
"@design.estate/dees-comms",
"@push.rocks/lik",
"@push.rocks/smartchok",
"@push.rocks/smartdelay",
"@push.rocks/smartenv@5.0.13",
"@push.rocks/smartfeed",
"@push.rocks/smartfile@11.2.7",
"@push.rocks/smartjson@5.2.0",
"@push.rocks/smartlog",
"@push.rocks/smartlog-destination-devtools",
"@push.rocks/smartlog-interfaces",
@@ -76,17 +72,12 @@
"@push.rocks/smartmime",
"@push.rocks/smartntml",
"@push.rocks/smartopen",
"@push.rocks/smartpath@6.0.0",
"@push.rocks/smartpromise",
"@push.rocks/smartrequest@4.4.2",
"@push.rocks/smartrx",
"@push.rocks/smartsitemap",
"@push.rocks/smartstream",
"@push.rocks/smarttime",
"@push.rocks/taskbuffer@3.5.0",
"@push.rocks/webrequest@3.0.37",
"@push.rocks/webstore",
"@tsclass/tsclass@9.3.0",
"@types/express",
"body-parser",
"cors",
@@ -143,9 +134,7 @@
"integrity": "sha512-Wkz3NlhmfdZMKqXXI2c2dMtGGmSmhdOegZiziL+9b2mqPYdc7Gd8AZRdEOKvbSoIvc9G22/5BEadIWHrfq66TA==",
"dependencies": [
"@api.global/typedrequest",
"@api.global/typedrequest-interfaces@3.0.19",
"@push.rocks/isohash",
"@push.rocks/smartjson@5.2.0",
"@push.rocks/smartrx",
"@push.rocks/smartserve",
"@push.rocks/smartsocket@2.1.0",
@@ -1305,17 +1294,6 @@
],
"tarball": "https://verdaccio.lossless.digital/@inquirer/type/-/type-2.0.0.tgz"
},
"@isaacs/balanced-match@4.0.1": {
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
"tarball": "https://verdaccio.lossless.digital/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz"
},
"@isaacs/brace-expansion@5.0.1": {
"integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==",
"dependencies": [
"@isaacs/balanced-match"
],
"tarball": "https://verdaccio.lossless.digital/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz"
},
"@isaacs/cliui@9.0.0": {
"integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==",
"tarball": "https://verdaccio.lossless.digital/@isaacs/cliui/-/cliui-9.0.0.tgz"
@@ -1748,7 +1726,7 @@
"@push.rocks/smartstring",
"@push.rocks/smartunique",
"@tsclass/tsclass@9.3.0",
"minimatch@10.1.2"
"minimatch@10.2.0"
],
"tarball": "https://verdaccio.lossless.digital/@push.rocks/smartbucket/-/smartbucket-4.4.1.tgz"
},
@@ -1853,7 +1831,7 @@
"acme-client",
"dns-packet",
"elliptic",
"minimatch@10.1.2"
"minimatch@10.2.0"
],
"tarball": "https://verdaccio.lossless.digital/@push.rocks/smartdns/-/smartdns-6.2.2.tgz"
},
@@ -1869,7 +1847,7 @@
"@types/dns-packet",
"acme-client",
"dns-packet",
"minimatch@10.1.2"
"minimatch@10.2.0"
],
"tarball": "https://verdaccio.lossless.digital/@push.rocks/smartdns/-/smartdns-7.8.0.tgz"
},
@@ -2259,12 +2237,12 @@
],
"tarball": "https://verdaccio.lossless.digital/@push.rocks/smartrouter/-/smartrouter-1.3.3.tgz"
},
"@push.rocks/smartrust@1.2.0": {
"integrity": "sha512-JlaALselIHoP6C3ceQbrvz424G21cND/QsH/KI3E/JrO4XphJiGZwM6f4yJWrijdPYR/YYMoaIiYN7ybZp0C4w==",
"@push.rocks/smartrust@1.2.1": {
"integrity": "sha512-ANwXXibUwoHNWF1hhXhXVVrfzYlhgHYRa2205Jkd/s/wXzcWHftYZthilJj+52B7nkzSB76umfxKfK5eBYY2Ug==",
"dependencies": [
"@push.rocks/smartpath@6.0.0"
],
"tarball": "https://verdaccio.lossless.digital/@push.rocks/smartrust/-/smartrust-1.2.0.tgz"
"tarball": "https://verdaccio.lossless.digital/@push.rocks/smartrust/-/smartrust-1.2.1.tgz"
},
"@push.rocks/smartrx@3.0.10": {
"integrity": "sha512-USjIYcsSfzn14cwOsxgq/bBmWDTTzy3ouWAnW5NdMyRRzEbmeNrvmy6TRqNeDlJ2PsYNTt1rr/zGUqvIy72ITg==",
@@ -2325,7 +2303,6 @@
"@push.rocks/smartsocket@2.1.0": {
"integrity": "sha512-etOGyfiDFQz/1WJnD3jFL2N7ykujTjiudAz6qZTz82xE5oabKuKX+Cn8SdM9dOwzyWmBUKbUdll8QhovAXjn+g==",
"dependencies": [
"@api.global/typedrequest-interfaces@3.0.19",
"@api.global/typedserver@3.0.80",
"@push.rocks/isohash",
"@push.rocks/isounique",
@@ -3507,7 +3484,7 @@
"@types/minimatch@6.0.0": {
"integrity": "sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA==",
"dependencies": [
"minimatch@10.1.2"
"minimatch@10.2.0"
],
"deprecated": true,
"tarball": "https://verdaccio.lossless.digital/@types/minimatch/-/minimatch-6.0.0.tgz"
@@ -3809,6 +3786,13 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"tarball": "https://verdaccio.lossless.digital/balanced-match/-/balanced-match-1.0.2.tgz"
},
"balanced-match@4.0.2": {
"integrity": "sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==",
"dependencies": [
"jackspeak"
],
"tarball": "https://verdaccio.lossless.digital/balanced-match/-/balanced-match-4.0.2.tgz"
},
"bare-events@2.8.2": {
"integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
"tarball": "https://verdaccio.lossless.digital/bare-events/-/bare-events-2.8.2.tgz"
@@ -3891,7 +3875,7 @@
"brace-expansion@1.1.12": {
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dependencies": [
"balanced-match",
"balanced-match@1.0.2",
"concat-map"
],
"tarball": "https://verdaccio.lossless.digital/brace-expansion/-/brace-expansion-1.1.12.tgz"
@@ -3899,10 +3883,17 @@
"brace-expansion@2.0.2": {
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dependencies": [
"balanced-match"
"balanced-match@1.0.2"
],
"tarball": "https://verdaccio.lossless.digital/brace-expansion/-/brace-expansion-2.0.2.tgz"
},
"brace-expansion@5.0.2": {
"integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==",
"dependencies": [
"balanced-match@4.0.2"
],
"tarball": "https://verdaccio.lossless.digital/brace-expansion/-/brace-expansion-5.0.2.tgz"
},
"broadcast-channel@7.3.0": {
"integrity": "sha512-UHPhLBQKfQ8OmMFMpmPfO5dRakyA1vsfiDGWTYNvChYol65tbuhivPEGgZZiuetorvExdvxaWiBy/ym1Ty08yA==",
"dependencies": [
@@ -4823,7 +4814,7 @@
"dependencies": [
"foreground-child@3.3.1",
"jackspeak",
"minimatch@10.1.2",
"minimatch@10.2.0",
"minipass",
"package-json-from-dist",
"path-scurry"
@@ -5862,12 +5853,12 @@
"integrity": "f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a",
"tarball": "https://verdaccio.lossless.digital/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz"
},
"minimatch@10.1.2": {
"integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==",
"minimatch@10.2.0": {
"integrity": "sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w==",
"dependencies": [
"@isaacs/brace-expansion"
"brace-expansion@5.0.2"
],
"tarball": "https://verdaccio.lossless.digital/minimatch/-/minimatch-10.1.2.tgz"
"tarball": "https://verdaccio.lossless.digital/minimatch/-/minimatch-10.2.0.tgz"
},
"minimatch@3.1.2": {
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
@@ -7313,7 +7304,7 @@
"npm:@push.rocks/smartnetwork@^4.4.0",
"npm:@push.rocks/smartpromise@^4.2.3",
"npm:@push.rocks/smartrequest@^5.0.1",
"npm:@push.rocks/smartrust@^1.2.0",
"npm:@push.rocks/smartrust@^1.2.1",
"npm:@push.rocks/smartrx@^3.0.10",
"npm:@push.rocks/smartserve@^2.0.1",
"npm:@push.rocks/smartstring@^4.1.0",
@@ -7322,7 +7313,7 @@
"npm:@types/minimatch@6",
"npm:@types/node@^25.2.3",
"npm:@types/ws@^8.18.1",
"npm:minimatch@^10.1.2",
"npm:minimatch@^10.2.0",
"npm:pretty-ms@^9.3.0",
"npm:typescript@^5.9.3",
"npm:why-is-node-running@^3.2.2",

View File

@@ -1,6 +1,6 @@
{
"name": "@push.rocks/smartproxy",
"version": "23.1.2",
"version": "23.1.5",
"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.",
"main": "dist_ts/index.js",
@@ -34,14 +34,14 @@
"@push.rocks/smartnetwork": "^4.4.0",
"@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartrequest": "^5.0.1",
"@push.rocks/smartrust": "^1.2.0",
"@push.rocks/smartrust": "^1.2.1",
"@push.rocks/smartrx": "^3.0.10",
"@push.rocks/smartstring": "^4.1.0",
"@push.rocks/taskbuffer": "^4.2.0",
"@tsclass/tsclass": "^9.3.0",
"@types/minimatch": "^6.0.0",
"@types/ws": "^8.18.1",
"minimatch": "^10.1.2",
"minimatch": "^10.2.0",
"pretty-ms": "^9.3.0",
"ws": "^8.19.0"
},

62
pnpm-lock.yaml generated
View File

@@ -36,8 +36,8 @@ importers:
specifier: ^5.0.1
version: 5.0.1
'@push.rocks/smartrust':
specifier: ^1.2.0
version: 1.2.0
specifier: ^1.2.1
version: 1.2.1
'@push.rocks/smartrx':
specifier: ^3.0.10
version: 3.0.10
@@ -57,8 +57,8 @@ importers:
specifier: ^8.18.1
version: 8.18.1
minimatch:
specifier: ^10.1.2
version: 10.1.2
specifier: ^10.2.0
version: 10.2.0
pretty-ms:
specifier: ^9.3.0
version: 9.3.0
@@ -570,14 +570,6 @@ packages:
resolution: {integrity: sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==}
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':
resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==}
engines: {node: '>=18'}
@@ -883,8 +875,8 @@ packages:
'@push.rocks/smartrouter@1.3.3':
resolution: {integrity: sha512-1+xZEnWlhzqLWAaJ1zFNhQ0zgbfCWQl1DBT72LygLxTs+P0K8AwJKgqo/IX6CT55kGCFnPAZIYSbVJlGsgrB0w==}
'@push.rocks/smartrust@1.2.0':
resolution: {integrity: sha512-JlaALselIHoP6C3ceQbrvz424G21cND/QsH/KI3E/JrO4XphJiGZwM6f4yJWrijdPYR/YYMoaIiYN7ybZp0C4w==}
'@push.rocks/smartrust@1.2.1':
resolution: {integrity: sha512-ANwXXibUwoHNWF1hhXhXVVrfzYlhgHYRa2205Jkd/s/wXzcWHftYZthilJj+52B7nkzSB76umfxKfK5eBYY2Ug==}
'@push.rocks/smartrx@3.0.10':
resolution: {integrity: sha512-USjIYcsSfzn14cwOsxgq/bBmWDTTzy3ouWAnW5NdMyRRzEbmeNrvmy6TRqNeDlJ2PsYNTt1rr/zGUqvIy72ITg==}
@@ -1649,6 +1641,10 @@ packages:
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
balanced-match@4.0.2:
resolution: {integrity: sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==}
engines: {node: 20 || >=22}
bare-events@2.8.2:
resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==}
peerDependencies:
@@ -1714,6 +1710,10 @@ packages:
brace-expansion@2.0.2:
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:
resolution: {integrity: sha512-JgraikEriG/TxBUi2W/w2O0jhHjXZUtXAvCZH0Yr3whjxYVgAg0hSe6r/teM+I5H5Q/q6RhyuKdC2pHNlFyepQ==}
@@ -2750,8 +2750,8 @@ packages:
minimalistic-crypto-utils@1.0.1:
resolution: {integrity: sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=}
minimatch@10.1.2:
resolution: {integrity: sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==}
minimatch@10.2.0:
resolution: {integrity: sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w==}
engines: {node: 20 || >=22}
minimatch@3.1.2:
@@ -4654,12 +4654,6 @@ snapshots:
dependencies:
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': {}
'@leichtgewicht/ip-codec@2.0.5': {}
@@ -5034,7 +5028,7 @@ snapshots:
'@push.rocks/smartstring': 4.1.0
'@push.rocks/smartunique': 3.0.9
'@tsclass/tsclass': 9.3.0
minimatch: 10.1.2
minimatch: 10.2.0
transitivePeerDependencies:
- aws-crt
@@ -5127,7 +5121,7 @@ snapshots:
acme-client: 5.4.0
dns-packet: 5.6.1
elliptic: 6.6.1
minimatch: 10.1.2
minimatch: 10.2.0
transitivePeerDependencies:
- supports-color
@@ -5143,7 +5137,7 @@ snapshots:
acme-client: 5.4.0
dns-packet: 5.6.1
elliptic: 6.6.1
minimatch: 10.1.2
minimatch: 10.2.0
transitivePeerDependencies:
- supports-color
@@ -5497,7 +5491,7 @@ snapshots:
'@push.rocks/smartrx': 3.0.10
path-to-regexp: 8.3.0
'@push.rocks/smartrust@1.2.0':
'@push.rocks/smartrust@1.2.1':
dependencies:
'@push.rocks/smartpath': 6.0.0
@@ -6323,7 +6317,7 @@ snapshots:
'@types/minimatch@6.0.0':
dependencies:
minimatch: 10.1.2
minimatch: 10.2.0
'@types/ms@2.1.0': {}
@@ -6494,6 +6488,10 @@ snapshots:
balanced-match@1.0.2: {}
balanced-match@4.0.2:
dependencies:
jackspeak: 4.2.3
bare-events@2.8.2: {}
bare-fs@4.5.3:
@@ -6564,6 +6562,10 @@ snapshots:
dependencies:
balanced-match: 1.0.2
brace-expansion@5.0.2:
dependencies:
balanced-match: 4.0.2
broadcast-channel@7.2.0:
dependencies:
'@babel/runtime': 7.28.4
@@ -7157,7 +7159,7 @@ snapshots:
dependencies:
foreground-child: 3.3.1
jackspeak: 4.2.3
minimatch: 10.1.2
minimatch: 10.2.0
minipass: 7.1.2
package-json-from-dist: 1.0.1
path-scurry: 2.0.1
@@ -7862,9 +7864,9 @@ snapshots:
minimalistic-crypto-utils@1.0.1: {}
minimatch@10.1.2:
minimatch@10.2.0:
dependencies:
'@isaacs/brace-expansion': 5.0.1
brace-expansion: 5.0.2
minimatch@3.1.2:
dependencies:

View File

@@ -364,6 +364,10 @@ impl TcpListenerManager {
// 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,
// 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 {
port,
@@ -384,7 +388,28 @@ impl TcpListenerManager {
// 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 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_port = target.port.resolve(port);
let route_id = quick_match.route.id.as_deref();
@@ -562,6 +587,17 @@ impl TcpListenerManager {
Some(rm) => rm,
None => {
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(());
}
};

View File

@@ -29,6 +29,11 @@ struct Cli {
#[tokio::main]
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();
// Initialize tracing - write to stderr so stdout is reserved for management IPC

View File

@@ -6,42 +6,49 @@ import { SmartProxy } from '../ts/index.js';
let testProxy: SmartProxy;
let targetServer: net.Server;
const ECHO_PORT = 47200;
const PROXY_PORT = 47201;
// Create a simple echo server as target
tap.test('setup test environment', async () => {
// Create target server that echoes data back
targetServer = net.createServer((socket) => {
console.log('Target server: client connected');
// Echo data back
socket.on('data', (data) => {
console.log(`Target server received: ${data.toString().trim()}`);
socket.write(data);
});
socket.on('close', () => {
console.log('Target server: client disconnected');
});
});
await new Promise<void>((resolve) => {
targetServer.listen(9876, () => {
console.log('Target server listening on port 9876');
await new Promise<void>((resolve, reject) => {
targetServer.on('error', (err) => {
console.error(`Echo server error: ${err.message}`);
reject(err);
});
targetServer.listen(ECHO_PORT, () => {
console.log(`Target server listening on port ${ECHO_PORT}`);
resolve();
});
});
// Create proxy with simple TCP forwarding (no TLS)
testProxy = new SmartProxy({
routes: [{
name: 'tcp-forward-test',
match: {
ports: 8888 // Plain TCP port
ports: PROXY_PORT // Plain TCP port
},
action: {
type: 'forward',
targets: [{
host: 'localhost',
port: 9876
port: ECHO_PORT
}]
// No TLS configuration - just plain TCP forwarding
}
@@ -49,7 +56,7 @@ tap.test('setup test environment', async () => {
defaults: {
target: {
host: 'localhost',
port: 9876
port: ECHO_PORT
}
},
enableDetailedLogging: true,
@@ -59,72 +66,72 @@ tap.test('setup test environment', async () => {
keepAlive: true,
keepAliveInitialDelay: 1000
});
await testProxy.start();
});
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();
let messagesReceived = 0;
let connectionClosed = false;
// Connect to proxy
await new Promise<void>((resolve, reject) => {
client.connect(8888, 'localhost', () => {
client.connect(PROXY_PORT, 'localhost', () => {
console.log('Client connected to proxy');
resolve();
});
client.on('error', reject);
});
// Set up data handler
client.on('data', (data) => {
console.log(`Client received: ${data.toString().trim()}`);
messagesReceived++;
});
client.on('close', () => {
console.log('Client connection closed');
connectionClosed = true;
});
// Send initial handshake-like data
client.write('HELLO\n');
// Wait for response
await new Promise(resolve => setTimeout(resolve, 100));
expect(messagesReceived).toEqual(1);
// Simulate WebSocket-like keep-alive pattern
// Send periodic messages over 60 seconds
// Send periodic messages over 5 seconds
const startTime = Date.now();
const pingInterval = setInterval(() => {
if (!connectionClosed && Date.now() - startTime < 60000) {
if (!connectionClosed && Date.now() - startTime < 5000) {
console.log('Sending ping...');
client.write('PING\n');
} else {
clearInterval(pingInterval);
}
}, 10000); // Every 10 seconds
// Wait for 55 seconds (must complete within 60s runner timeout)
await new Promise(resolve => setTimeout(resolve, 55000));
}, 1000); // Every 1 second
// Wait for 5 seconds — sufficient to verify the connection stays open
await new Promise(resolve => setTimeout(resolve, 5000));
// Clean up interval
clearInterval(pingInterval);
// Connection should still be open
expect(connectionClosed).toEqual(false);
// Should have received responses (1 hello + 6 pings)
expect(messagesReceived).toBeGreaterThan(5);
// Should have received responses (1 hello + ~5 pings)
expect(messagesReceived).toBeGreaterThan(3);
// Close connection gracefully
client.end();
// Wait for close
await new Promise(resolve => setTimeout(resolve, 100));
expect(connectionClosed).toEqual(true);
@@ -134,7 +141,7 @@ tap.test('should keep WebSocket-like connection open for extended period', async
tap.test('cleanup', async () => {
await testProxy.stop();
await new Promise<void>((resolve) => {
targetServer.close(() => {
console.log('Target server closed');

View File

@@ -5,8 +5,8 @@ import * as net from 'net';
let smartProxyInstance: SmartProxy;
let echoServer: net.Server;
const echoServerPort = 9876;
const proxyPort = 8080;
const echoServerPort = 47300;
const proxyPort = 47301;
// Create an echo server for testing
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, () => {
console.log(`Echo server listening on port ${echoServerPort}`);
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 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) => {
// Set a timeout for this test
tools.timeout(10000); // 10 seconds
// Create an echo server
echoServer = await new Promise<net.Server>((resolve) => {
echoServer = await new Promise<net.Server>((resolve, reject) => {
const server = net.createServer((socket) => {
socket.on('data', (data) => {
socket.write(`ECHO: ${data}`);
});
});
server.listen(8888, () => {
console.log('Echo server listening on port 8888');
server.on('error', (err) => {
console.error(`Echo server error: ${err.message}`);
reject(err);
});
server.listen(ECHO_PORT, () => {
console.log(`Echo server listening on port ${ECHO_PORT}`);
resolve(server);
});
});
@@ -26,10 +34,10 @@ tap.test('port forwarding should not immediately close connections', async (tool
proxy = new SmartProxy({
routes: [{
name: 'test-forward',
match: { ports: 9999 },
match: { ports: PROXY_PORT_1 },
action: {
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();
// 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) => {
client.on('data', (data) => {
const response = data.toString();
client.end(); // Close the connection after receiving data
resolve(response);
});
client.on('error', reject);
client.write('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 () => {
@@ -59,7 +70,7 @@ tap.test('TLS passthrough should work correctly', async () => {
proxy = new SmartProxy({
routes: [{
name: 'tls-test',
match: { ports: 8443, domains: 'test.example.com' },
match: { ports: PROXY_PORT_2, domains: 'test.example.com' },
action: {
type: 'forward',
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(() => {
// Force exit after tests complete
setTimeout(() => {
console.log('Forcing process exit');
process.exit(0);
}, 1000);
});
export default tap.start();

View File

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

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];
for (const domain of domains) {
if (domain.includes('*')) continue;
try {
const result: TSmartProxyCertProvisionObject = await provisionFn(domain);