use super::*;
#[test]
fn filenames_are_encoded_without_header_injection_and_fallback_is_bounded() {
for (name, fallback, encoded) in [
("café.txt", "caf__.txt", "caf%C3%A9.txt"),
("a\";\r\nb.txt", "a____b.txt", "a%22%3B%0D%0Ab.txt"),
("a'%*.txt", "a___.txt", "a%27%25%2A.txt"),
("a!#$&+^`|~.txt", "a_________.txt", "a!#$&+^`|~.txt"),
] {
let got = crate::http_objects::content_headers::disposition(name.as_bytes(), "inline");
assert_eq!(
got,
format!("inline; filename=\"{fallback}\"; filename*=UTF-8''{encoded}")
);
assert!(!got.contains(['\r', '\n']));
}
let got = crate::http_objects::content_headers::disposition(&[b'a'; 1024], "attachment");
assert!(got.starts_with(&format!("attachment; filename=\"{}\";", "a".repeat(255))));
assert!(got.ends_with(&"a".repeat(1024)));
let got = crate::http_objects::content_headers::disposition(b"nonutf8-\xff.txt", "inline");
assert!(got.ends_with("nonutf8-%FF.txt"));
}
#[test]
fn ref_file_headers_follow_only_the_last_extension_and_preserve_bytes() {
let fx = fixture();
let file = blob(b"<html>no sniffing</html>");
let cases = [
("png", "image/png", "inline"),
("jpg", "image/jpeg", "inline"),
("jpeg", "image/jpeg", "inline"),
("gif", "image/gif", "inline"),
("webp", "image/webp", "inline"),
("avif", "image/avif", "inline"),
("txt", "text/plain; charset=utf-8", "inline"),
("json", "application/json", "attachment"),
("pdf", "application/pdf", "inline"),
("mp4", "video/mp4", "inline"),
("webm", "video/webm", "inline"),
("mp3", "audio/mpeg", "inline"),
("ogg", "audio/ogg", "inline"),
("wav", "audio/wav", "inline"),
("heic", "image/heic", "inline"),
("md", "text/markdown; charset=utf-8", "inline"),
("csv", "text/csv; charset=utf-8", "inline"),
("svg", "application/octet-stream", "attachment"),
("html", "application/octet-stream", "attachment"),
("htm", "application/octet-stream", "attachment"),
("xhtml", "application/octet-stream", "attachment"),
("xml", "application/octet-stream", "attachment"),
("js", "application/octet-stream", "attachment"),
("mjs", "application/octet-stream", "attachment"),
("css", "application/octet-stream", "attachment"),
];
let mut names = vec![
"png".to_owned(),
"no-extension".to_owned(),
"image.png.exe".to_owned(),
];
for (ext, _, _) in cases {
names.extend([
format!("file.{ext}"),
format!("many.dots.{}", ext.to_ascii_uppercase()),
]);
}
names.sort();
let entries: Vec<_> = names
.iter()
.map(|n| (n.as_str(), EntryMode::Blob, &file))
.collect();
let root = tree(&entries);
let head = commit(&root, &[], "headers");
fx.push("room", &[&file, &root, &head], id(&head), None);
for name in names {
let expected = cases
.iter()
.find(|(ext, _, _)| name.to_ascii_lowercase().ends_with(&format!(".{ext}")));
let (media, kind) = expected.map_or(
("application/octet-stream", "attachment"),
|(_, media, kind)| (*media, *kind),
);
let got = fx.get(&fx.ref_url("room", "main", &name));
assert_eq!(got.status, 200, "{name}");
assert_eq!(got.header("Content-Type"), Some(media), "{name}");
assert_eq!(
got.header("Content-Disposition"),
Some(format!("{kind}; filename=\"{name}\"; filename*=UTF-8''{name}").as_str())
);
assert_eq!(got.body, b"<html>no sniffing</html>");
for (header, value) in [
("X-Content-Type-Options", "nosniff"),
("Content-Security-Policy", "sandbox; default-src 'none'"),
("Referrer-Policy", "no-referrer"),
] {
assert_eq!(got.header(header), Some(value));
}
}
}
#[test]
fn ordinary_ref_headers_cover_head_ranges_both_file_types_and_exclude_other_responses() {
let (fx, d) = published();
for name in ["small.txt", "chunked.bin"] {
let path = fx.ref_url("room", "main", name);
let full = fx.get(&path);
for method in ["GET", "HEAD"] {
let partial = read(fx.request(method, &path, None, &[("range", "bytes=1-2")]));
assert_eq!(partial.status, 206);
assert_eq!(partial.header("Content-Type"), full.header("Content-Type"));
assert_eq!(
partial.header("Content-Disposition"),
full.header("Content-Disposition")
);
assert_eq!(
partial.body,
if method == "HEAD" {
vec![]
} else {
full.body[1..3].to_vec()
}
);
let head = read(fx.request("HEAD", &path, None, &[]));
assert_eq!(head.status, 200);
assert_eq!(
head.header("Content-Disposition"),
full.header("Content-Disposition")
);
assert!(head.body.is_empty());
}
let etag = full.header("ETag").unwrap();
for headers in [[("if-none-match", etag)], [("range", "bytes=999999-")]] {
let got = fx.get_with(&path, &headers);
assert!(matches!(got.status, 304 | 416));
assert_eq!(got.header("Content-Disposition"), None);
assert_eq!(got.header("Content-Type"), None);
}
}
for object in [&d.small, &d.manifest, &d.root, &d.commit] {
let got = fx.get(&fx.object_url("room", &id(object)));
assert_eq!(got.status, 200);
assert_eq!(got.header("Content-Disposition"), None);
}
for path in ["", "dir"] {
let got = fx.get(&fx.ref_url("room", "main", path));
assert_eq!(
got.header("Content-Type"),
Some("application/vnd.mkit.object")
);
assert_eq!(got.header("Content-Disposition"), None);
}
let paid = with_seams(fixture(), |s| {
s.admission = Arc::new(Admit {
seen: Mutex::default(),
ended: Arc::default(),
challenge: true,
});
});
paid.push("room", &d.refs(), d.head(), None);
let got = paid.get(&paid.ref_url("room", "main", "small.txt"));
assert_eq!(got.status, 402);
assert_eq!(got.header("Content-Disposition"), None);
}
#[test]
fn mp4_ranges_preserve_inline_headers_and_exact_bytes_for_both_file_types() {
let fx = fixture();
let bytes = b"0123456789";
let file = blob(bytes);
let (chunked, chunks) = manifest(&[&bytes[..5], &bytes[5..]]);
let root = tree(&[
("chunked.MP4", EntryMode::Blob, &chunked),
("video.mp4", EntryMode::Blob, &file),
]);
let head = commit(&root, &[], "mp4 ranges");
let mut objects = vec![&file, &chunked, &root, &head];
objects.extend(chunks.iter());
fx.push("room", &objects, id(&head), None);
for name in ["video.mp4", "chunked.MP4"] {
let path = fx.ref_url("room", "main", name);
let full = fx.get(&path);
assert_eq!(full.status, 200);
assert_eq!(full.body, bytes);
let etag = full.header("ETag").unwrap();
for (range, content_range, selected) in [
("bytes=3-6", "bytes 3-6/10", &bytes[3..7]),
("bytes=7-", "bytes 7-9/10", &bytes[7..]),
("bytes=-3", "bytes 7-9/10", &bytes[7..]),
] {
for method in ["GET", "HEAD"] {
let got =
read(fx.request(method, &path, None, &[("range", range), ("if-range", etag)]));
assert_eq!(got.status, 206);
assert_eq!(got.header("Content-Type"), Some("video/mp4"));
assert_eq!(
got.header("Content-Disposition"),
full.header("Content-Disposition")
);
assert_eq!(got.header("Content-Range"), Some(content_range));
assert_eq!(
got.header("Content-Length"),
Some(selected.len().to_string().as_str())
);
assert_eq!(got.header("Accept-Ranges"), Some("bytes"));
assert_eq!(got.header("ETag"), Some(etag));
assert_eq!(got.header("X-Content-Type-Options"), Some("nosniff"));
assert_eq!(
got.header("Content-Security-Policy"),
Some("sandbox; default-src 'none'")
);
assert_eq!(got.header("Referrer-Policy"), Some("no-referrer"));
assert_eq!(
got.body.as_slice(),
if method == "HEAD" { &[] } else { selected }
);
}
}
}
}