This commit is contained in:
Mateusz Gruszczyński committed 2026-10-09 13:11:33 +02:00
1 parent f87eec7287
commit dd8c195600
38 files changed
+5491 -4682

No files matched your search

+73 -50
View File
@@ -1,3 +1,72 @@
impl Php {
pub fn validate(&mut self) -> Result<()> {
ensure!(
self.extensions.len() <= MAX_PHP_EXTENSIONS,
"Web Service PHP allows at most 16 extensions"
);
self.extensions = normalize_extensions(
std::mem::take(&mut self.extensions),
"Web Service PHP extension",
)?;
self.endpoint = self.endpoint.trim().to_owned();
ensure!(
self.connect_timeout_ms >= 50 && self.connect_timeout_ms <= 60_000,
"Web Service PHP connect timeout must be 50..60000 ms"
);
ensure!(
self.request_timeout_ms >= 100 && self.request_timeout_ms <= 3_600_000,
"Web Service PHP request timeout must be 100..3600000 ms"
);
ensure!(
(1..=4096).contains(&self.max_concurrent_requests),
"Web Service PHP max concurrent requests must be 1..4096"
);
ensure!(
self.max_request_body_bytes <= 8_u64 * 1024 * 1024 * 1024,
"Web Service PHP request body limit must be 0..8589934592 bytes"
);
ensure!(
self.max_response_header_bytes >= 4096
&& self.max_response_header_bytes <= 1024 * 1024,
"Web Service PHP response header limit must be 4096..1048576 bytes"
);
ensure!(
self.stderr_log_bytes <= 64 * 1024,
"Web Service PHP stderr log limit must be 0..65536 bytes"
);
self.custom_params = normalize_fastcgi_params(std::mem::take(&mut self.custom_params))?;
self.php_values = normalize_php_values(std::mem::take(&mut self.php_values))?;
self.php_admin_values = normalize_php_values(std::mem::take(&mut self.php_admin_values))?;
ensure!(!self.php_values.keys().any(|key| self.php_admin_values.contains_key(key)), "PHP_VALUE and PHP_ADMIN_VALUE must not define the same directive");
let php_custom_bytes = self
.custom_params
.iter()
.chain(self.php_values.iter())
.chain(self.php_admin_values.iter())
.map(|(name, value)| name.len() + value.len() + 2)
.sum::<usize>();
ensure!(
php_custom_bytes <= 128 * 1024,
"Web Service PHP custom FastCGI/PHP_VALUE data must be at most 128 KiB"
);
if self.enabled {
ensure!(!self.extensions.is_empty(), "PHP-FPM needs executable extensions");
FastCgiEndpoint::parse(&self.endpoint)?;
}
Ok(())
}
}
impl PhpFpmProfile {
pub fn validate(&mut self) -> Result<()> {
ensure!(crate::config::valid_id(&self.id), "invalid PHP-FPM profile id");
self.name = self.name.trim().to_owned();
ensure!(!self.name.is_empty() && self.name.len() <= MAX_SERVICE_NAME_BYTES
&& !self.name.chars().any(char::is_control), "PHP-FPM profile name is invalid");
self.php.validate()
}
}
impl Service {
pub fn validate(&mut self) -> Result<()> {
ensure!(crate::config::valid_id(&self.id), "invalid Web Service id");
@@ -206,56 +275,10 @@ impl Service {
"Web Service download extension",
)?;
ensure!(
self.php.extensions.len() <= MAX_PHP_EXTENSIONS,
"Web Service PHP allows at most 16 extensions"
);
self.php.extensions = normalize_extensions(
std::mem::take(&mut self.php.extensions),
"Web Service PHP extension",
)?;
self.php.endpoint = self.php.endpoint.trim().to_owned();
ensure!(
self.php.connect_timeout_ms >= 50 && self.php.connect_timeout_ms <= 60_000,
"Web Service PHP connect timeout must be 50..60000 ms"
);
ensure!(
self.php.request_timeout_ms >= 100 && self.php.request_timeout_ms <= 3_600_000,
"Web Service PHP request timeout must be 100..3600000 ms"
);
ensure!(
(1..=4096).contains(&self.php.max_concurrent_requests),
"Web Service PHP max concurrent requests must be 1..4096"
);
ensure!(
self.php.max_request_body_bytes <= 8_u64 * 1024 * 1024 * 1024,
"Web Service PHP request body limit must be 0..8589934592 bytes"
);
ensure!(
self.php.max_response_header_bytes >= 4096
&& self.php.max_response_header_bytes <= 1024 * 1024,
"Web Service PHP response header limit must be 4096..1048576 bytes"
);
ensure!(
self.php.stderr_log_bytes <= 64 * 1024,
"Web Service PHP stderr log limit must be 0..65536 bytes"
);
self.php.custom_params = normalize_fastcgi_params(std::mem::take(&mut self.php.custom_params))?;
self.php.php_values = normalize_php_values(std::mem::take(&mut self.php.php_values))?;
self.php.php_admin_values = normalize_php_values(std::mem::take(&mut self.php.php_admin_values))?;
ensure!(!self.php.php_values.keys().any(|key| self.php.php_admin_values.contains_key(key)), "PHP_VALUE and PHP_ADMIN_VALUE must not define the same directive");
let php_custom_bytes = self
.php
.custom_params
.iter()
.chain(self.php.php_values.iter())
.chain(self.php.php_admin_values.iter())
.map(|(name, value)| name.len() + value.len() + 2)
.sum::<usize>();
ensure!(
php_custom_bytes <= 128 * 1024,
"Web Service PHP custom FastCGI/PHP_VALUE data must be at most 128 KiB"
);
self.php.validate()?;
if let Some(id) = &self.php_profile_id {
ensure!(crate::config::valid_id(id), "invalid PHP-FPM profile reference");
}
htaccess::validate(&mut self.htaccess)?;
if self.php.enabled {
+15
View File
@@ -309,6 +309,16 @@ impl Default for Php {
}
}
/// Reusable PHP-FPM configuration. A Web Service can reference one profile or
/// keep its local `php` settings. Profiles are part of the revisioned Config.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default, deny_unknown_fields)]
pub struct PhpFpmProfile {
pub id: String,
pub name: String,
pub php: Php,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default, deny_unknown_fields)]
pub struct Hardening {
@@ -352,6 +362,10 @@ pub struct Service {
pub download_extensions: Vec<String>,
/// Symlink handling. The conservative default is `deny`.
pub symlink_policy: SymlinkPolicy,
/// Optional reference to a shared FPM profile; `php` stays the standalone
/// configuration and is preserved when attaching/detaching a profile.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub php_profile_id: Option<String>,
/// Optional FastCGI/PHP-FPM execution for configured script extensions.
pub php: Php,
/// Optional bounded Apache .htaccess compatibility layer compiled at Web Service apply time.
@@ -379,6 +393,7 @@ impl Default for Service {
compression: crate::compression::Overrides::default(),
download_extensions: Vec::new(),
symlink_policy: SymlinkPolicy::Deny,
php_profile_id: None,
php: Php::default(),
htaccess: HtaccessConfig::default(),
hardening: Hardening::default(),
+3
View File
@@ -201,6 +201,9 @@ pub(super) fn fastcgi_params(
})
.or_insert_with(|| value.to_owned());
}
// HTTP/2 and HTTP/3 use :authority and may not carry a literal Host
// header. WordPress and other CGI applications require HTTP_HOST.
params.entry("HTTP_HOST".into()).or_insert_with(|| context.server_name.clone());
// Trusted operator-defined parameters cannot override request-derived/core CGI names.
for (name, value) in &php.custom_params {
params.insert(name.clone(), value.clone());
+1 -1
View File
@@ -16,7 +16,7 @@ mod security;
mod serve;
pub use config::{
CacheRule, CompiledService, Hardening, Php, Precompressed, RewriteRule, Service, SymlinkPolicy,
CacheRule, CompiledService, Hardening, Php, PhpFpmProfile, Precompressed, RewriteRule, Service, SymlinkPolicy,
};
pub use htaccess::Config as HtaccessConfig;
pub use request::RequestContext;
+74
View File
@@ -285,3 +285,77 @@ fn php_admin_values_are_operator_only_and_serialized() {
configured.php.php_admin_values.insert("unsafe".into(), "value\nother=1".into());
assert!(configured.validate().is_err());
}
#[test]
fn fastcgi_always_sets_http_host_for_http2_http3_wordpress_requests() {
let tmp = TempDir::new().unwrap();
let mut configured = Service {
id: "host-fallback".into(),
name: "Host fallback".into(),
document_root: tmp.path().to_path_buf(),
php: Php { enabled: true, endpoint: "tcp:127.0.0.1:9000".into(), ..Default::default() },
..Default::default()
};
configured.validate().unwrap();
let service = configured.compile().unwrap();
let php = service.php.as_ref().unwrap();
let uri: Uri = "/wp-admin/index.php".parse().unwrap();
for version in [Version::HTTP_2, Version::HTTP_3] {
let encoded = fastcgi_params(
&service,
php,
&Method::GET,
&HeaderMap::new(),
&uri,
"/wp-admin/index.php",
None,
None,
&RequestContext {
client_ip: "127.0.0.1".parse().unwrap(),
client_port: Some(12345),
server_name: "example.test".into(),
server_port: 443,
tls: true,
version,
},
).unwrap();
assert!(encoded.windows(b"HTTP_HOST".len()).any(|w| w == b"HTTP_HOST"));
assert!(encoded.windows(b"example.test".len()).any(|w| w == b"example.test"));
}
}
#[test]
fn fastcgi_http_host_keeps_explicit_host_and_port() {
let tmp = TempDir::new().unwrap();
let mut configured = Service {
id: "host-port".into(),
name: "Host port".into(),
document_root: tmp.path().to_path_buf(),
php: Php { enabled: true, endpoint: "tcp:127.0.0.1:9000".into(), ..Default::default() },
..Default::default()
};
configured.validate().unwrap();
let service = configured.compile().unwrap();
let mut headers = HeaderMap::new();
headers.insert(http::header::HOST, HeaderValue::from_static("example.test:8443"));
let encoded = fastcgi_params(
&service,
service.php.as_ref().unwrap(),
&Method::GET,
&headers,
&"/index.php".parse().unwrap(),
"/index.php",
None,
None,
&RequestContext {
client_ip: "127.0.0.1".parse().unwrap(),
client_port: None,
server_name: "example.test".into(),
server_port: 443,
tls: true,
version: Version::HTTP_11,
},
).unwrap();
assert!(encoded.windows(b"HTTP_HOST".len()).any(|w| w == b"HTTP_HOST"));
assert!(encoded.windows(b"example.test:8443".len()).any(|w| w == b"example.test:8443"));
}