feat(routes): add protocol-based route matching and ensure terminate-and-reencrypt routes HTTP through the full HTTP proxy; update docs and tests
This commit is contained in:
@@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-02-16 - 25.7.0 - feat(routes)
|
||||||
|
add protocol-based route matching and ensure terminate-and-reencrypt routes HTTP through the full HTTP proxy; update docs and tests
|
||||||
|
|
||||||
|
- Introduce a new 'protocol' match field for routes (supports 'http' and 'tcp') and preserve it through cloning/merging.
|
||||||
|
- Add Rust integration test verifying terminate-and-reencrypt decrypts TLS and routes HTTP traffic via the HTTP proxy (per-request Host/path routing) instead of raw tunneling.
|
||||||
|
- Add TypeScript unit tests covering protocol field validation, preservation, interaction with terminate-and-reencrypt, cloning, merging, and matching behavior.
|
||||||
|
- Update README with a Protocol-Specific Routing section and clarify terminate-and-reencrypt behavior (HTTP routed via HTTP proxy; non-HTTP uses raw TLS-to-TLS tunnel).
|
||||||
|
- Example config: include health check thresholds (unhealthyThreshold and healthyThreshold) in the sample healthCheck settings.
|
||||||
|
|
||||||
## 2026-02-16 - 25.6.0 - feat(rustproxy)
|
## 2026-02-16 - 25.6.0 - feat(rustproxy)
|
||||||
add protocol-based routing and backend TLS re-encryption support
|
add protocol-based routing and backend TLS re-encryption support
|
||||||
|
|
||||||
|
|||||||
57
readme.md
57
readme.md
@@ -27,7 +27,7 @@ Whether you're building microservices, deploying edge infrastructure, or need a
|
|||||||
| 🦀 **Rust-Powered Engine** | All networking handled by a high-performance Rust binary via IPC |
|
| 🦀 **Rust-Powered Engine** | All networking handled by a high-performance Rust binary via IPC |
|
||||||
| 🔀 **Unified Route-Based Config** | Clean match/action patterns for intuitive traffic routing |
|
| 🔀 **Unified Route-Based Config** | Clean match/action patterns for intuitive traffic routing |
|
||||||
| 🔒 **Automatic SSL/TLS** | Zero-config HTTPS with Let's Encrypt ACME integration |
|
| 🔒 **Automatic SSL/TLS** | Zero-config HTTPS with Let's Encrypt ACME integration |
|
||||||
| 🎯 **Flexible Matching** | Route by port, domain, path, client IP, TLS version, headers, or custom logic |
|
| 🎯 **Flexible Matching** | Route by port, domain, path, protocol, client IP, TLS version, headers, or custom logic |
|
||||||
| 🚄 **High-Performance** | Choose between user-space or kernel-level (NFTables) forwarding |
|
| 🚄 **High-Performance** | Choose between user-space or kernel-level (NFTables) forwarding |
|
||||||
| ⚖️ **Load Balancing** | Round-robin, least-connections, IP-hash with health checks |
|
| ⚖️ **Load Balancing** | Round-robin, least-connections, IP-hash with health checks |
|
||||||
| 🛡️ **Enterprise Security** | IP filtering, rate limiting, basic auth, JWT auth, connection limits |
|
| 🛡️ **Enterprise Security** | IP filtering, rate limiting, basic auth, JWT auth, connection limits |
|
||||||
@@ -89,7 +89,7 @@ SmartProxy uses a powerful **match/action** pattern that makes routing predictab
|
|||||||
```
|
```
|
||||||
|
|
||||||
Every route consists of:
|
Every route consists of:
|
||||||
- **Match** — What traffic to capture (ports, domains, paths, IPs, headers)
|
- **Match** — What traffic to capture (ports, domains, paths, protocol, IPs, headers)
|
||||||
- **Action** — What to do with it (`forward` or `socket-handler`)
|
- **Action** — What to do with it (`forward` or `socket-handler`)
|
||||||
- **Security** (optional) — IP allow/block lists, rate limits, authentication
|
- **Security** (optional) — IP allow/block lists, rate limits, authentication
|
||||||
- **Headers** (optional) — Request/response header manipulation with template variables
|
- **Headers** (optional) — Request/response header manipulation with template variables
|
||||||
@@ -103,7 +103,7 @@ SmartProxy supports three TLS handling modes:
|
|||||||
|------|-------------|----------|
|
|------|-------------|----------|
|
||||||
| `passthrough` | Forward encrypted traffic as-is (SNI-based routing) | Backend handles TLS |
|
| `passthrough` | Forward encrypted traffic as-is (SNI-based routing) | Backend handles TLS |
|
||||||
| `terminate` | Decrypt at proxy, forward plain HTTP to backend | Standard reverse proxy |
|
| `terminate` | Decrypt at proxy, forward plain HTTP to backend | Standard reverse proxy |
|
||||||
| `terminate-and-reencrypt` | Decrypt, then re-encrypt to backend | Zero-trust environments |
|
| `terminate-and-reencrypt` | Decrypt at proxy, re-encrypt to backend. HTTP traffic gets full per-request routing (Host header, path matching) via the HTTP proxy; non-HTTP traffic uses a raw TLS-to-TLS tunnel | Zero-trust / defense-in-depth environments |
|
||||||
|
|
||||||
## 💡 Common Use Cases
|
## 💡 Common Use Cases
|
||||||
|
|
||||||
@@ -135,13 +135,13 @@ const proxy = new SmartProxy({
|
|||||||
],
|
],
|
||||||
{
|
{
|
||||||
tls: { mode: 'terminate', certificate: 'auto' },
|
tls: { mode: 'terminate', certificate: 'auto' },
|
||||||
loadBalancing: {
|
algorithm: 'round-robin',
|
||||||
algorithm: 'round-robin',
|
healthCheck: {
|
||||||
healthCheck: {
|
path: '/health',
|
||||||
path: '/health',
|
interval: 30000,
|
||||||
interval: 30000,
|
timeout: 5000,
|
||||||
timeout: 5000
|
unhealthyThreshold: 3,
|
||||||
}
|
healthyThreshold: 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -318,6 +318,42 @@ const proxy = new SmartProxy({
|
|||||||
|
|
||||||
> **Note:** Routes with dynamic functions (host/port callbacks) are automatically relayed through the TypeScript socket handler server, since JavaScript functions can't be serialized to Rust.
|
> **Note:** Routes with dynamic functions (host/port callbacks) are automatically relayed through the TypeScript socket handler server, since JavaScript functions can't be serialized to Rust.
|
||||||
|
|
||||||
|
### 🔀 Protocol-Specific Routing
|
||||||
|
|
||||||
|
Restrict routes to specific application-layer protocols. When `protocol` is set, the Rust engine detects the protocol after connection (or after TLS termination) and only matches routes that accept that protocol:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// HTTP-only route (rejects raw TCP connections)
|
||||||
|
const httpOnlyRoute: IRouteConfig = {
|
||||||
|
name: 'http-api',
|
||||||
|
match: {
|
||||||
|
ports: 443,
|
||||||
|
domains: 'api.example.com',
|
||||||
|
protocol: 'http', // Only match HTTP/1.1, HTTP/2, and WebSocket upgrades
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: 'forward',
|
||||||
|
targets: [{ host: 'api-backend', port: 8080 }],
|
||||||
|
tls: { mode: 'terminate', certificate: 'auto' }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Raw TCP route (rejects HTTP traffic)
|
||||||
|
const tcpOnlyRoute: IRouteConfig = {
|
||||||
|
name: 'database-proxy',
|
||||||
|
match: {
|
||||||
|
ports: 5432,
|
||||||
|
protocol: 'tcp', // Only match non-HTTP TCP streams
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: 'forward',
|
||||||
|
targets: [{ host: 'db-server', port: 5432 }]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** Omitting `protocol` (the default) matches any protocol. For TLS routes, protocol detection happens *after* TLS termination — during the initial SNI-based route match, `protocol` is not yet known and the route is allowed to match. The protocol restriction is enforced after the proxy peeks at the decrypted data.
|
||||||
|
|
||||||
### 🔒 Security Controls
|
### 🔒 Security Controls
|
||||||
|
|
||||||
Comprehensive per-route security options:
|
Comprehensive per-route security options:
|
||||||
@@ -549,6 +585,7 @@ interface IRouteMatch {
|
|||||||
clientIp?: string[]; // ['10.0.0.0/8', '192.168.*']
|
clientIp?: string[]; // ['10.0.0.0/8', '192.168.*']
|
||||||
tlsVersion?: string[]; // ['TLSv1.2', 'TLSv1.3']
|
tlsVersion?: string[]; // ['TLSv1.2', 'TLSv1.3']
|
||||||
headers?: Record<string, string | RegExp>; // Match by HTTP headers
|
headers?: Record<string, string | RegExp>; // Match by HTTP headers
|
||||||
|
protocol?: 'http' | 'tcp'; // Match specific protocol ('http' includes h2 + WebSocket upgrades)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -407,6 +407,169 @@ async fn test_websocket_through_proxy() {
|
|||||||
proxy.stop().await.unwrap();
|
proxy.stop().await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Test that terminate-and-reencrypt mode routes HTTP traffic through the
|
||||||
|
/// full HTTP proxy with per-request Host-based routing.
|
||||||
|
///
|
||||||
|
/// This verifies the new behavior: after TLS termination, HTTP data is detected
|
||||||
|
/// and routed through HttpProxyService (like nginx) instead of being blindly tunneled.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_terminate_and_reencrypt_http_routing() {
|
||||||
|
let backend1_port = next_port();
|
||||||
|
let backend2_port = next_port();
|
||||||
|
let proxy_port = next_port();
|
||||||
|
|
||||||
|
// Start plain HTTP echo backends (proxy terminates client TLS, connects plain to backend)
|
||||||
|
let _b1 = start_http_echo_backend(backend1_port, "alpha").await;
|
||||||
|
let _b2 = start_http_echo_backend(backend2_port, "beta").await;
|
||||||
|
|
||||||
|
let (cert1, key1) = generate_self_signed_cert("alpha.example.com");
|
||||||
|
let (cert2, key2) = generate_self_signed_cert("beta.example.com");
|
||||||
|
|
||||||
|
// Create terminate-and-reencrypt routes
|
||||||
|
let mut route1 = make_tls_terminate_route(
|
||||||
|
proxy_port, "alpha.example.com", "127.0.0.1", backend1_port, &cert1, &key1,
|
||||||
|
);
|
||||||
|
route1.action.tls.as_mut().unwrap().mode = rustproxy_config::TlsMode::TerminateAndReencrypt;
|
||||||
|
|
||||||
|
let mut route2 = make_tls_terminate_route(
|
||||||
|
proxy_port, "beta.example.com", "127.0.0.1", backend2_port, &cert2, &key2,
|
||||||
|
);
|
||||||
|
route2.action.tls.as_mut().unwrap().mode = rustproxy_config::TlsMode::TerminateAndReencrypt;
|
||||||
|
|
||||||
|
let options = RustProxyOptions {
|
||||||
|
routes: vec![route1, route2],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut proxy = RustProxy::new(options).unwrap();
|
||||||
|
proxy.start().await.unwrap();
|
||||||
|
assert!(wait_for_port(proxy_port, 2000).await);
|
||||||
|
|
||||||
|
// Test alpha domain - HTTP request through TLS terminate-and-reencrypt
|
||||||
|
let alpha_result = with_timeout(async {
|
||||||
|
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||||
|
let tls_config = rustls::ClientConfig::builder()
|
||||||
|
.dangerous()
|
||||||
|
.with_custom_certificate_verifier(std::sync::Arc::new(InsecureVerifier))
|
||||||
|
.with_no_client_auth();
|
||||||
|
let connector = tokio_rustls::TlsConnector::from(std::sync::Arc::new(tls_config));
|
||||||
|
|
||||||
|
let stream = tokio::net::TcpStream::connect(format!("127.0.0.1:{}", proxy_port))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let server_name = rustls::pki_types::ServerName::try_from("alpha.example.com".to_string()).unwrap();
|
||||||
|
let mut tls_stream = connector.connect(server_name, stream).await.unwrap();
|
||||||
|
|
||||||
|
let request = "GET /api/data HTTP/1.1\r\nHost: alpha.example.com\r\nConnection: close\r\n\r\n";
|
||||||
|
tls_stream.write_all(request.as_bytes()).await.unwrap();
|
||||||
|
|
||||||
|
let mut response = Vec::new();
|
||||||
|
tls_stream.read_to_end(&mut response).await.unwrap();
|
||||||
|
String::from_utf8_lossy(&response).to_string()
|
||||||
|
}, 10)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let alpha_body = extract_body(&alpha_result);
|
||||||
|
assert!(
|
||||||
|
alpha_body.contains(r#""backend":"alpha"#),
|
||||||
|
"Expected alpha backend, got: {}",
|
||||||
|
alpha_body
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
alpha_body.contains(r#""method":"GET"#),
|
||||||
|
"Expected GET method, got: {}",
|
||||||
|
alpha_body
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
alpha_body.contains(r#""path":"/api/data"#),
|
||||||
|
"Expected /api/data path, got: {}",
|
||||||
|
alpha_body
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test beta domain - different host goes to different backend
|
||||||
|
let beta_result = with_timeout(async {
|
||||||
|
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||||
|
let tls_config = rustls::ClientConfig::builder()
|
||||||
|
.dangerous()
|
||||||
|
.with_custom_certificate_verifier(std::sync::Arc::new(InsecureVerifier))
|
||||||
|
.with_no_client_auth();
|
||||||
|
let connector = tokio_rustls::TlsConnector::from(std::sync::Arc::new(tls_config));
|
||||||
|
|
||||||
|
let stream = tokio::net::TcpStream::connect(format!("127.0.0.1:{}", proxy_port))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let server_name = rustls::pki_types::ServerName::try_from("beta.example.com".to_string()).unwrap();
|
||||||
|
let mut tls_stream = connector.connect(server_name, stream).await.unwrap();
|
||||||
|
|
||||||
|
let request = "GET /other HTTP/1.1\r\nHost: beta.example.com\r\nConnection: close\r\n\r\n";
|
||||||
|
tls_stream.write_all(request.as_bytes()).await.unwrap();
|
||||||
|
|
||||||
|
let mut response = Vec::new();
|
||||||
|
tls_stream.read_to_end(&mut response).await.unwrap();
|
||||||
|
String::from_utf8_lossy(&response).to_string()
|
||||||
|
}, 10)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let beta_body = extract_body(&beta_result);
|
||||||
|
assert!(
|
||||||
|
beta_body.contains(r#""backend":"beta"#),
|
||||||
|
"Expected beta backend, got: {}",
|
||||||
|
beta_body
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
beta_body.contains(r#""path":"/other"#),
|
||||||
|
"Expected /other path, got: {}",
|
||||||
|
beta_body
|
||||||
|
);
|
||||||
|
|
||||||
|
proxy.stop().await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that the protocol field on route config is accepted and processed.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_protocol_field_in_route_config() {
|
||||||
|
let backend_port = next_port();
|
||||||
|
let proxy_port = next_port();
|
||||||
|
|
||||||
|
let _backend = start_http_echo_backend(backend_port, "main").await;
|
||||||
|
|
||||||
|
// Create a route with protocol: "http" - should only match HTTP traffic
|
||||||
|
let mut route = make_test_route(proxy_port, None, "127.0.0.1", backend_port);
|
||||||
|
route.route_match.protocol = Some("http".to_string());
|
||||||
|
|
||||||
|
let options = RustProxyOptions {
|
||||||
|
routes: vec![route],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut proxy = RustProxy::new(options).unwrap();
|
||||||
|
proxy.start().await.unwrap();
|
||||||
|
assert!(wait_for_port(proxy_port, 2000).await);
|
||||||
|
|
||||||
|
// HTTP request should match the route and get proxied
|
||||||
|
let result = with_timeout(async {
|
||||||
|
let response = send_http_request(proxy_port, "example.com", "GET", "/test").await;
|
||||||
|
extract_body(&response).to_string()
|
||||||
|
}, 10)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
result.contains(r#""backend":"main"#),
|
||||||
|
"Expected main backend, got: {}",
|
||||||
|
result
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
result.contains(r#""path":"/test"#),
|
||||||
|
"Expected /test path, got: {}",
|
||||||
|
result
|
||||||
|
);
|
||||||
|
|
||||||
|
proxy.stop().await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
/// InsecureVerifier for test TLS client connections.
|
/// InsecureVerifier for test TLS client connections.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct InsecureVerifier;
|
struct InsecureVerifier;
|
||||||
|
|||||||
@@ -562,4 +562,168 @@ tap.test('Route Integration - Combining Multiple Route Types', async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --------------------------------- Protocol Match Field Tests ---------------------------------
|
||||||
|
|
||||||
|
tap.test('Routes: Should accept protocol field on route match', async () => {
|
||||||
|
// Create a route with protocol: 'http'
|
||||||
|
const httpOnlyRoute: IRouteConfig = {
|
||||||
|
match: {
|
||||||
|
ports: 443,
|
||||||
|
domains: 'api.example.com',
|
||||||
|
protocol: 'http',
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: 'forward',
|
||||||
|
targets: [{ host: 'backend', port: 8080 }],
|
||||||
|
tls: {
|
||||||
|
mode: 'terminate',
|
||||||
|
certificate: 'auto',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: 'HTTP-only Route',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validate the route - protocol field should not cause errors
|
||||||
|
const validation = validateRouteConfig(httpOnlyRoute);
|
||||||
|
expect(validation.valid).toBeTrue();
|
||||||
|
|
||||||
|
// Verify the protocol field is preserved
|
||||||
|
expect(httpOnlyRoute.match.protocol).toEqual('http');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('Routes: Should accept protocol tcp on route match', async () => {
|
||||||
|
// Create a route with protocol: 'tcp'
|
||||||
|
const tcpOnlyRoute: IRouteConfig = {
|
||||||
|
match: {
|
||||||
|
ports: 443,
|
||||||
|
domains: 'db.example.com',
|
||||||
|
protocol: 'tcp',
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: 'forward',
|
||||||
|
targets: [{ host: 'db-server', port: 5432 }],
|
||||||
|
tls: {
|
||||||
|
mode: 'passthrough',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: 'TCP-only Route',
|
||||||
|
};
|
||||||
|
|
||||||
|
const validation = validateRouteConfig(tcpOnlyRoute);
|
||||||
|
expect(validation.valid).toBeTrue();
|
||||||
|
|
||||||
|
expect(tcpOnlyRoute.match.protocol).toEqual('tcp');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('Routes: Protocol field should work with terminate-and-reencrypt', async () => {
|
||||||
|
// Create a terminate-and-reencrypt route that only accepts HTTP
|
||||||
|
const reencryptRoute = createHttpsTerminateRoute(
|
||||||
|
'secure.example.com',
|
||||||
|
{ host: 'backend', port: 443 },
|
||||||
|
{ reencrypt: true, certificate: 'auto', name: 'Reencrypt HTTP Route' }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set protocol restriction to http
|
||||||
|
reencryptRoute.match.protocol = 'http';
|
||||||
|
|
||||||
|
// Validate the route
|
||||||
|
const validation = validateRouteConfig(reencryptRoute);
|
||||||
|
expect(validation.valid).toBeTrue();
|
||||||
|
|
||||||
|
// Verify TLS mode
|
||||||
|
expect(reencryptRoute.action.tls?.mode).toEqual('terminate-and-reencrypt');
|
||||||
|
// Verify protocol field is preserved
|
||||||
|
expect(reencryptRoute.match.protocol).toEqual('http');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('Routes: Protocol field should not affect domain/port matching', async () => {
|
||||||
|
// Routes with and without protocol field should both match the same domain/port
|
||||||
|
const routeWithProtocol: IRouteConfig = {
|
||||||
|
match: {
|
||||||
|
ports: 443,
|
||||||
|
domains: 'example.com',
|
||||||
|
protocol: 'http',
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: 'forward',
|
||||||
|
targets: [{ host: 'backend', port: 8080 }],
|
||||||
|
tls: { mode: 'terminate', certificate: 'auto' },
|
||||||
|
},
|
||||||
|
name: 'With Protocol',
|
||||||
|
priority: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
const routeWithoutProtocol: IRouteConfig = {
|
||||||
|
match: {
|
||||||
|
ports: 443,
|
||||||
|
domains: 'example.com',
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: 'forward',
|
||||||
|
targets: [{ host: 'fallback', port: 8081 }],
|
||||||
|
tls: { mode: 'terminate', certificate: 'auto' },
|
||||||
|
},
|
||||||
|
name: 'Without Protocol',
|
||||||
|
priority: 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
const routes = [routeWithProtocol, routeWithoutProtocol];
|
||||||
|
|
||||||
|
// Both routes should match the domain/port (protocol is a hint for Rust-side matching)
|
||||||
|
const matches = findMatchingRoutes(routes, { domain: 'example.com', port: 443 });
|
||||||
|
expect(matches.length).toEqual(2);
|
||||||
|
|
||||||
|
// The one with higher priority should be first
|
||||||
|
const best = findBestMatchingRoute(routes, { domain: 'example.com', port: 443 });
|
||||||
|
expect(best).not.toBeUndefined();
|
||||||
|
expect(best!.name).toEqual('With Protocol');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('Routes: Protocol field preserved through route cloning', async () => {
|
||||||
|
const original: IRouteConfig = {
|
||||||
|
match: {
|
||||||
|
ports: 8443,
|
||||||
|
domains: 'clone-test.example.com',
|
||||||
|
protocol: 'http',
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: 'forward',
|
||||||
|
targets: [{ host: 'backend', port: 3000 }],
|
||||||
|
tls: { mode: 'terminate-and-reencrypt', certificate: 'auto' },
|
||||||
|
},
|
||||||
|
name: 'Clone Test',
|
||||||
|
};
|
||||||
|
|
||||||
|
const cloned = cloneRoute(original);
|
||||||
|
|
||||||
|
// Verify protocol is preserved in clone
|
||||||
|
expect(cloned.match.protocol).toEqual('http');
|
||||||
|
expect(cloned.action.tls?.mode).toEqual('terminate-and-reencrypt');
|
||||||
|
|
||||||
|
// Modify clone should not affect original
|
||||||
|
cloned.match.protocol = 'tcp';
|
||||||
|
expect(original.match.protocol).toEqual('http');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('Routes: Protocol field preserved through route merging', async () => {
|
||||||
|
const base: IRouteConfig = {
|
||||||
|
match: {
|
||||||
|
ports: 443,
|
||||||
|
domains: 'merge-test.example.com',
|
||||||
|
protocol: 'http',
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: 'forward',
|
||||||
|
targets: [{ host: 'backend', port: 3000 }],
|
||||||
|
tls: { mode: 'terminate-and-reencrypt', certificate: 'auto' },
|
||||||
|
},
|
||||||
|
name: 'Merge Base',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Merge with override that changes name but not protocol
|
||||||
|
const merged = mergeRouteConfigs(base, { name: 'Merged Route' });
|
||||||
|
expect(merged.match.protocol).toEqual('http');
|
||||||
|
expect(merged.name).toEqual('Merged Route');
|
||||||
|
});
|
||||||
|
|
||||||
export default tap.start();
|
export default tap.start();
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartproxy',
|
name: '@push.rocks/smartproxy',
|
||||||
version: '25.6.0',
|
version: '25.7.0',
|
||||||
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.'
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user