v0.33.0
This commit is contained in:
1 parent
bbfa8cbc3f
commit
2ccc598b12
49 files changed
+4771
-639
No files matched your search
@@ -26,8 +26,7 @@ impl Php {
|
||||
"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,
|
||||
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!(
|
||||
@@ -37,7 +36,13 @@ impl Php {
|
||||
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");
|
||||
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()
|
||||
@@ -50,7 +55,10 @@ impl Php {
|
||||
"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");
|
||||
ensure!(
|
||||
!self.extensions.is_empty(),
|
||||
"PHP-FPM needs executable extensions"
|
||||
);
|
||||
FastCgiEndpoint::parse(&self.endpoint)?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -59,10 +67,17 @@ impl Php {
|
||||
|
||||
impl PhpFpmProfile {
|
||||
pub fn validate(&mut self) -> Result<()> {
|
||||
ensure!(crate::config::valid_id(&self.id), "invalid PHP-FPM profile id");
|
||||
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");
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -98,7 +113,10 @@ impl Service {
|
||||
&& item != "..",
|
||||
"Web Service index entries must be plain file names"
|
||||
);
|
||||
ensure!(seen.insert(item.clone()), "duplicate Web Service index file");
|
||||
ensure!(
|
||||
seen.insert(item.clone()),
|
||||
"duplicate Web Service index file"
|
||||
);
|
||||
}
|
||||
ensure!(
|
||||
self.mime_types.len() <= MAX_MIME_TYPES,
|
||||
@@ -106,7 +124,10 @@ impl Service {
|
||||
);
|
||||
let mut normalized_mime_types = BTreeMap::new();
|
||||
for (extension, value) in std::mem::take(&mut self.mime_types) {
|
||||
let extension = extension.trim().trim_start_matches('.').to_ascii_lowercase();
|
||||
let extension = extension
|
||||
.trim()
|
||||
.trim_start_matches('.')
|
||||
.to_ascii_lowercase();
|
||||
ensure!(
|
||||
!extension.is_empty()
|
||||
&& extension.len() <= MAX_MIME_EXTENSION_BYTES
|
||||
@@ -160,14 +181,11 @@ impl Service {
|
||||
.matcher
|
||||
.strip_prefix("ext:")
|
||||
.is_some_and(valid_extension)
|
||||
|| rule
|
||||
.matcher
|
||||
.strip_prefix("mime:")
|
||||
.is_some_and(|value| {
|
||||
!value.is_empty()
|
||||
&& value.len() <= MAX_MIME_VALUE_BYTES
|
||||
&& !value.chars().any(char::is_control)
|
||||
});
|
||||
|| rule.matcher.strip_prefix("mime:").is_some_and(|value| {
|
||||
!value.is_empty()
|
||||
&& value.len() <= MAX_MIME_VALUE_BYTES
|
||||
&& !value.chars().any(char::is_control)
|
||||
});
|
||||
ensure!(
|
||||
valid_matcher,
|
||||
"Web Service cache rule matcher must be ext:<extension> or mime:<prefix>"
|
||||
@@ -192,8 +210,8 @@ impl Service {
|
||||
!name.is_empty() && name.len() <= MAX_HEADER_NAME_BYTES,
|
||||
"Web Service response header name must be 1..128 bytes"
|
||||
);
|
||||
let header_name = HeaderName::from_str(&name)
|
||||
.context("invalid Web Service response header name")?;
|
||||
let header_name =
|
||||
HeaderName::from_str(&name).context("invalid Web Service response header name")?;
|
||||
ensure!(
|
||||
!protected_service_header(&header_name),
|
||||
"Web Service response header cannot override framing/transport headers"
|
||||
@@ -203,8 +221,7 @@ impl Service {
|
||||
value.len() <= MAX_HEADER_VALUE_BYTES,
|
||||
"Web Service response header value must be at most 8192 bytes"
|
||||
);
|
||||
HeaderValue::from_str(&value)
|
||||
.context("invalid Web Service response header value")?;
|
||||
HeaderValue::from_str(&value).context("invalid Web Service response header value")?;
|
||||
ensure!(
|
||||
normalized_headers.insert(name, value).is_none(),
|
||||
"duplicate Web Service response header"
|
||||
@@ -277,7 +294,10 @@ impl Service {
|
||||
|
||||
self.php.validate()?;
|
||||
if let Some(id) = &self.php_profile_id {
|
||||
ensure!(crate::config::valid_id(id), "invalid PHP-FPM profile reference");
|
||||
ensure!(
|
||||
crate::config::valid_id(id),
|
||||
"invalid PHP-FPM profile reference"
|
||||
);
|
||||
}
|
||||
htaccess::validate(&mut self.htaccess)?;
|
||||
|
||||
@@ -320,7 +340,10 @@ impl Service {
|
||||
);
|
||||
#[cfg(unix)]
|
||||
let root_handle = Arc::new(open_root_directory(&root).with_context(|| {
|
||||
format!("Web Service {} document root could not be opened safely", self.id)
|
||||
format!(
|
||||
"Web Service {} document root could not be opened safely",
|
||||
self.id
|
||||
)
|
||||
})?);
|
||||
let htaccess = htaccess::compile(&root, &self.htaccess)?;
|
||||
if let Some(compiled) = &htaccess {
|
||||
@@ -337,10 +360,7 @@ impl Service {
|
||||
.response_headers
|
||||
.iter()
|
||||
.map(|(name, value)| {
|
||||
Ok::<_, anyhow::Error>((
|
||||
HeaderName::from_str(name)?,
|
||||
HeaderValue::from_str(value)?,
|
||||
))
|
||||
Ok::<_, anyhow::Error>((HeaderName::from_str(name)?, HeaderValue::from_str(value)?))
|
||||
})
|
||||
.collect::<std::result::Result<Vec<_>, anyhow::Error>>()?;
|
||||
let cache_rules = self
|
||||
@@ -367,12 +387,20 @@ impl Service {
|
||||
Ok(CompiledRewriteRule {
|
||||
pattern: Regex::new(&rule.pattern)?,
|
||||
replacement: rule.replacement.clone(),
|
||||
status: rule.status.and_then(|value| StatusCode::from_u16(value).ok()),
|
||||
status: rule
|
||||
.status
|
||||
.and_then(|value| StatusCode::from_u16(value).ok()),
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
let mut script_extensions: HashSet<String> = self.php.extensions.iter().cloned().collect();
|
||||
script_extensions.extend(["php", "phtml", "phar", "php3", "php4", "php5", "php7", "php8"].into_iter().map(str::to_owned));
|
||||
script_extensions.extend(
|
||||
[
|
||||
"php", "phtml", "phar", "php3", "php4", "php5", "php7", "php8",
|
||||
]
|
||||
.into_iter()
|
||||
.map(str::to_owned),
|
||||
);
|
||||
let php = if self.php.enabled {
|
||||
Some(CompiledPhp {
|
||||
extensions: self.php.extensions.iter().cloned().collect(),
|
||||
@@ -381,6 +409,7 @@ impl Service {
|
||||
connect_timeout: Duration::from_millis(self.php.connect_timeout_ms),
|
||||
request_timeout: Duration::from_millis(self.php.request_timeout_ms),
|
||||
permits: Arc::new(Semaphore::new(self.php.max_concurrent_requests)),
|
||||
max_concurrent: self.php.max_concurrent_requests,
|
||||
pass_authorization: self.php.pass_authorization,
|
||||
max_request_body_bytes: self.php.max_request_body_bytes,
|
||||
max_response_header_bytes: self.php.max_response_header_bytes,
|
||||
@@ -403,10 +432,9 @@ impl Service {
|
||||
mime_types: self
|
||||
.mime_types
|
||||
.iter()
|
||||
.map(|(extension, value)| {
|
||||
Ok((extension.clone(), HeaderValue::from_str(value)?))
|
||||
})
|
||||
.collect::<std::result::Result<BTreeMap<_, _>, http::header::InvalidHeaderValue>>()?,
|
||||
.map(|(extension, value)| Ok((extension.clone(), HeaderValue::from_str(value)?)))
|
||||
.collect::<std::result::Result<BTreeMap<_, _>, http::header::InvalidHeaderValue>>(
|
||||
)?,
|
||||
default_mime_type: HeaderValue::from_str(&self.default_mime_type)?,
|
||||
cache_control: self
|
||||
.cache_control
|
||||
|
||||
@@ -31,6 +31,7 @@ pub(crate) struct CompiledPhp {
|
||||
pub(super) connect_timeout: Duration,
|
||||
pub(super) request_timeout: Duration,
|
||||
pub(crate) permits: Arc<Semaphore>,
|
||||
pub(crate) max_concurrent: usize,
|
||||
pub(super) pass_authorization: bool,
|
||||
pub(super) max_request_body_bytes: u64,
|
||||
pub(super) max_response_header_bytes: usize,
|
||||
|
||||
@@ -338,6 +338,7 @@ async fn drain_fastcgi_body(
|
||||
initial: Vec<u8>,
|
||||
send_body: bool,
|
||||
stderr_log_bytes: usize,
|
||||
metrics: std::sync::Arc<crate::web_server::metrics::PhpMetrics>,
|
||||
_permit: OwnedSemaphorePermit,
|
||||
) {
|
||||
if send_body && !initial.is_empty() && tx.send(Ok(Bytes::from(initial))).await.is_err() {
|
||||
@@ -361,6 +362,7 @@ async fn drain_fastcgi_body(
|
||||
Ok((FCGI_END_REQUEST, _)) => break,
|
||||
Ok(_) => {}
|
||||
Err(error) => {
|
||||
metrics.failure(8);
|
||||
if send_body {
|
||||
let _ = tx.send(Err(error)).await;
|
||||
}
|
||||
|
||||
@@ -11,8 +11,9 @@ pub(super) async fn php_response(
|
||||
context: RequestContext,
|
||||
) -> Response {
|
||||
let start = Instant::now();
|
||||
let response = php_response_inner( service, php, method, headers, original_uri, script_path, path_info, query, body, context).await;
|
||||
let mut response = php_response_inner(service, php, method, headers, original_uri, script_path, path_info, query, body, context).await;
|
||||
php.metrics.record(response.status(), start.elapsed().as_micros().min(u64::MAX as u128) as u64);
|
||||
response.extensions_mut().insert(crate::web_server::metrics::ResponseKind::Php);
|
||||
response
|
||||
}
|
||||
|
||||
@@ -42,6 +43,7 @@ async fn php_response_inner(
|
||||
let permit = match php.permits.clone().try_acquire_owned() {
|
||||
Ok(permit) => permit,
|
||||
Err(_) => {
|
||||
php.metrics.failure(0);
|
||||
let mut response = builtin_error_response(StatusCode::SERVICE_UNAVAILABLE, &method);
|
||||
response
|
||||
.headers_mut()
|
||||
@@ -52,10 +54,11 @@ async fn php_response_inner(
|
||||
let mut io = match tokio::time::timeout(php.connect_timeout, connect_fastcgi(&php.endpoint)).await {
|
||||
Ok(Ok(io)) => io,
|
||||
Ok(Err(error)) => {
|
||||
php.metrics.failure(2);
|
||||
tracing::warn!(%error, service = %service.id, "PHP-FPM connection failed");
|
||||
return builtin_error_response(StatusCode::BAD_GATEWAY, &method);
|
||||
}
|
||||
Err(_) => return builtin_error_response(StatusCode::GATEWAY_TIMEOUT, &method),
|
||||
Err(_) => { php.metrics.failure(1); return builtin_error_response(StatusCode::GATEWAY_TIMEOUT, &method); },
|
||||
};
|
||||
let params = match fastcgi_params(
|
||||
service,
|
||||
@@ -71,6 +74,7 @@ async fn php_response_inner(
|
||||
Ok(params) => params,
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, service = %service.id, "PHP-FPM parameter preparation failed");
|
||||
php.metrics.failure(4);
|
||||
return builtin_error_response(StatusCode::BAD_GATEWAY, &method);
|
||||
}
|
||||
};
|
||||
@@ -109,10 +113,11 @@ async fn php_response_inner(
|
||||
return builtin_error_response(StatusCode::PAYLOAD_TOO_LARGE, &method);
|
||||
}
|
||||
Ok(Err(FastCgiRequestWriteError::Io(error))) => {
|
||||
php.metrics.failure(4);
|
||||
tracing::warn!(%error, service = %service.id, "PHP-FPM request write failed");
|
||||
return builtin_error_response(StatusCode::BAD_GATEWAY, &method);
|
||||
}
|
||||
Err(_) => return builtin_error_response(StatusCode::GATEWAY_TIMEOUT, &method),
|
||||
Err(_) => { php.metrics.failure(3); return builtin_error_response(StatusCode::GATEWAY_TIMEOUT, &method); },
|
||||
}
|
||||
|
||||
let mut stdout = Vec::new();
|
||||
@@ -122,10 +127,12 @@ async fn php_response_inner(
|
||||
let (record_type, content) = match record {
|
||||
Ok(value) => value,
|
||||
Err(error) if error.kind() == io::ErrorKind::TimedOut => {
|
||||
php.metrics.failure(5);
|
||||
return builtin_error_response(StatusCode::GATEWAY_TIMEOUT, &method)
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, service = %service.id, "PHP-FPM response read failed");
|
||||
php.metrics.failure(6);
|
||||
return builtin_error_response(StatusCode::BAD_GATEWAY, &method);
|
||||
}
|
||||
};
|
||||
@@ -133,6 +140,7 @@ async fn php_response_inner(
|
||||
FCGI_STDOUT => {
|
||||
stdout.extend_from_slice(&content);
|
||||
if stdout.len() > php.max_response_header_bytes {
|
||||
php.metrics.failure(7);
|
||||
return builtin_error_response(StatusCode::BAD_GATEWAY, &method);
|
||||
}
|
||||
if let Some((position, separator_len)) = find_cgi_header_end(&stdout) {
|
||||
@@ -142,6 +150,7 @@ async fn php_response_inner(
|
||||
Ok((status, headers)) => break (status, headers, body),
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, service = %service.id, "PHP-FPM CGI headers rejected");
|
||||
php.metrics.failure(7);
|
||||
return builtin_error_response(StatusCode::BAD_GATEWAY, &method);
|
||||
}
|
||||
}
|
||||
@@ -152,6 +161,7 @@ async fn php_response_inner(
|
||||
stderr.extend_from_slice(&content[..content.len().min(room)]);
|
||||
}
|
||||
FCGI_END_REQUEST => {
|
||||
php.metrics.failure(7);
|
||||
if !stderr.is_empty() {
|
||||
tracing::warn!(stderr = %String::from_utf8_lossy(&stderr), service = %service.id, "PHP-FPM FastCGI stderr");
|
||||
}
|
||||
@@ -175,6 +185,7 @@ async fn php_response_inner(
|
||||
initial_body,
|
||||
send_body,
|
||||
php.stderr_log_bytes,
|
||||
php.metrics.clone(),
|
||||
permit,
|
||||
));
|
||||
let mut response = Response::new(if send_body {
|
||||
|
||||
+117
-4
@@ -1,6 +1,26 @@
|
||||
//! Fixed-cardinality PHP counters. Values are per compiled Web Service snapshot.
|
||||
//! Fixed-cardinality, per-compiled-Web-Service counters. No request-derived labels.
|
||||
use http::{Method, StatusCode};
|
||||
use serde_json::{json, Value};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(super) enum ResponseKind {
|
||||
Static,
|
||||
Php,
|
||||
}
|
||||
|
||||
pub(crate) const PHP_FAILURE_NAMES: [&str; 9] = [
|
||||
"capacity",
|
||||
"connect_timeout",
|
||||
"connect_error",
|
||||
"request_timeout",
|
||||
"request_io",
|
||||
"response_timeout",
|
||||
"response_io",
|
||||
"invalid_response",
|
||||
"body_io",
|
||||
];
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct PhpMetrics {
|
||||
pub(crate) requests: AtomicU64,
|
||||
@@ -9,10 +29,32 @@ pub(crate) struct PhpMetrics {
|
||||
pub(crate) status_4xx: AtomicU64,
|
||||
pub(crate) status_5xx: AtomicU64,
|
||||
pub(crate) response_headers_us: AtomicU64,
|
||||
pub(crate) response_bytes: AtomicU64, // observed uncompressed body frames, not wire bytes
|
||||
pub(crate) in_flight_headers: AtomicU64,
|
||||
pub(crate) static_requests: AtomicU64,
|
||||
pub(crate) php_requests: AtomicU64,
|
||||
pub(crate) other_requests: AtomicU64,
|
||||
pub(crate) methods: [AtomicU64; 5], // GET POST PUT DELETE other
|
||||
pub(crate) selected_status: [AtomicU64; 8], // 200 206 304 403 404 500 502 503
|
||||
pub(crate) failures: [AtomicU64; 9],
|
||||
pub(crate) compression: [AtomicU64; 4], // gzip Brotli zstd deflate (post-route encoder only)
|
||||
}
|
||||
|
||||
pub(crate) struct HeadersInFlight<'a>(&'a AtomicU64);
|
||||
impl<'a> HeadersInFlight<'a> {
|
||||
pub(crate) fn new(counter: &'a AtomicU64) -> Self {
|
||||
counter.fetch_add(1, Ordering::Relaxed);
|
||||
Self(counter)
|
||||
}
|
||||
}
|
||||
impl Drop for HeadersInFlight<'_> {
|
||||
fn drop(&mut self) {
|
||||
self.0.fetch_sub(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
impl PhpMetrics {
|
||||
pub fn record(&self, status: http::StatusCode, elapsed_us: u64) {
|
||||
pub(crate) fn record(&self, status: StatusCode, elapsed_us: u64) {
|
||||
self.requests.fetch_add(1, Ordering::Relaxed);
|
||||
let counter = match status.as_u16() / 100 {
|
||||
2 => Some(&self.status_2xx),
|
||||
@@ -21,7 +63,78 @@ impl PhpMetrics {
|
||||
5 => Some(&self.status_5xx),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(counter) = counter { counter.fetch_add(1, Ordering::Relaxed); }
|
||||
self.response_headers_us.fetch_add(elapsed_us, Ordering::Relaxed);
|
||||
if let Some(counter) = counter {
|
||||
counter.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
self.response_headers_us
|
||||
.fetch_add(elapsed_us, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub(super) fn record_web(
|
||||
&self,
|
||||
status: StatusCode,
|
||||
elapsed_us: u64,
|
||||
method: &Method,
|
||||
kind: Option<ResponseKind>,
|
||||
) {
|
||||
self.record(status, elapsed_us);
|
||||
let method_index = if method == Method::GET {
|
||||
0
|
||||
} else if method == Method::POST {
|
||||
1
|
||||
} else if method == Method::PUT {
|
||||
2
|
||||
} else if method == Method::DELETE {
|
||||
3
|
||||
} else {
|
||||
4
|
||||
};
|
||||
self.methods[method_index].fetch_add(1, Ordering::Relaxed);
|
||||
match kind {
|
||||
Some(ResponseKind::Static) => &self.static_requests,
|
||||
Some(ResponseKind::Php) => &self.php_requests,
|
||||
None => &self.other_requests,
|
||||
}
|
||||
.fetch_add(1, Ordering::Relaxed);
|
||||
let status_index = [200, 206, 304, 403, 404, 500, 502, 503]
|
||||
.iter()
|
||||
.position(|code| *code == status.as_u16());
|
||||
if let Some(index) = status_index {
|
||||
self.selected_status[index].fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn failure(&self, index: usize) {
|
||||
if let Some(counter) = self.failures.get(index) {
|
||||
counter.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn record_encoding(&self, encoding: crate::compression::AppliedEncoding) {
|
||||
let index = match encoding {
|
||||
crate::compression::AppliedEncoding::Gzip => 0,
|
||||
crate::compression::AppliedEncoding::Brotli => 1,
|
||||
crate::compression::AppliedEncoding::Zstd => 2,
|
||||
crate::compression::AppliedEncoding::Deflate => 3,
|
||||
};
|
||||
self.compression[index].fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub(crate) fn view(&self) -> Value {
|
||||
let n = |counter: &AtomicU64| counter.load(Ordering::Relaxed);
|
||||
json!({
|
||||
"requests": n(&self.requests),
|
||||
"status_2xx": n(&self.status_2xx), "status_3xx": n(&self.status_3xx),
|
||||
"status_4xx": n(&self.status_4xx), "status_5xx": n(&self.status_5xx),
|
||||
"response_headers_us": n(&self.response_headers_us),
|
||||
"response_bytes": n(&self.response_bytes),
|
||||
"active_headers": n(&self.in_flight_headers),
|
||||
"static_requests": n(&self.static_requests),
|
||||
"php_requests": n(&self.php_requests), "other_requests": n(&self.other_requests),
|
||||
"methods": self.methods.iter().map(n).collect::<Vec<_>>(),
|
||||
"selected_status": self.selected_status.iter().map(n).collect::<Vec<_>>(),
|
||||
"failures": self.failures.iter().map(n).collect::<Vec<_>>(),
|
||||
"compression": self.compression.iter().map(n).collect::<Vec<_>>()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ mod config;
|
||||
mod fastcgi;
|
||||
mod filesystem;
|
||||
mod htaccess;
|
||||
mod metrics;
|
||||
pub(crate) mod metrics;
|
||||
mod request;
|
||||
mod response;
|
||||
mod routing;
|
||||
@@ -16,7 +16,8 @@ mod security;
|
||||
mod serve;
|
||||
|
||||
pub use config::{
|
||||
CacheRule, CompiledService, Hardening, Php, PhpFpmProfile, Precompressed, RewriteRule, Service, SymlinkPolicy,
|
||||
CacheRule, CompiledService, Hardening, Php, PhpFpmProfile, Precompressed, RewriteRule, Service,
|
||||
SymlinkPolicy,
|
||||
};
|
||||
pub use htaccess::Config as HtaccessConfig;
|
||||
pub use request::RequestContext;
|
||||
|
||||
@@ -5,6 +5,22 @@ pub(super) async fn file_response(
|
||||
method: &Method,
|
||||
headers: &HeaderMap,
|
||||
content_encoding: Option<&'static str>,
|
||||
) -> Response {
|
||||
let mut response =
|
||||
file_response_inner(service, opened, path, method, headers, content_encoding).await;
|
||||
response
|
||||
.extensions_mut()
|
||||
.insert(crate::web_server::metrics::ResponseKind::Static);
|
||||
response
|
||||
}
|
||||
|
||||
async fn file_response_inner(
|
||||
service: &CompiledService,
|
||||
opened: Opened,
|
||||
path: &Path,
|
||||
method: &Method,
|
||||
headers: &HeaderMap,
|
||||
content_encoding: Option<&'static str>,
|
||||
) -> Response {
|
||||
let length = opened.metadata.len();
|
||||
let (etag, modified) = metadata_validators(&opened.metadata);
|
||||
@@ -65,9 +81,15 @@ pub(super) async fn file_response(
|
||||
};
|
||||
|
||||
let multipart = ranges.as_ref().is_some_and(|ranges| ranges.len() > 1);
|
||||
let single_range = ranges.as_ref().and_then(|ranges| (ranges.len() == 1).then_some(ranges[0]));
|
||||
let single_range = ranges
|
||||
.as_ref()
|
||||
.and_then(|ranges| (ranges.len() == 1).then_some(ranges[0]));
|
||||
let (status, start, body_length) = match single_range {
|
||||
Some(range) => (StatusCode::PARTIAL_CONTENT, range.start, range.end - range.start + 1),
|
||||
Some(range) => (
|
||||
StatusCode::PARTIAL_CONTENT,
|
||||
range.start,
|
||||
range.end - range.start + 1,
|
||||
),
|
||||
None if multipart => (StatusCode::PARTIAL_CONTENT, 0, 0),
|
||||
None => (StatusCode::OK, 0, length),
|
||||
};
|
||||
@@ -131,7 +153,10 @@ pub(super) async fn file_response(
|
||||
let mut file = tokio::fs::File::from_std(opened.file);
|
||||
if let Some(boundary) = boundary {
|
||||
let ranges = ranges.expect("multipart ranges");
|
||||
let mime_text = mime.to_str().unwrap_or("application/octet-stream").to_owned();
|
||||
let mime_text = mime
|
||||
.to_str()
|
||||
.unwrap_or("application/octet-stream")
|
||||
.to_owned();
|
||||
let (mut writer, reader) = tokio::io::duplex(64 * 1024);
|
||||
tokio::spawn(async move {
|
||||
use tokio::io::AsyncWriteExt;
|
||||
@@ -141,7 +166,10 @@ pub(super) async fn file_response(
|
||||
range.start, range.end
|
||||
);
|
||||
if writer.write_all(header.as_bytes()).await.is_err()
|
||||
|| file.seek(std::io::SeekFrom::Start(range.start)).await.is_err()
|
||||
|| file
|
||||
.seek(std::io::SeekFrom::Start(range.start))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -152,7 +180,9 @@ pub(super) async fn file_response(
|
||||
return;
|
||||
}
|
||||
}
|
||||
let _ = writer.write_all(format!("--{boundary}--\r\n").as_bytes()).await;
|
||||
let _ = writer
|
||||
.write_all(format!("--{boundary}--\r\n").as_bytes())
|
||||
.await;
|
||||
});
|
||||
*response.body_mut() = Body::from_stream(ReaderStream::new(reader));
|
||||
return response;
|
||||
|
||||
+101
-40
@@ -1,5 +1,7 @@
|
||||
use super::{
|
||||
fastcgi::{method_not_allowed_response, php_for_path, php_response, protected_script_without_handler},
|
||||
fastcgi::{
|
||||
method_not_allowed_response, php_for_path, php_response, protected_script_without_handler,
|
||||
},
|
||||
filesystem::{list_directory_sync, path_components},
|
||||
htaccess,
|
||||
response::{
|
||||
@@ -59,7 +61,10 @@ async fn serve_inner(
|
||||
context: RequestContext,
|
||||
) -> Response {
|
||||
if crate::acl::validate_decoded_routing_path(path).is_err() {
|
||||
return status_response(StatusCode::BAD_REQUEST, "RustProxy: invalid Web Server path\n");
|
||||
return status_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"RustProxy: invalid Web Server path\n",
|
||||
);
|
||||
}
|
||||
if hardening_denied_method(service, &method) || hardening_denied_path(service, path) {
|
||||
return builtin_error_response(StatusCode::FORBIDDEN, &method);
|
||||
@@ -68,8 +73,11 @@ async fn serve_inner(
|
||||
return method_not_allowed_response();
|
||||
}
|
||||
|
||||
if path_components(path).ok().is_some_and(|components| components.iter().any(|component| htaccess::protected_component(service.htaccess.as_ref(), component)))
|
||||
|| htaccess::denied(service.htaccess.as_ref(), path)
|
||||
if path_components(path).ok().is_some_and(|components| {
|
||||
components
|
||||
.iter()
|
||||
.any(|component| htaccess::protected_component(service.htaccess.as_ref(), component))
|
||||
}) || htaccess::denied(service.htaccess.as_ref(), path)
|
||||
{
|
||||
return builtin_error_response(StatusCode::FORBIDDEN, &method);
|
||||
}
|
||||
@@ -93,15 +101,30 @@ async fn serve_inner(
|
||||
&context,
|
||||
&rewritten_path,
|
||||
rewritten_query.as_deref(),
|
||||
).await {
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(htaccess::RewriteOutcome::Redirect(response)) => return response,
|
||||
Ok(htaccess::RewriteOutcome::Forbidden) => return builtin_error_response(StatusCode::FORBIDDEN, &method),
|
||||
Ok(htaccess::RewriteOutcome::Forbidden) => {
|
||||
return builtin_error_response(StatusCode::FORBIDDEN, &method)
|
||||
}
|
||||
Ok(htaccess::RewriteOutcome::Internal { path, query }) => (path, query),
|
||||
Err(_) => return status_response(StatusCode::BAD_REQUEST, "RustProxy: invalid .htaccess rewrite target\n"),
|
||||
Err(_) => {
|
||||
return status_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"RustProxy: invalid .htaccess rewrite target\n",
|
||||
)
|
||||
}
|
||||
};
|
||||
if crate::acl::validate_decoded_routing_path(&rewritten_path).is_err()
|
||||
|| hardening_denied_path(service, &rewritten_path)
|
||||
|| path_components(&rewritten_path).ok().is_some_and(|components| components.iter().any(|component| htaccess::protected_component(service.htaccess.as_ref(), component)))
|
||||
|| path_components(&rewritten_path)
|
||||
.ok()
|
||||
.is_some_and(|components| {
|
||||
components.iter().any(|component| {
|
||||
htaccess::protected_component(service.htaccess.as_ref(), component)
|
||||
})
|
||||
})
|
||||
|| htaccess::denied(service.htaccess.as_ref(), &rewritten_path)
|
||||
{
|
||||
return builtin_error_response(StatusCode::FORBIDDEN, &method);
|
||||
@@ -115,7 +138,9 @@ async fn serve_inner(
|
||||
if let Some((script_path, path_info)) = resolve_php_path_info(service, &components).await {
|
||||
let script_components = path_components(&script_path).unwrap_or_default();
|
||||
if hardening_denied_path(service, &script_path)
|
||||
|| script_components.iter().any(|component| htaccess::protected_component(service.htaccess.as_ref(), component))
|
||||
|| script_components.iter().any(|component| {
|
||||
htaccess::protected_component(service.htaccess.as_ref(), component)
|
||||
})
|
||||
|| htaccess::denied(service.htaccess.as_ref(), &script_path)
|
||||
{
|
||||
return builtin_error_response(StatusCode::FORBIDDEN, &method);
|
||||
@@ -138,29 +163,26 @@ async fn serve_inner(
|
||||
}
|
||||
}
|
||||
|
||||
let (effective_path, effective_query) = match resolve_try_files(
|
||||
service,
|
||||
&rewritten_path,
|
||||
rewritten_query.as_deref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(TryFilesOutcome::Path { path, query }) => (path, query),
|
||||
Ok(TryFilesOutcome::Status(status)) => return builtin_error_response(status, &method),
|
||||
Err(_) => {
|
||||
return status_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"RustProxy: invalid Web Server try_files target\n",
|
||||
)
|
||||
}
|
||||
};
|
||||
let (effective_path, effective_query) =
|
||||
match resolve_try_files(service, &rewritten_path, rewritten_query.as_deref()).await {
|
||||
Ok(TryFilesOutcome::Path { path, query }) => (path, query),
|
||||
Ok(TryFilesOutcome::Status(status)) => return builtin_error_response(status, &method),
|
||||
Err(_) => {
|
||||
return status_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"RustProxy: invalid Web Server try_files target\n",
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let components = match path_components(&effective_path) {
|
||||
Ok(components) => components,
|
||||
Err(error) => return io_response(&error),
|
||||
};
|
||||
if hardening_denied_path(service, &effective_path)
|
||||
|| components.iter().any(|component| htaccess::protected_component(service.htaccess.as_ref(), component))
|
||||
|| components
|
||||
.iter()
|
||||
.any(|component| htaccess::protected_component(service.htaccess.as_ref(), component))
|
||||
|| htaccess::denied(service.htaccess.as_ref(), &effective_path)
|
||||
{
|
||||
return builtin_error_response(StatusCode::FORBIDDEN, &method);
|
||||
@@ -169,12 +191,19 @@ async fn serve_inner(
|
||||
let opened = match open_components(service, components.clone()).await {
|
||||
Ok(opened) => opened,
|
||||
Err(error)
|
||||
if matches!(error.kind(), io::ErrorKind::NotFound | io::ErrorKind::NotADirectory) =>
|
||||
if matches!(
|
||||
error.kind(),
|
||||
io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
|
||||
) =>
|
||||
{
|
||||
if let Some((script_path, path_info)) = resolve_php_path_info(service, &components).await {
|
||||
if let Some((script_path, path_info)) =
|
||||
resolve_php_path_info(service, &components).await
|
||||
{
|
||||
let script_components = path_components(&script_path).unwrap_or_default();
|
||||
if hardening_denied_path(service, &script_path)
|
||||
|| script_components.iter().any(|component| htaccess::protected_component(service.htaccess.as_ref(), component))
|
||||
|| script_components.iter().any(|component| {
|
||||
htaccess::protected_component(service.htaccess.as_ref(), component)
|
||||
})
|
||||
|| htaccess::denied(service.htaccess.as_ref(), &script_path)
|
||||
{
|
||||
return builtin_error_response(StatusCode::FORBIDDEN, &method);
|
||||
@@ -290,15 +319,8 @@ async fn serve_inner(
|
||||
)
|
||||
.await;
|
||||
}
|
||||
return file_response(
|
||||
service,
|
||||
index_opened,
|
||||
&public_path,
|
||||
&method,
|
||||
&headers,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
return file_response(service, index_opened, &public_path, &method, &headers, None)
|
||||
.await;
|
||||
}
|
||||
Ok(_) => continue,
|
||||
Err(error)
|
||||
@@ -316,7 +338,11 @@ async fn serve_inner(
|
||||
if method != Method::GET && method != Method::HEAD {
|
||||
return method_not_allowed_response();
|
||||
}
|
||||
if !htaccess::directory_listing(service.htaccess.as_ref(), &components, service.directory_listing) {
|
||||
if !htaccess::directory_listing(
|
||||
service.htaccess.as_ref(),
|
||||
&components,
|
||||
service.directory_listing,
|
||||
) {
|
||||
return builtin_error_response(StatusCode::FORBIDDEN, &method);
|
||||
}
|
||||
let service_for_list = service.clone();
|
||||
@@ -369,6 +395,9 @@ async fn serve_inner(
|
||||
.headers_mut()
|
||||
.insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
|
||||
response
|
||||
.extensions_mut()
|
||||
.insert(crate::web_server::metrics::ResponseKind::Static);
|
||||
response
|
||||
}
|
||||
|
||||
/// Serve a decoded, validated routing path from a compiled Web Service.
|
||||
@@ -384,11 +413,43 @@ pub async fn serve(
|
||||
body: Body,
|
||||
context: RequestContext,
|
||||
) -> Response {
|
||||
use std::sync::atomic::Ordering;
|
||||
let start = std::time::Instant::now();
|
||||
let _in_flight =
|
||||
crate::web_server::metrics::HeadersInFlight::new(&service.web_metrics.in_flight_headers);
|
||||
let request_method = method.clone();
|
||||
let mut response = serve_inner(service, method, headers, uri, path, body, context).await;
|
||||
add_service_headers(&mut response, service);
|
||||
htaccess::apply_response(service.htaccess.as_ref(), &mut response, path);
|
||||
strip_unnecessary_headers(&mut response, service);
|
||||
service.web_metrics.record(response.status(), start.elapsed().as_micros().min(u64::MAX as u128) as u64);
|
||||
let kind = response
|
||||
.extensions()
|
||||
.get::<crate::web_server::metrics::ResponseKind>()
|
||||
.copied();
|
||||
service.web_metrics.record_web(
|
||||
response.status(),
|
||||
start.elapsed().as_micros().min(u64::MAX as u128) as u64,
|
||||
&request_method,
|
||||
kind,
|
||||
);
|
||||
// Count bytes actually yielded by the Web Server body before downstream compression;
|
||||
// never infer transferred bytes from Content-Length or count an unpolled response.
|
||||
let web = service.web_metrics.clone();
|
||||
let php = if matches!(kind, Some(crate::web_server::metrics::ResponseKind::Php)) {
|
||||
service.php.as_ref().map(|p| p.metrics.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let body = std::mem::replace(response.body_mut(), Body::empty());
|
||||
*response.body_mut() = Body::new(http_body_util::BodyExt::map_frame(body, move |frame| {
|
||||
if let Some(data) = frame.data_ref() {
|
||||
let size = data.len() as u64;
|
||||
web.response_bytes.fetch_add(size, Ordering::Relaxed);
|
||||
if let Some(metrics) = php.as_ref() {
|
||||
metrics.response_bytes.fetch_add(size, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
frame
|
||||
}));
|
||||
response
|
||||
}
|
||||
+511
-136
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user