v0.34.4
This commit is contained in:
1 parent
ee174bc283
commit
52e7ce7b46
24 files changed
+672
-418
No files matched your search
+32
-109
@@ -48,58 +48,29 @@ pub fn compile_with_active(
|
||||
config.compression.validate()?;
|
||||
config.server_header.validate()?;
|
||||
let forwarding = config.forwarding.compile()?;
|
||||
ensure!(
|
||||
config.php_fpm_profiles.len() <= 128,
|
||||
"too many PHP-FPM profiles (max 128)"
|
||||
);
|
||||
ensure!(config.php_fpm_profiles.len() <= 128, "too many PHP-FPM profiles (max 128)");
|
||||
let mut php_profiles = HashMap::new();
|
||||
for profile in &mut config.php_fpm_profiles {
|
||||
profile
|
||||
.validate()
|
||||
.with_context(|| format!("PHP-FPM profile {}", profile.id))?;
|
||||
ensure!(
|
||||
php_profiles
|
||||
.insert(profile.id.clone(), profile.php.clone())
|
||||
.is_none(),
|
||||
"duplicate PHP-FPM profile id"
|
||||
);
|
||||
profile.validate().with_context(|| format!("PHP-FPM profile {}", profile.id))?;
|
||||
ensure!(php_profiles.insert(profile.id.clone(), profile.php.clone()).is_none(),
|
||||
"duplicate PHP-FPM profile id");
|
||||
}
|
||||
ensure!(
|
||||
config.web_services.len() <= 128,
|
||||
"too many Web Services (max 128)"
|
||||
);
|
||||
ensure!(config.web_services.len() <= 128, "too many Web Services (max 128)");
|
||||
let mut web_services = HashMap::new();
|
||||
// PHP-FPM can retain FastCGI PHP_VALUE/PHP_ADMIN_VALUE directives in reused
|
||||
// workers. Different directive sets on one socket can leak to the next vhost.
|
||||
// Report unsafe legacy configurations but keep them loadable for migration.
|
||||
let mut php_endpoint_policies: HashMap<
|
||||
String,
|
||||
(
|
||||
String,
|
||||
std::collections::BTreeMap<String, String>,
|
||||
std::collections::BTreeMap<String, String>,
|
||||
),
|
||||
> = HashMap::new();
|
||||
let mut php_endpoint_policies: HashMap<String, (String, std::collections::BTreeMap<String, String>, std::collections::BTreeMap<String, String>)> = HashMap::new();
|
||||
for service in &mut config.web_services {
|
||||
service
|
||||
.validate()
|
||||
.with_context(|| format!("Web Service {}", service.id))?;
|
||||
service.validate().with_context(|| format!("Web Service {}", service.id))?;
|
||||
// A profile changes effective runtime settings, not the saved standalone
|
||||
// PHP configuration. Detaching a profile restores local settings exactly.
|
||||
let mut effective = service.clone();
|
||||
if let Some(id) = &service.php_profile_id {
|
||||
effective.php = php_profiles
|
||||
.get(id)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Web Service {} references unknown PHP-FPM profile {}",
|
||||
service.id, id
|
||||
)
|
||||
})?
|
||||
.clone();
|
||||
effective
|
||||
.validate()
|
||||
.with_context(|| format!("Web Service {} PHP-FPM profile {}", service.id, id))?;
|
||||
effective.php = php_profiles.get(id).with_context(|| {
|
||||
format!("Web Service {} references unknown PHP-FPM profile {}", service.id, id)
|
||||
})?.clone();
|
||||
effective.validate().with_context(|| format!("Web Service {} PHP-FPM profile {}", service.id, id))?;
|
||||
}
|
||||
if effective.php.enabled {
|
||||
let php = &effective.php;
|
||||
@@ -111,25 +82,13 @@ pub fn compile_with_active(
|
||||
);
|
||||
}
|
||||
} else {
|
||||
php_endpoint_policies.insert(
|
||||
php.endpoint.clone(),
|
||||
(
|
||||
effective.id.clone(),
|
||||
php.php_values.clone(),
|
||||
php.php_admin_values.clone(),
|
||||
),
|
||||
);
|
||||
php_endpoint_policies.insert(php.endpoint.clone(), (
|
||||
effective.id.clone(), php.php_values.clone(), php.php_admin_values.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
let compiled = Arc::new(
|
||||
effective
|
||||
.compile()
|
||||
.with_context(|| format!("Web Service {}", service.id))?,
|
||||
);
|
||||
ensure!(
|
||||
web_services.insert(service.id.clone(), compiled).is_none(),
|
||||
"duplicate Web Service id"
|
||||
);
|
||||
let compiled = Arc::new(effective.compile().with_context(|| format!("Web Service {}", service.id))?);
|
||||
ensure!(web_services.insert(service.id.clone(), compiled).is_none(), "duplicate Web Service id");
|
||||
}
|
||||
ensure!(config.vhosts.len() <= 64, "too many vhosts (max 64)");
|
||||
let mut vhost_ids = HashSet::new();
|
||||
@@ -308,27 +267,19 @@ pub fn compile_with_active(
|
||||
// Prevent unbounded DNS ACL work across the entire published configuration.
|
||||
crate::acl::dynamic::configured_hostnames(&config)?;
|
||||
for agent in &config.docker_agents.agents {
|
||||
for profile_id in
|
||||
crate::acl::profile_ids(agent.acl_profile_id.as_deref(), &agent.acl_profile_ids)
|
||||
{
|
||||
for profile_id in crate::acl::profile_ids(agent.acl_profile_id.as_deref(), &agent.acl_profile_ids) {
|
||||
ensure!(
|
||||
acl_profiles.contains_key(profile_id),
|
||||
"Docker agent {} references unknown default ACL profile {}",
|
||||
agent.id,
|
||||
profile_id
|
||||
agent.id, profile_id
|
||||
);
|
||||
}
|
||||
for assignment in &agent.assignments {
|
||||
for profile_id in crate::acl::profile_ids(
|
||||
assignment.acl_profile_id.as_deref(),
|
||||
&assignment.acl_profile_ids,
|
||||
) {
|
||||
for profile_id in crate::acl::profile_ids(assignment.acl_profile_id.as_deref(), &assignment.acl_profile_ids) {
|
||||
ensure!(
|
||||
acl_profiles.contains_key(profile_id),
|
||||
"Docker assignment {} on agent {} references unknown ACL profile {}",
|
||||
assignment.service_key,
|
||||
agent.id,
|
||||
profile_id
|
||||
assignment.service_key, agent.id, profile_id
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -441,9 +392,7 @@ fn validate_route_rewrite(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_location_redirect(
|
||||
redirect: Option<&crate::vhost::LocationRedirect>,
|
||||
) -> Result<Option<crate::config::CompiledLocationRedirect>> {
|
||||
fn validate_location_redirect(redirect: Option<&crate::vhost::LocationRedirect>) -> Result<Option<crate::config::CompiledLocationRedirect>> {
|
||||
let Some(redirect) = redirect else {
|
||||
return Ok(None);
|
||||
};
|
||||
@@ -459,50 +408,24 @@ fn validate_location_redirect(
|
||||
);
|
||||
// Replace capture references with a harmless segment so URL structure can
|
||||
// be validated before requests reach the data plane.
|
||||
let authority_end = redirect
|
||||
.target
|
||||
.find("://")
|
||||
.and_then(|start| {
|
||||
let rest = &redirect.target[start + 3..];
|
||||
rest.char_indices()
|
||||
.find(|(_, ch)| matches!(ch, '/' | '?' | '#'))
|
||||
.map(|(offset, _)| start + 3 + offset)
|
||||
.or(Some(redirect.target.len()))
|
||||
})
|
||||
.ok_or_else(|| anyhow::anyhow!("location redirect target must be an absolute URL"))?;
|
||||
ensure!(
|
||||
!redirect.target[..authority_end].contains('$'),
|
||||
"location redirect captures are not allowed in the URL authority"
|
||||
);
|
||||
let authority_end = redirect.target.find("://").and_then(|start| {
|
||||
let rest = &redirect.target[start + 3..];
|
||||
rest.char_indices().find(|(_, ch)| matches!(ch, '/' | '?' | '#')).map(|(offset, _)| start + 3 + offset).or(Some(redirect.target.len()))
|
||||
}).ok_or_else(|| anyhow::anyhow!("location redirect target must be an absolute URL"))?;
|
||||
ensure!(!redirect.target[..authority_end].contains('$'), "location redirect captures are not allowed in the URL authority");
|
||||
let sample = regex::Regex::new(r"\$\{?[A-Za-z0-9_]+\}?")
|
||||
.expect("static capture regex")
|
||||
.replace_all(&redirect.target, "capture");
|
||||
let parsed =
|
||||
url::Url::parse(&sample).context("location redirect target must be an absolute URL")?;
|
||||
ensure!(
|
||||
matches!(parsed.scheme(), "http" | "https") && parsed.host_str().is_some(),
|
||||
"location redirect target must use http/https and include a host"
|
||||
);
|
||||
ensure!(
|
||||
parsed.username().is_empty() && parsed.password().is_none(),
|
||||
"location redirect target cannot contain credentials"
|
||||
);
|
||||
let parsed = url::Url::parse(&sample).context("location redirect target must be an absolute URL")?;
|
||||
ensure!(matches!(parsed.scheme(), "http" | "https") && parsed.host_str().is_some(), "location redirect target must use http/https and include a host");
|
||||
ensure!(parsed.username().is_empty() && parsed.password().is_none(), "location redirect target cannot contain credentials");
|
||||
let rewrite_regex = match redirect.rewrite_pattern.as_deref() {
|
||||
None | Some("") => None,
|
||||
Some(pattern) => {
|
||||
ensure!(
|
||||
pattern.len() <= 1024 && !pattern.chars().any(char::is_control),
|
||||
"invalid location redirect rewrite regex"
|
||||
);
|
||||
ensure!(pattern.len() <= 1024 && !pattern.chars().any(char::is_control), "invalid location redirect rewrite regex");
|
||||
let mut builder = regex::RegexBuilder::new(pattern);
|
||||
builder
|
||||
.size_limit(2 * 1024 * 1024)
|
||||
.dfa_size_limit(2 * 1024 * 1024);
|
||||
Some(
|
||||
builder
|
||||
.build()
|
||||
.context("invalid location redirect rewrite regex")?,
|
||||
)
|
||||
builder.size_limit(2 * 1024 * 1024).dfa_size_limit(2 * 1024 * 1024);
|
||||
Some(builder.build().context("invalid location redirect rewrite regex")?)
|
||||
}
|
||||
};
|
||||
Ok(Some(crate::config::CompiledLocationRedirect {
|
||||
|
||||
+14
-21
@@ -1,6 +1,9 @@
|
||||
impl Snapshot {
|
||||
// Exact-first lookups with stack-buffered wildcard keys for normal hosts.
|
||||
fn find_host_pattern<T>(host: &str, mut lookup: impl FnMut(&str) -> Option<T>) -> Option<T> {
|
||||
fn find_host_pattern<T>(
|
||||
host: &str,
|
||||
mut lookup: impl FnMut(&str) -> Option<T>,
|
||||
) -> Option<T> {
|
||||
if let Some(value) = lookup(host) {
|
||||
return Some(value);
|
||||
}
|
||||
@@ -133,7 +136,11 @@ impl Snapshot {
|
||||
});
|
||||
}
|
||||
}
|
||||
for (candidate, matched_by) in [(authority, "authority"), (host, "host"), (sni, "sni")] {
|
||||
for (candidate, matched_by) in [
|
||||
(authority, "authority"),
|
||||
(host, "host"),
|
||||
(sni, "sni"),
|
||||
] {
|
||||
let Some(candidate) = candidate else {
|
||||
continue;
|
||||
};
|
||||
@@ -205,10 +212,7 @@ impl Snapshot {
|
||||
/// Legacy inspection helper only; network handlers must call select_on.
|
||||
pub fn select(&self, host: &str, path: &str) -> Option<&CompiledRoute> {
|
||||
Self::find_host_pattern(host, |pattern| {
|
||||
let fallback = self
|
||||
.fallbacks
|
||||
.get(pattern)
|
||||
.and_then(|routes| routes.first())?;
|
||||
let fallback = self.fallbacks.get(pattern).and_then(|routes| routes.first())?;
|
||||
self.routes
|
||||
.get(pattern)
|
||||
.and_then(|routes| select_route(routes, path, |_| true))
|
||||
@@ -265,25 +269,17 @@ where
|
||||
prefix
|
||||
}
|
||||
impl CompiledRoute {
|
||||
pub fn redirect_location(
|
||||
&self,
|
||||
path: &str,
|
||||
uri: &http::Uri,
|
||||
) -> Result<Option<http::HeaderValue>> {
|
||||
pub fn redirect_location(&self, path: &str, uri: &http::Uri) -> Result<Option<http::HeaderValue>> {
|
||||
let Some(spec) = &self.redirect else {
|
||||
return Ok(None);
|
||||
};
|
||||
let target = if let Some(regex) = &spec.rewrite_regex {
|
||||
ensure!(
|
||||
regex.is_match(path),
|
||||
"location redirect rewrite regex did not match"
|
||||
);
|
||||
ensure!(regex.is_match(path), "location redirect rewrite regex did not match");
|
||||
regex.replace(path, spec.target.as_str()).into_owned()
|
||||
} else {
|
||||
spec.target.clone()
|
||||
};
|
||||
let mut target =
|
||||
url::Url::parse(&target).context("invalid compiled location redirect target")?;
|
||||
let mut target = url::Url::parse(&target).context("invalid compiled location redirect target")?;
|
||||
if spec.preserve_query {
|
||||
if let Some(query) = uri.query() {
|
||||
let merged = match target.query() {
|
||||
@@ -293,10 +289,7 @@ impl CompiledRoute {
|
||||
target.set_query(Some(&merged));
|
||||
}
|
||||
}
|
||||
Ok(Some(
|
||||
http::HeaderValue::from_str(target.as_str())
|
||||
.context("invalid redirect Location header")?,
|
||||
))
|
||||
Ok(Some(http::HeaderValue::from_str(target.as_str()).context("invalid redirect Location header")?))
|
||||
}
|
||||
/// Return a normalized replacement path for the upstream hop. `None` means
|
||||
/// preserve the client's exact origin-form path bytes.
|
||||
|
||||
Reference in new issue
Block a user