feat(proxy): add per-route disable_secure_redirection; preserve query string in redirect

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 <sienna@sunbeam.pt>
This commit is contained in:
2026-03-10 23:38:19 +00:00
parent 6ec0f78a5b
commit d0146b47e3
2 changed files with 27 additions and 2 deletions

View File

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

View File

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