From d0146b47e364d4cc156b3a209422462c72eaefe4 Mon Sep 17 00:00:00 2001 From: Sienna Meridian Satterwhite Date: Tue, 10 Mar 2026 23:38:19 +0000 Subject: [PATCH] feat(proxy): add per-route disable_secure_redirection; preserve query string in redirect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit By default every plain-HTTP request is 301-redirected to HTTPS — no upstream is ever contacted, making it as close to an L4 redirect as HTTP allows. New RouteConfig field `disable_secure_redirection` (bool, default false): when set to true on a route, plain-HTTP requests for that host pass through to the backend unchanged instead of being redirected. Also fixes the redirect URL to include the original query string, which was previously dropped (e.g. ?next=/dashboard would be lost after redirect). Signed-off-by: Sienna Meridian Satterwhite --- src/config.rs | 4 ++++ src/proxy.rs | 25 +++++++++++++++++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/config.rs b/src/config.rs index a76d83b..09fcf74 100644 --- a/src/config.rs +++ b/src/config.rs @@ -46,6 +46,10 @@ pub struct RouteConfig { pub backend: String, #[serde(default)] pub websocket: bool, + /// When true, plain-HTTP requests for this host are forwarded as-is rather + /// than being redirected to HTTPS. Defaults to false (redirect enforced). + #[serde(default)] + pub disable_secure_redirection: bool, /// Optional path-based sub-routes (longest prefix wins). /// If the request path matches a sub-route, its backend is used instead. #[serde(default)] diff --git a/src/proxy.rs b/src/proxy.rs index 6d229cc..dcfbfa4 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -102,9 +102,29 @@ impl ProxyHttp for SunbeamProxy { return Ok(true); } - // All other plain-HTTP traffic: redirect to HTTPS. + // All other plain-HTTP traffic. let host = extract_host(session); - let location = format!("https://{host}{path}"); + let prefix = host.split('.').next().unwrap_or(""); + + // Routes that explicitly opt out of HTTPS enforcement pass through. + // All other requests — including unknown hosts — are redirected. + // This is as close to an L4 redirect as HTTP allows: the upstream is + // never contacted; the 301 is written directly to the downstream socket. + if self + .find_route(prefix) + .map(|r| r.disable_secure_redirection) + .unwrap_or(false) + { + return Ok(false); + } + + let query = session + .req_header() + .uri + .query() + .map(|q| format!("?{q}")) + .unwrap_or_default(); + let location = format!("https://{host}{path}{query}"); let mut resp = ResponseHeader::build(301, None)?; resp.insert_header("Location", location)?; resp.insert_header("Content-Length", "0")?; @@ -162,6 +182,7 @@ impl ProxyHttp for SunbeamProxy { host_prefix: route.host_prefix.clone(), backend: pr.backend.clone(), websocket: pr.websocket || route.websocket, + disable_secure_redirection: route.disable_secure_redirection, paths: vec![], }); return Ok(Box::new(HttpPeer::new(