Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b04bc612b | |||
| e774ec87ca | |||
| cbde778f09 | |||
| bc2bc874a5 | |||
| fdabf807b0 | |||
| 81e0e6b4d8 | |||
| 28fa69bf59 | |||
| 5019658032 |
31
changelog.md
31
changelog.md
@@ -1,5 +1,36 @@
|
||||
# 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
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@push.rocks/smartproxy",
|
||||
"version": "23.1.1",
|
||||
"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
62
pnpm-lock.yaml
generated
@@ -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:
|
||||
|
||||
@@ -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(());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
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,6 +6,9 @@ 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
|
||||
@@ -23,9 +26,13 @@ tap.test('setup test environment', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -35,13 +42,13 @@ tap.test('setup test environment', async () => {
|
||||
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,
|
||||
@@ -64,7 +71,7 @@ tap.test('setup test environment', async () => {
|
||||
});
|
||||
|
||||
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;
|
||||
@@ -72,7 +79,7 @@ tap.test('should keep WebSocket-like connection open for extended period', async
|
||||
|
||||
// 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();
|
||||
});
|
||||
@@ -99,19 +106,19 @@ tap.test('should keep WebSocket-like connection open for extended period', async
|
||||
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
|
||||
}, 1000); // Every 1 second
|
||||
|
||||
// Wait for 55 seconds (must complete within 60s runner timeout)
|
||||
await new Promise(resolve => setTimeout(resolve, 55000));
|
||||
// Wait for 5 seconds — sufficient to verify the connection stays open
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
|
||||
// Clean up interval
|
||||
clearInterval(pingInterval);
|
||||
@@ -119,8 +126,8 @@ tap.test('should keep WebSocket-like connection open for extended period', async
|
||||
// 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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,7 +45,7 @@ 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) => {
|
||||
@@ -52,6 +60,9 @@ tap.test('port forwarding should not immediately close connections', async (tool
|
||||
});
|
||||
|
||||
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();
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartproxy',
|
||||
version: '23.1.1',
|
||||
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.'
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as net from 'net';
|
||||
import * as net from 'node:net';
|
||||
import { WrappedSocket } from './wrapped-socket.js';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LifecycleComponent } from './lifecycle-component.js';
|
||||
import { BinaryHeap } from './binary-heap.js';
|
||||
import { AsyncMutex } from './async-utils.js';
|
||||
import { EventEmitter } from 'events';
|
||||
import { EventEmitter } from 'node:events';
|
||||
|
||||
/**
|
||||
* Interface for pooled connection
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Provides standardized socket cleanup with proper listener and timer management
|
||||
*/
|
||||
|
||||
import type { Socket } from 'net';
|
||||
import type { Socket } from 'node:net';
|
||||
|
||||
export type SocketTracked = {
|
||||
cleanup: () => void;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// node native scope
|
||||
import { EventEmitter } from 'events';
|
||||
import * as fs from 'fs';
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
import * as net from 'net';
|
||||
import * as path from 'path';
|
||||
import * as tls from 'tls';
|
||||
import * as url from 'url';
|
||||
import * as http2 from 'http2';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import * as fs from 'node:fs';
|
||||
import * as http from 'node:http';
|
||||
import * as https from 'node:https';
|
||||
import * as net from 'node:net';
|
||||
import * as path from 'node:path';
|
||||
import * as tls from 'node:tls';
|
||||
import * as url from 'node:url';
|
||||
import * as http2 from 'node:http2';
|
||||
|
||||
export { EventEmitter, fs, http, https, net, path, tls, url, http2 };
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* that may span multiple TCP packets.
|
||||
*/
|
||||
|
||||
import { Buffer } from 'buffer';
|
||||
import { Buffer } from 'node:buffer';
|
||||
|
||||
/**
|
||||
* Fragment tracking information
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Buffer } from 'buffer';
|
||||
import { Buffer } from 'node:buffer';
|
||||
import {
|
||||
TlsRecordType,
|
||||
TlsHandshakeType,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Buffer } from 'buffer';
|
||||
import { Buffer } from 'node:buffer';
|
||||
import { TlsExtensionType, TlsUtils } from '../utils/tls-utils.js';
|
||||
import {
|
||||
ClientHelloParser,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* WebSocket Protocol Utilities
|
||||
*/
|
||||
|
||||
import * as crypto from 'crypto';
|
||||
import * as crypto from 'node:crypto';
|
||||
import { WEBSOCKET_MAGIC_STRING } from './constants.js';
|
||||
import type { RawData } from './types.js';
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Buffer } from 'buffer';
|
||||
import { Buffer } from 'node:buffer';
|
||||
import {
|
||||
TlsRecordType,
|
||||
TlsHandshakeType,
|
||||
|
||||
Reference in New Issue
Block a user