v0.29.0
This commit is contained in:
1 parent
65e082b9d1
commit
76cb4acef0
72 files changed
+6134
-4709
No files matched your search
+10
-1
@@ -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>,
|
||||
}
|
||||
@@ -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
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user