This commit is contained in:
Mateusz Gruszczyński committed 2026-10-15 09:37:47 +02:00
1 parent bbfa8cbc3f
commit 2ccc598b12
49 files changed
+4771 -639

No files matched your search

+61 -33
View File
@@ -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
+1
View File
@@ -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,
+2
View File
@@ -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;
}
+14 -3
View File
@@ -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
View File
@@ -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<_>>()
})
}
}
+3 -2
View File
@@ -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;
+35 -5
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff