v0.34.0_1
This commit is contained in:
1 parent
9b3d6731cc
commit
00f29f92c8
68 files changed
+2599
-513
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user