This commit is contained in:
Mateusz Gruszczyński committed 2026-10-08 15:59:37 +02:00
1 parent 65e082b9d1
commit 76cb4acef0
72 files changed
+6134 -4709

No files matched your search

+10 -1
View File
@@ -193,7 +193,6 @@ impl Default for LocationResponse {
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Source {
@@ -268,6 +267,13 @@ pub struct Vhost {
/// Redirect cleartext requests for this vhost to the selected HTTPS frontend.
#[serde(default)]
pub force_ssl: bool,
/// Redirect the www/non-www companion to a selected exact hostname.
#[serde(default)]
pub www_redirect: bool,
/// Canonical hostname for the quick www redirect. None preserves the legacy
/// first-hostname behavior. Other vhost hostnames remain independent aliases.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub www_redirect_target: Option<String>,
/// Forward the effective client IP to the application using Forwarded/X-Forwarded-For/X-Real-IP.
/// Disable only for applications that must see the RustProxy transport peer instead.
#[serde(default = "yes")]
@@ -483,4 +489,7 @@ pub struct PathRoute {
/// Nginx-style local `return`: when present no backend connection is opened.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub response: Option<LocationResponse>,
/// Local HTTP redirect; mutually exclusive with `response`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub redirect: Option<LocationRedirect>,
}
+34
View File
@@ -0,0 +1,34 @@
// Custom-location redirect model.
fn redirect_status() -> u16 {
302
}
fn preserve_redirect_query() -> bool {
true
}
/// Local redirect action for a custom location. `rewrite_pattern` is optional;
/// when present it is applied to the normalized request path and supports the
/// Rust regex replacement syntax (`$1`, `${name}`, ...).
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct LocationRedirect {
#[serde(default = "redirect_status")]
pub status: u16,
pub target: String,
#[serde(default)]
pub rewrite_pattern: Option<String>,
#[serde(default = "preserve_redirect_query")]
pub preserve_query: bool,
}
impl Default for LocationRedirect {
fn default() -> Self {
Self {
status: redirect_status(),
target: String::new(),
rewrite_pattern: None,
preserve_query: preserve_redirect_query(),
}
}
}
+76 -1
View File
@@ -15,6 +15,7 @@ impl Default for PathRoute {
headers: HeaderRules::default(),
rewrite: PathRewrite::default(),
response: None,
redirect: None,
}
}
}
@@ -40,6 +41,7 @@ impl PathRoute {
&& self.rewrite.mode == RewriteMode::Preserve
&& self.rewrite.value.is_empty()
&& self.response.is_none()
&& self.redirect.is_none()
}
}
impl Vhost {
@@ -71,6 +73,22 @@ impl Vhost {
*host = crate::config::normalize_host_pattern(host)?;
ensure!(hosts.insert(host.clone()), "duplicate vhost hostname");
}
if let Some(target) = &mut self.www_redirect_target {
*target = crate::config::normalize_host_pattern(target)?;
ensure!(
!target.starts_with("*.") && target.parse::<std::net::IpAddr>().is_err(),
"www redirect target must be an exact DNS hostname"
);
let partner = www_partner(target)?;
ensure!(
self.frontend.hosts.contains(target) || self.frontend.hosts.contains(&partner),
"www redirect target or its www/non-www partner must belong to this vhost"
);
}
if self.www_redirect {
let _ = self.www_redirect_pair()?;
let _ = self.routing_hosts()?;
}
if let Some(id) = &self.acl_profile_id {
ensure!(crate::config::valid_id(id), "invalid ACL profile id");
}
@@ -86,7 +104,10 @@ impl Vhost {
self.source.validate()?;
let needs_proxy = self.source.is_reverse_proxy()
|| self.routes.iter().any(|route| {
route.enabled && route.response.is_none() && route.source.is_reverse_proxy()
route.enabled
&& route.response.is_none()
&& route.redirect.is_none()
&& route.source.is_reverse_proxy()
});
ensure!(
self.backends.len() <= 32,
@@ -121,6 +142,10 @@ impl Vhost {
self.routes.retain(|route| !route.is_legacy_main_fallback());
for route in &mut self.routes {
route.source.validate()?;
ensure!(
route.response.is_none() || route.redirect.is_none(),
"location cannot define both a local response and a redirect"
);
if let Some(id) = &route.acl_profile_id {
ensure!(crate::config::valid_id(id), "invalid location ACL profile id");
}
@@ -224,6 +249,56 @@ pub fn validate_listener(listener: Listener) -> Result<()> {
);
Ok(())
}
/// Return the other side of a www/non-www redirect pair. The target and its
/// counterpart must both be routable, even if only one was configured.
fn www_partner(host: &str) -> Result<String> {
if let Some(bare) = host.strip_prefix("www.") {
ensure!(!bare.is_empty() && bare.parse::<std::net::IpAddr>().is_err(),
"www redirect requires a DNS hostname");
crate::config::normalize_host(bare)
} else {
crate::config::normalize_host(&format!("www.{host}"))
}
}
impl Vhost {
fn www_canonical_host(&self) -> Result<&str> {
let canonical = self.www_redirect_target.as_deref()
.or_else(|| self.frontend.hosts.first().map(String::as_str))
.ok_or_else(|| anyhow::anyhow!("vhost requires at least one hostname"))?;
ensure!(!canonical.starts_with("*.") && canonical.parse::<std::net::IpAddr>().is_err(),
"www redirect requires an exact DNS hostname");
let partner = www_partner(canonical)?;
ensure!(self.frontend.hosts.iter().any(|host| host == canonical || host == &partner),
"www redirect target or its www/non-www partner must belong to this vhost");
Ok(canonical)
}
/// Hostnames owned by the vhost at runtime. Both the selected canonical
/// host and its www/non-www partner are claimed, even if one is implicit.
pub fn routing_hosts(&self) -> Result<Vec<String>> {
let mut hosts = self.frontend.hosts.clone();
if !self.www_redirect {
return Ok(hosts);
}
let canonical = self.www_canonical_host()?.to_owned();
let companion = www_partner(&canonical)?;
for name in [canonical, companion] {
if !hosts.contains(&name) {
hosts.push(name);
}
}
Ok(hosts)
}
pub fn www_redirect_pair(&self) -> Result<Option<(String, String)>> {
if !self.www_redirect {
return Ok(None);
}
let canonical = self.www_canonical_host()?.to_owned();
let companion = www_partner(&canonical)?;
Ok(Some((companion, canonical)))
}
}
impl Frontend {
pub fn resolved(&self, settings: &crate::settings::Listeners) -> Result<Vec<Listener>> {
let mut result = Vec::new();