v0.33.8
This commit is contained in:
1 parent
6b937aaf85
commit
87c8393d86
79 files changed
+6727
-297
No files matched your search
@@ -1769,3 +1769,459 @@ async fn php_metrics_are_separate_from_static_and_record_capacity() {
|
||||
assert_eq!(php.max_concurrent, 2);
|
||||
assert_eq!(php.permits.available_permits(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn static_streaming_keeps_large_payloads_intact_in_bounded_frames() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let service = service(tmp.path(), false);
|
||||
let mut accounted = 0u64;
|
||||
for length in [
|
||||
0usize,
|
||||
1,
|
||||
1040,
|
||||
16 * 1024 + 3,
|
||||
64 * 1024,
|
||||
64 * 1024 + 1,
|
||||
1024 * 1024,
|
||||
] {
|
||||
let expected: Vec<u8> = (0..length).map(|index| (index % 251) as u8).collect();
|
||||
std::fs::write(tmp.path().join("asset.bin"), &expected).unwrap();
|
||||
let response =
|
||||
serve_request(&service, &request(Method::GET, "/asset.bin"), "/asset.bin").await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(response.headers()[CONTENT_LENGTH], length.to_string());
|
||||
let mut body = response.into_body();
|
||||
let mut received = Vec::new();
|
||||
let mut frames = 0usize;
|
||||
while let Some(frame) = body.frame().await {
|
||||
let data = frame.unwrap().into_data().unwrap();
|
||||
assert!(
|
||||
data.len() <= 64 * 1024,
|
||||
"static frame exceeded the memory bound"
|
||||
);
|
||||
received.extend_from_slice(&data);
|
||||
frames += 1;
|
||||
}
|
||||
assert_eq!(received, expected);
|
||||
if length >= 64 * 1024 {
|
||||
// A normal regular file must not regress to one dispatched read and
|
||||
// body-accounting pass for every 4 KiB of its representation.
|
||||
assert!(frames < length.div_ceil(4 * 1024));
|
||||
}
|
||||
accounted += length as u64;
|
||||
assert_eq!(service.web_metrics.view()["response_bytes"], accounted);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn large_static_ranges_keep_offsets_validators_and_head_semantics() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let expected: Vec<u8> = (0..256 * 1024 + 31)
|
||||
.map(|index| (index % 251) as u8)
|
||||
.collect();
|
||||
std::fs::write(tmp.path().join("asset.bin"), &expected).unwrap();
|
||||
let service = service(tmp.path(), false);
|
||||
let initial = serve_request(&service, &request(Method::HEAD, "/asset.bin"), "/asset.bin").await;
|
||||
let etag = initial.headers()[ETAG].clone();
|
||||
assert!(initial
|
||||
.into_body()
|
||||
.collect()
|
||||
.await
|
||||
.unwrap()
|
||||
.to_bytes()
|
||||
.is_empty());
|
||||
for (range, start, end) in [
|
||||
("bytes=0-0", 0, 1),
|
||||
("bytes=65530-131089", 65530, 131090),
|
||||
("bytes=-65543", expected.len() - 65543, expected.len()),
|
||||
("bytes=131065-", 131065, expected.len()),
|
||||
] {
|
||||
for method in [Method::GET, Method::HEAD] {
|
||||
let mut requested = request(method.clone(), "/asset.bin");
|
||||
requested
|
||||
.headers_mut()
|
||||
.insert(RANGE, range.parse().unwrap());
|
||||
requested.headers_mut().insert(IF_RANGE, etag.clone());
|
||||
let response = serve_request(&service, &requested, "/asset.bin").await;
|
||||
assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
|
||||
assert_eq!(
|
||||
response.headers()[CONTENT_LENGTH],
|
||||
(end - start).to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
response.headers()[CONTENT_RANGE],
|
||||
format!("bytes {start}-{}/{}", end - 1, expected.len())
|
||||
);
|
||||
assert_eq!(response.headers()[ETAG], etag);
|
||||
let data = response.into_body().collect().await.unwrap().to_bytes();
|
||||
if method == Method::HEAD {
|
||||
assert!(data.is_empty());
|
||||
} else {
|
||||
assert_eq!(&data[..], &expected[start..end]);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut changed = request(Method::GET, "/asset.bin");
|
||||
changed
|
||||
.headers_mut()
|
||||
.insert(RANGE, "bytes=65530-131089".parse().unwrap());
|
||||
changed
|
||||
.headers_mut()
|
||||
.insert(IF_RANGE, "\"changed\"".parse().unwrap());
|
||||
let response = serve_request(&service, &changed, "/asset.bin").await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
response.into_body().collect().await.unwrap().to_bytes(),
|
||||
expected
|
||||
);
|
||||
|
||||
let mut conditional = request(Method::GET, "/asset.bin");
|
||||
conditional
|
||||
.headers_mut()
|
||||
.insert(RANGE, "bytes=65530-131089".parse().unwrap());
|
||||
conditional.headers_mut().insert(IF_NONE_MATCH, etag);
|
||||
let response = serve_request(&service, &conditional, "/asset.bin").await;
|
||||
assert_eq!(response.status(), StatusCode::NOT_MODIFIED);
|
||||
assert!(response
|
||||
.into_body()
|
||||
.collect()
|
||||
.await
|
||||
.unwrap()
|
||||
.to_bytes()
|
||||
.is_empty());
|
||||
|
||||
let mut invalid = request(Method::GET, "/asset.bin");
|
||||
invalid
|
||||
.headers_mut()
|
||||
.insert(RANGE, "bytes=999999-".parse().unwrap());
|
||||
let response = serve_request(&service, &invalid, "/asset.bin").await;
|
||||
assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE);
|
||||
assert_eq!(
|
||||
response.headers()[CONTENT_RANGE],
|
||||
format!("bytes */{}", expected.len())
|
||||
);
|
||||
assert!(response
|
||||
.into_body()
|
||||
.collect()
|
||||
.await
|
||||
.unwrap()
|
||||
.to_bytes()
|
||||
.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn large_precompressed_and_multipart_representations_remain_exact() {
|
||||
use std::io::Write;
|
||||
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let plain: Vec<u8> = (0..512 * 1024)
|
||||
.map(|index| b'a' + (index % 26) as u8)
|
||||
.collect();
|
||||
let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::none());
|
||||
encoder.write_all(&plain).unwrap();
|
||||
let compressed = encoder.finish().unwrap();
|
||||
std::fs::write(tmp.path().join("asset.css"), &plain).unwrap();
|
||||
std::fs::write(tmp.path().join("asset.css.gz"), &compressed).unwrap();
|
||||
let service = Service {
|
||||
id: "large-compressed".into(),
|
||||
name: "Large compressed".into(),
|
||||
document_root: tmp.path().to_path_buf(),
|
||||
precompressed: Precompressed {
|
||||
enabled: true,
|
||||
brotli: false,
|
||||
gzip: true,
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
.compile()
|
||||
.unwrap();
|
||||
|
||||
let mut requested = request(Method::GET, "/asset.css");
|
||||
requested
|
||||
.headers_mut()
|
||||
.insert(http::header::ACCEPT_ENCODING, "gzip".parse().unwrap());
|
||||
let response = serve_request(&service, &requested, "/asset.css").await;
|
||||
assert_eq!(response.headers()[CONTENT_ENCODING], "gzip");
|
||||
assert_eq!(response.headers()[CONTENT_TYPE], "text/css; charset=utf-8");
|
||||
assert_eq!(response.headers()[VARY], "Accept-Encoding");
|
||||
assert_eq!(
|
||||
response.headers()[CONTENT_LENGTH],
|
||||
compressed.len().to_string()
|
||||
);
|
||||
let etag = response.headers()[ETAG].clone();
|
||||
assert_eq!(
|
||||
response.into_body().collect().await.unwrap().to_bytes(),
|
||||
compressed
|
||||
);
|
||||
|
||||
*requested.method_mut() = Method::HEAD;
|
||||
let response = serve_request(&service, &requested, "/asset.css").await;
|
||||
assert_eq!(response.headers()[CONTENT_ENCODING], "gzip");
|
||||
assert_eq!(
|
||||
response.headers()[CONTENT_LENGTH],
|
||||
compressed.len().to_string()
|
||||
);
|
||||
assert!(response
|
||||
.into_body()
|
||||
.collect()
|
||||
.await
|
||||
.unwrap()
|
||||
.to_bytes()
|
||||
.is_empty());
|
||||
|
||||
*requested.method_mut() = Method::GET;
|
||||
requested.headers_mut().insert(IF_NONE_MATCH, etag);
|
||||
let response = serve_request(&service, &requested, "/asset.css").await;
|
||||
assert_eq!(response.status(), StatusCode::NOT_MODIFIED);
|
||||
assert_eq!(response.headers()[CONTENT_ENCODING], "gzip");
|
||||
assert!(response
|
||||
.into_body()
|
||||
.collect()
|
||||
.await
|
||||
.unwrap()
|
||||
.to_bytes()
|
||||
.is_empty());
|
||||
requested.headers_mut().remove(IF_NONE_MATCH);
|
||||
|
||||
requested
|
||||
.headers_mut()
|
||||
.insert(RANGE, "bytes=65530-131089".parse().unwrap());
|
||||
let response = serve_request(&service, &requested, "/asset.css").await;
|
||||
assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
|
||||
assert_eq!(response.headers()[CONTENT_ENCODING], "gzip");
|
||||
assert_eq!(
|
||||
response.into_body().collect().await.unwrap().to_bytes(),
|
||||
compressed[65530..131090]
|
||||
);
|
||||
|
||||
requested
|
||||
.headers_mut()
|
||||
.insert(RANGE, "bytes=65520-131124,262100-458800".parse().unwrap());
|
||||
let response = serve_request(&service, &requested, "/asset.css").await;
|
||||
assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
|
||||
assert!(!response.headers().contains_key(CONTENT_ENCODING));
|
||||
let content_type = response.headers()[CONTENT_TYPE].to_str().unwrap();
|
||||
let boundary = content_type
|
||||
.strip_prefix("multipart/byteranges; boundary=")
|
||||
.unwrap();
|
||||
let mut expected = Vec::new();
|
||||
for (start, end) in [(65520, 131124), (262100, 458800)] {
|
||||
write!(&mut expected, "--{boundary}\r\nContent-Type: text/css; charset=utf-8\r\nContent-Range: bytes {start}-{end}/{}\r\n\r\n", plain.len()).unwrap();
|
||||
expected.extend_from_slice(&plain[start..=end]);
|
||||
expected.extend_from_slice(b"\r\n");
|
||||
}
|
||||
write!(&mut expected, "--{boundary}--\r\n").unwrap();
|
||||
assert_eq!(
|
||||
response.headers()[CONTENT_LENGTH],
|
||||
expected.len().to_string()
|
||||
);
|
||||
let multipart_length = expected.len();
|
||||
assert_eq!(
|
||||
response.into_body().collect().await.unwrap().to_bytes(),
|
||||
expected
|
||||
);
|
||||
|
||||
*requested.method_mut() = Method::HEAD;
|
||||
let response = serve_request(&service, &requested, "/asset.css").await;
|
||||
assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
|
||||
assert!(!response.headers().contains_key(CONTENT_ENCODING));
|
||||
assert_eq!(
|
||||
response.headers()[CONTENT_LENGTH],
|
||||
multipart_length.to_string()
|
||||
);
|
||||
assert!(response
|
||||
.into_body()
|
||||
.collect()
|
||||
.await
|
||||
.unwrap()
|
||||
.to_bytes()
|
||||
.is_empty());
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn filesystem_context_owns_root_and_preserves_independent_directory_offsets() {
|
||||
use std::io::Read;
|
||||
|
||||
let tmp = TempDir::new().unwrap();
|
||||
std::fs::create_dir(tmp.path().join("nested")).unwrap();
|
||||
std::fs::write(tmp.path().join("top.txt"), b"top").unwrap();
|
||||
std::fs::write(tmp.path().join("nested/file.txt"), b"nested").unwrap();
|
||||
let service = service(tmp.path(), true);
|
||||
let root = filesystem::FilesystemRoot::for_service(&service);
|
||||
drop(service);
|
||||
for _ in 0..3 {
|
||||
let components = filesystem::path_components("/nested/file.txt").unwrap();
|
||||
let mut opened = filesystem::secure_open_sync(&root, &components).unwrap();
|
||||
let mut contents = String::new();
|
||||
opened.file.read_to_string(&mut contents).unwrap();
|
||||
assert_eq!(contents, "nested");
|
||||
let entries = filesystem::list_directory_sync(&root, &[]).unwrap();
|
||||
assert_eq!(
|
||||
entries
|
||||
.iter()
|
||||
.map(|entry| entry.name.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
["nested", "top.txt"]
|
||||
);
|
||||
let entries = filesystem::list_directory_sync(&root, &["nested".into()]).unwrap();
|
||||
assert_eq!(entries.len(), 1);
|
||||
assert_eq!(entries[0].name, "file.txt");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[tokio::test]
|
||||
async fn filesystem_context_keeps_symlink_policy_on_intermediate_components() {
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let outside = TempDir::new().unwrap();
|
||||
std::fs::create_dir(tmp.path().join("safe")).unwrap();
|
||||
std::fs::write(tmp.path().join("safe/asset.txt"), b"safe").unwrap();
|
||||
std::fs::write(outside.path().join("asset.txt"), b"secret").unwrap();
|
||||
symlink(tmp.path().join("safe"), tmp.path().join("inside-link")).unwrap();
|
||||
symlink(outside.path(), tmp.path().join("outside-link")).unwrap();
|
||||
for policy in [SymlinkPolicy::Deny, SymlinkPolicy::InsideRoot] {
|
||||
let service = Service {
|
||||
id: "links".into(),
|
||||
name: "Links".into(),
|
||||
document_root: tmp.path().to_path_buf(),
|
||||
directory_listing: true,
|
||||
symlink_policy: policy,
|
||||
..Default::default()
|
||||
}
|
||||
.compile()
|
||||
.unwrap();
|
||||
let response = serve_request(
|
||||
&service,
|
||||
&request(Method::GET, "/inside-link/asset.txt"),
|
||||
"/inside-link/asset.txt",
|
||||
)
|
||||
.await;
|
||||
if policy == SymlinkPolicy::InsideRoot {
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
response.into_body().collect().await.unwrap().to_bytes(),
|
||||
"safe"
|
||||
);
|
||||
} else {
|
||||
assert_ne!(response.status(), StatusCode::OK);
|
||||
}
|
||||
let response = serve_request(
|
||||
&service,
|
||||
&request(Method::GET, "/outside-link/asset.txt"),
|
||||
"/outside-link/asset.txt",
|
||||
)
|
||||
.await;
|
||||
assert_ne!(response.status(), StatusCode::OK);
|
||||
assert!(!response
|
||||
.into_body()
|
||||
.collect()
|
||||
.await
|
||||
.unwrap()
|
||||
.to_bytes()
|
||||
.windows(6)
|
||||
.any(|part| part == b"secret"));
|
||||
for _ in 0..3 {
|
||||
let response = serve_request(&service, &request(Method::GET, "/"), "/").await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = response.into_body().collect().await.unwrap().to_bytes();
|
||||
let listing = std::str::from_utf8(&body).unwrap();
|
||||
assert!(listing.contains("safe/"));
|
||||
assert!(!listing.contains("outside-link"));
|
||||
assert_eq!(
|
||||
listing.contains("inside-link"),
|
||||
policy == SymlinkPolicy::InsideRoot
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn borrowed_root_descriptor_stays_anchored_after_root_path_replacement() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let configured_root = tmp.path().join("site");
|
||||
std::fs::create_dir_all(configured_root.join("nested")).unwrap();
|
||||
std::fs::write(configured_root.join("nested/asset.txt"), b"original").unwrap();
|
||||
let service = service(&configured_root, false);
|
||||
std::fs::rename(&configured_root, tmp.path().join("moved-site")).unwrap();
|
||||
std::fs::create_dir_all(configured_root.join("nested")).unwrap();
|
||||
std::fs::write(
|
||||
configured_root.join("nested/asset.txt"),
|
||||
b"replacement-secret",
|
||||
)
|
||||
.unwrap();
|
||||
for _ in 0..3 {
|
||||
let response = serve_request(
|
||||
&service,
|
||||
&request(Method::GET, "/nested/asset.txt"),
|
||||
"/nested/asset.txt",
|
||||
)
|
||||
.await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
response.into_body().collect().await.unwrap().to_bytes(),
|
||||
"original"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn open_descriptors_for(path: &Path) -> usize {
|
||||
std::fs::read_dir("/proc/self/fd")
|
||||
.unwrap()
|
||||
.filter_map(|entry| std::fs::read_link(entry.ok()?.path()).ok())
|
||||
.filter(|target| target == path)
|
||||
.count()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn wait_for_file_descriptors_to_close(path: &Path) {
|
||||
tokio::time::timeout(Duration::from_secs(5), async {
|
||||
while open_descriptors_for(path) != 0 {
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("cancelled static response retained its file descriptor");
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[tokio::test]
|
||||
async fn cancelled_static_and_multipart_bodies_release_files_and_count_only_emitted_bytes() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let path = tmp.path().join("asset.bin");
|
||||
std::fs::write(&path, vec![b'x'; 1024 * 1024]).unwrap();
|
||||
let service = service(tmp.path(), false);
|
||||
let unpolled = serve_request(&service, &request(Method::GET, "/asset.bin"), "/asset.bin").await;
|
||||
drop(unpolled);
|
||||
wait_for_file_descriptors_to_close(&path).await;
|
||||
assert_eq!(service.web_metrics.view()["response_bytes"], 0);
|
||||
|
||||
let mut accounted = 0u64;
|
||||
for range in [None, Some("bytes=0-400000,600000-1000000")] {
|
||||
let mut requested = request(Method::GET, "/asset.bin");
|
||||
if let Some(range) = range {
|
||||
requested
|
||||
.headers_mut()
|
||||
.insert(RANGE, range.parse().unwrap());
|
||||
}
|
||||
let mut body = serve_request(&service, &requested, "/asset.bin")
|
||||
.await
|
||||
.into_body();
|
||||
let first = body.frame().await.unwrap().unwrap().into_data().unwrap();
|
||||
assert!(!first.is_empty());
|
||||
assert!(first.len() <= 64 * 1024);
|
||||
accounted += first.len() as u64;
|
||||
assert_eq!(service.web_metrics.view()["response_bytes"], accounted);
|
||||
// The multipart writer may fill its bounded duplex buffer in the
|
||||
// background. Neither it nor a pending file read counts unsent bytes.
|
||||
tokio::task::yield_now().await;
|
||||
assert_eq!(service.web_metrics.view()["response_bytes"], accounted);
|
||||
drop(body);
|
||||
wait_for_file_descriptors_to_close(&path).await;
|
||||
assert_eq!(service.web_metrics.view()["response_bytes"], accounted);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user