v0.34.0_1

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-17 20:34:13 +02:00
1 parent 9b3d6731cc
commit 00f29f92c8
68 files changed
+2599 -513

No files matched your search

+1
View File
@@ -426,6 +426,7 @@ impl Service {
web_metrics: Arc::new(crate::web_server::metrics::PhpMetrics::default()),
id: self.id.clone(),
name: self.name.clone(),
sendfile: self.sendfile,
root,
index: self.index.clone(),
directory_listing: self.directory_listing,
+13
View File
@@ -216,6 +216,16 @@ pub enum SymlinkPolicy {
InsideRoot,
}
/// Opt-in kernel zero-copy for suitable Linux plain HTTP/1.1 static GETs.
/// Other requests continue through the original streaming implementation.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum SendfileMode {
#[default]
Off,
Auto,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default, deny_unknown_fields)]
pub struct RewriteRule {
@@ -338,6 +348,8 @@ pub struct Service {
pub document_root: PathBuf,
pub index: Vec<String>,
pub directory_listing: bool,
/// Transport-only preference: it must not rotate cache identity.
pub sendfile: SendfileMode,
/// Optional per-extension MIME overrides. Keys are extensions without a leading dot.
pub mime_types: BTreeMap<String, String>,
/// Fallback MIME type used when neither an override nor a built-in mapping exists.
@@ -382,6 +394,7 @@ impl Default for Service {
document_root: PathBuf::new(),
index: default_index(),
directory_listing: false,
sendfile: SendfileMode::Off,
mime_types: BTreeMap::new(),
default_mime_type: "application/octet-stream".into(),
cache_control: None,
+1
View File
@@ -47,6 +47,7 @@ pub struct CompiledService {
pub id: String,
pub(crate) web_metrics: Arc<crate::web_server::metrics::PhpMetrics>,
pub name: String,
pub sendfile: SendfileMode,
pub(super) root: PathBuf,
pub(super) index: Vec<String>,
pub(super) directory_listing: bool,
+1 -1
View File
@@ -17,7 +17,7 @@ mod serve;
pub use config::{
CacheRule, CompiledService, Hardening, Php, PhpFpmProfile, Precompressed, RewriteRule, Service,
SymlinkPolicy,
SymlinkPolicy, SendfileMode,
};
pub use htaccess::Config as HtaccessConfig;
pub use request::RequestContext;
+16
View File
@@ -162,6 +162,18 @@ async fn file_response_inner(
return response;
}
// Clone the already confined, validated file descriptor. Never reopen
// the user-controlled path for kernel sendfile (TOCTOU/symlink escape).
// When no HTTP/1 TCP bridge accepts it, the regular reader remains used.
#[cfg(target_os = "linux")]
let sendfile_candidate = if service.sendfile == super::SendfileMode::Auto
&& !multipart && single_range.is_none() && content_encoding.is_none()
&& body_length >= 64 * 1024
{
opened.file.try_clone().ok().map(|file| crate::server::sendfile::SendfileCandidate {
file: std::sync::Arc::new(file), length: body_length,
})
} else { None };
let mut file = tokio::fs::File::from_std(opened.file);
if let Some(boundary) = boundary {
let ranges = ranges.expect("multipart ranges");
@@ -211,6 +223,10 @@ async fn file_response_inner(
}
let stream = ReaderStream::with_capacity(file.take(body_length), stream_capacity);
*response.body_mut() = Body::new(file_body::FileBody::new(stream, body_length));
#[cfg(target_os = "linux")]
if let Some(candidate) = sendfile_candidate {
response.extensions_mut().insert(candidate);
}
response
}
+51
View File
@@ -2225,3 +2225,54 @@ async fn cancelled_static_and_multipart_bodies_release_files_and_count_only_emit
assert_eq!(service.web_metrics.view()["response_bytes"], accounted);
}
}
#[cfg(target_os = "linux")]
#[tokio::test]
async fn sendfile_auto_without_tcp_bridge_preserves_full_head_range_and_validators() {
let tmp = TempDir::new().unwrap();
let original = vec![0x5au8; 128 * 1024 + 3];
std::fs::write(tmp.path().join("large.bin"), &original).unwrap();
let service = Service {
id: "sendfile-fallback".into(),
name: "Sendfile fallback".into(),
document_root: tmp.path().to_path_buf(),
sendfile: SendfileMode::Auto,
..Default::default()
}.compile().unwrap();
let get = serve_request(&service, &request(Method::GET, "/large.bin"), "/large.bin").await;
assert_eq!(get.status(), StatusCode::OK);
assert_eq!(get.headers()[CONTENT_LENGTH].to_str().unwrap(), original.len().to_string());
// No transport adapter here. Even if a candidate extension was produced,
// its fallback Body must still stream the exact original representation.
let etag = get.headers()[ETAG].clone();
assert_eq!(get.into_body().collect().await.unwrap().to_bytes().to_vec(), original);
let head = serve_request(&service, &request(Method::HEAD, "/large.bin"), "/large.bin").await;
assert_eq!(head.status(), StatusCode::OK);
assert_eq!(head.headers()[CONTENT_LENGTH].to_str().unwrap(), original.len().to_string());
assert!(head.into_body().collect().await.unwrap().to_bytes().is_empty());
let mut range = request(Method::GET, "/large.bin");
range.headers_mut().insert(RANGE, HeaderValue::from_static("bytes=100-199"));
let ranged = serve_request(&service, &range, "/large.bin").await;
assert_eq!(ranged.status(), StatusCode::PARTIAL_CONTENT);
assert_eq!(ranged.headers()[CONTENT_RANGE].to_str().unwrap(), format!("bytes 100-199/{}", original.len()));
assert_eq!(ranged.into_body().collect().await.unwrap().to_bytes().to_vec(), original[100..200]);
let mut conditional = request(Method::GET, "/large.bin");
conditional.headers_mut().insert(IF_NONE_MATCH, etag);
let unchanged = serve_request(&service, &conditional, "/large.bin").await;
assert_eq!(unchanged.status(), StatusCode::NOT_MODIFIED);
assert!(unchanged.into_body().collect().await.unwrap().to_bytes().is_empty());
}
#[test]
fn sendfile_mode_is_backward_compatible_and_rejects_unknown_values() {
assert_eq!(SendfileMode::default(), SendfileMode::Off);
assert_eq!(serde_json::from_str::<SendfileMode>("\"auto\"").unwrap(), SendfileMode::Auto);
assert_eq!(serde_json::from_str::<SendfileMode>("\"off\"").unwrap(), SendfileMode::Off);
assert!(serde_json::from_str::<SendfileMode>("\"unsafe\"").is_err());
let legacy = serde_json::json!({"id":"legacy", "name":"Legacy", "document_root":"/tmp"});
assert_eq!(serde_json::from_value::<Service>(legacy).unwrap().sendfile, SendfileMode::Off);
}