v0.25.3
This commit is contained in:
1 parent
174389e5b1
commit
e6604e41fc
73 files changed
+1240
-201
No files matched your search
@@ -45,6 +45,59 @@ impl Default for ProxyTimeouts {
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum MethodPolicyMode {
|
||||
#[default]
|
||||
Off,
|
||||
Allow,
|
||||
Block,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct MethodPolicy {
|
||||
pub mode: MethodPolicyMode,
|
||||
pub methods: Vec<String>,
|
||||
}
|
||||
impl MethodPolicy {
|
||||
pub fn validate(&mut self) -> Result<()> {
|
||||
ensure!(self.methods.len() <= 32, "method policy allows at most 32 methods");
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for method in &mut self.methods {
|
||||
*method = method.trim().to_ascii_uppercase();
|
||||
ensure!(
|
||||
!method.is_empty() && method.len() <= 32 && method.parse::<http::Method>().is_ok(),
|
||||
"invalid HTTP method in method policy"
|
||||
);
|
||||
ensure!(seen.insert(method.clone()), "duplicate HTTP method in method policy");
|
||||
}
|
||||
ensure!(
|
||||
self.mode == MethodPolicyMode::Off || !self.methods.is_empty(),
|
||||
"method allow/block policy requires at least one method"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn allows(&self, method: &http::Method) -> bool {
|
||||
let listed = self
|
||||
.methods
|
||||
.iter()
|
||||
.any(|candidate| candidate.eq_ignore_ascii_case(method.as_str()));
|
||||
match self.mode {
|
||||
MethodPolicyMode::Off => true,
|
||||
MethodPolicyMode::Allow => listed,
|
||||
MethodPolicyMode::Block => !listed,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn allow_header(&self) -> Option<http::HeaderValue> {
|
||||
(self.mode == MethodPolicyMode::Allow)
|
||||
.then(|| self.methods.join(", ").parse::<http::HeaderValue>().ok())
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
impl ProxyTimeouts {
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
for (name, value, max) in [
|
||||
@@ -204,6 +257,9 @@ pub struct Vhost {
|
||||
pub backend_timing_header: bool,
|
||||
#[serde(default)]
|
||||
pub rate_limit: crate::rate_limit::Settings,
|
||||
/// Optional allow/block policy for HTTP methods. Vhost and location policies stack.
|
||||
#[serde(default)]
|
||||
pub method_policy: MethodPolicy,
|
||||
#[serde(default)]
|
||||
pub basic_auth: crate::basic_auth::Settings,
|
||||
pub id: String,
|
||||
@@ -403,8 +459,17 @@ pub struct PathRoute {
|
||||
pub enabled: bool,
|
||||
#[serde(default)]
|
||||
pub cache: Option<bool>,
|
||||
/// Optional reusable ACL profile applied in addition to vhost policy.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub acl_profile_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub acl: Option<Acl>,
|
||||
/// Location-scoped country allow/block policy, stacked with vhost/profile policy.
|
||||
#[serde(default)]
|
||||
pub country_access: crate::rate_limit::CountryAccess,
|
||||
/// Location-scoped HTTP method allow/block policy, stacked with vhost policy.
|
||||
#[serde(default)]
|
||||
pub method_policy: MethodPolicy,
|
||||
#[serde(default)]
|
||||
pub headers: HeaderRules,
|
||||
/// Optional origin-path transformation. Query parameters are preserved.
|
||||
|
||||
@@ -82,6 +82,30 @@ mod default_listener_tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn method_policy_normalizes_and_enforces_allow_and_block_modes() {
|
||||
let mut allow: MethodPolicy = serde_json::from_value(json!({
|
||||
"mode": "allow",
|
||||
"methods": ["get", "HEAD", "post"]
|
||||
}))
|
||||
.unwrap();
|
||||
allow.validate().unwrap();
|
||||
assert_eq!(allow.methods, vec!["GET", "HEAD", "POST"]);
|
||||
assert!(allow.allows(&http::Method::GET));
|
||||
assert!(!allow.allows(&http::Method::DELETE));
|
||||
assert_eq!(allow.allow_header().unwrap().to_str().unwrap(), "GET, HEAD, POST");
|
||||
|
||||
let mut block: MethodPolicy = serde_json::from_value(json!({
|
||||
"mode": "block",
|
||||
"methods": ["delete"]
|
||||
}))
|
||||
.unwrap();
|
||||
block.validate().unwrap();
|
||||
assert!(!block.allows(&http::Method::DELETE));
|
||||
assert!(block.allows(&http::Method::GET));
|
||||
assert!(block.allow_header().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn h2c_is_cleartext_only_and_defaults_off() {
|
||||
let plain: Listener =
|
||||
|
||||
+14
-1
@@ -8,7 +8,10 @@ impl Default for PathRoute {
|
||||
match_type: PathMatch::Prefix,
|
||||
enabled: true,
|
||||
cache: None,
|
||||
acl_profile_id: None,
|
||||
acl: None,
|
||||
country_access: crate::rate_limit::CountryAccess::default(),
|
||||
method_policy: MethodPolicy::default(),
|
||||
headers: HeaderRules::default(),
|
||||
rewrite: PathRewrite::default(),
|
||||
response: None,
|
||||
@@ -28,7 +31,11 @@ impl PathRoute {
|
||||
&& self.source == Source::ReverseProxy
|
||||
&& self.backend_id.is_none()
|
||||
&& self.cache.is_none()
|
||||
&& self.acl_profile_id.is_none()
|
||||
&& self.acl.is_none()
|
||||
&& self.country_access.mode == crate::rate_limit::CountryMode::Off
|
||||
&& self.country_access.countries.is_empty()
|
||||
&& self.method_policy == MethodPolicy::default()
|
||||
&& self.headers.is_empty()
|
||||
&& self.rewrite.mode == RewriteMode::Preserve
|
||||
&& self.rewrite.value.is_empty()
|
||||
@@ -67,6 +74,7 @@ impl Vhost {
|
||||
if let Some(id) = &self.acl_profile_id {
|
||||
ensure!(crate::config::valid_id(id), "invalid ACL profile id");
|
||||
}
|
||||
self.method_policy.validate()?;
|
||||
ensure!(
|
||||
!self.frontend.listener_ids.is_empty(),
|
||||
"vhost requires at least one named listener"
|
||||
@@ -111,8 +119,13 @@ impl Vhost {
|
||||
// Normalize the untouched pre-0.16 synthetic root route out of the custom
|
||||
// location list so adding/removing Custom locations cannot delete fallback.
|
||||
self.routes.retain(|route| !route.is_legacy_main_fallback());
|
||||
for route in &self.routes {
|
||||
for route in &mut self.routes {
|
||||
route.source.validate()?;
|
||||
if let Some(id) = &route.acl_profile_id {
|
||||
ensure!(crate::config::valid_id(id), "invalid location ACL profile id");
|
||||
}
|
||||
route.country_access.validate()?;
|
||||
route.method_policy.validate()?;
|
||||
ensure!(
|
||||
route.source.is_reverse_proxy() || route.backend_id.is_none(),
|
||||
"Web Server location cannot pin a reverse-proxy backend"
|
||||
|
||||
Reference in new issue
Block a user