mirror of https://github.com/fafhrd91/actix-web
fix(files): do not panic on pre-EPOCH files (#3922)
This commit is contained in:
parent
0fb2527c60
commit
f31f9bc92c
|
|
@ -57,6 +57,7 @@ dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
"filetime",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"http-range",
|
"http-range",
|
||||||
"log",
|
"log",
|
||||||
|
|
@ -1263,6 +1264,17 @@ version = "2.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "filetime"
|
||||||
|
version = "0.2.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"libredox",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.9"
|
version = "0.1.9"
|
||||||
|
|
@ -1857,6 +1869,17 @@ version = "0.2.180"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libredox"
|
||||||
|
version = "0.1.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.10.0",
|
||||||
|
"libc",
|
||||||
|
"redox_syscall 0.7.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
|
|
@ -2094,7 +2117,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"redox_syscall",
|
"redox_syscall 0.5.18",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
@ -2357,6 +2380,15 @@ dependencies = [
|
||||||
"bitflags 2.10.0",
|
"bitflags 2.10.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_syscall"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.10.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.12.3"
|
version = "1.12.3"
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,10 @@
|
||||||
|
|
||||||
- Add `Files::try_compressed()` to support serving pre-compressed static files [#2615]
|
- Add `Files::try_compressed()` to support serving pre-compressed static files [#2615]
|
||||||
- Fix handling of `bytes=0-`
|
- Fix handling of `bytes=0-`
|
||||||
|
- Fix `NamedFile` panic when serving files with pre-UNIX epoch modification times. [#2748]
|
||||||
|
|
||||||
[#2615]: https://github.com/actix/actix-web/pull/2615
|
[#2615]: https://github.com/actix/actix-web/pull/2615
|
||||||
|
[#2748]: https://github.com/actix/actix-web/issues/2748
|
||||||
|
|
||||||
## 0.6.10
|
## 0.6.10
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ actix-rt = "2.7"
|
||||||
actix-test = "0.1"
|
actix-test = "0.1"
|
||||||
actix-web = "4"
|
actix-web = "4"
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
|
filetime = "0.2"
|
||||||
tempfile = "3.2"
|
tempfile = "3.2"
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
|
|
|
||||||
|
|
@ -405,7 +405,9 @@ impl NamedFile {
|
||||||
|
|
||||||
/// Creates an `ETag` in a format is similar to Apache's.
|
/// Creates an `ETag` in a format is similar to Apache's.
|
||||||
pub(crate) fn etag(&self) -> Option<header::EntityTag> {
|
pub(crate) fn etag(&self) -> Option<header::EntityTag> {
|
||||||
self.modified.as_ref().map(|mtime| {
|
let mtime = self.modified?;
|
||||||
|
|
||||||
|
Some({
|
||||||
let ino = {
|
let ino = {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
|
|
@ -421,22 +423,50 @@ impl NamedFile {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let dur = mtime
|
// Don't panic for pre-epoch modification times. Encode the timestamp as seconds and
|
||||||
.duration_since(UNIX_EPOCH)
|
// sub-second nanoseconds relative to the UNIX epoch, allowing negative values.
|
||||||
.expect("modification time must be after epoch");
|
let (secs, nanos) = match mtime.duration_since(UNIX_EPOCH) {
|
||||||
|
Ok(dur) => (dur.as_secs() as i64, dur.subsec_nanos()),
|
||||||
|
Err(err) => {
|
||||||
|
let dur = err.duration();
|
||||||
|
|
||||||
|
// For timestamps before the epoch, represent the time as a negative seconds
|
||||||
|
// offset with positive nanoseconds (like POSIX timespec).
|
||||||
|
if dur.subsec_nanos() == 0 {
|
||||||
|
(-(dur.as_secs() as i64), 0)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
-(dur.as_secs() as i64) - 1,
|
||||||
|
1_000_000_000 - dur.subsec_nanos(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
header::EntityTag::new_strong(format!(
|
header::EntityTag::new_strong(format!(
|
||||||
"{:x}:{:x}:{:x}:{:x}",
|
"{:x}:{:x}:{:x}:{:x}",
|
||||||
ino,
|
ino,
|
||||||
self.md.len(),
|
self.md.len(),
|
||||||
dur.as_secs(),
|
secs as u64,
|
||||||
dur.subsec_nanos()
|
nanos
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn last_modified(&self) -> Option<header::HttpDate> {
|
pub(crate) fn last_modified(&self) -> Option<header::HttpDate> {
|
||||||
self.modified.map(|mtime| mtime.into())
|
let mtime = self.modified?;
|
||||||
|
|
||||||
|
// avoid panic in `httpdate` crate when formatting as an HTTP date
|
||||||
|
// see: https://github.com/actix/actix-web/issues/2748
|
||||||
|
//
|
||||||
|
// httpdate supports dates in range [1970, 9999); see:
|
||||||
|
// https://github.com/seanmonstar/httpdate/blob/v1.0.3/src/date.rs
|
||||||
|
let dur = mtime.duration_since(UNIX_EPOCH).ok()?;
|
||||||
|
if dur.as_secs() >= 253_402_300_800 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(mtime.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates an `HttpResponse` with file as a streaming body.
|
/// Creates an `HttpResponse` with file as a streaming body.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
use std::time::UNIX_EPOCH;
|
||||||
|
|
||||||
|
use actix_files::NamedFile;
|
||||||
|
use actix_web::{
|
||||||
|
http::{header, StatusCode},
|
||||||
|
test, web, App,
|
||||||
|
};
|
||||||
|
use filetime::{set_file_mtime, FileTime};
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
#[actix_web::test]
|
||||||
|
async fn serves_file_with_pre_epoch_mtime() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let path = dir.path().join("pre_epoch.txt");
|
||||||
|
|
||||||
|
std::fs::write(&path, b"hello").unwrap();
|
||||||
|
|
||||||
|
// set mtime to before UNIX epoch; this used to panic during ETag/Last-Modified generation
|
||||||
|
set_file_mtime(&path, FileTime::from_unix_time(-60, 0)).unwrap();
|
||||||
|
|
||||||
|
let mtime = std::fs::metadata(&path).unwrap().modified().unwrap();
|
||||||
|
assert!(
|
||||||
|
mtime < UNIX_EPOCH,
|
||||||
|
"fixture mtime should be before UNIX_EPOCH"
|
||||||
|
);
|
||||||
|
|
||||||
|
let srv = {
|
||||||
|
let path = path.clone();
|
||||||
|
test::init_service(App::new().default_service(web::to(move || {
|
||||||
|
let path = path.clone();
|
||||||
|
async move { NamedFile::open_async(path).await.unwrap() }
|
||||||
|
})))
|
||||||
|
.await
|
||||||
|
};
|
||||||
|
|
||||||
|
let req = test::TestRequest::with_uri("/").to_request();
|
||||||
|
let res = test::call_service(&srv, req).await;
|
||||||
|
|
||||||
|
assert_eq!(res.status(), StatusCode::OK);
|
||||||
|
|
||||||
|
// ETag is still generated even for pre-epoch times.
|
||||||
|
assert!(res.headers().contains_key(header::ETAG));
|
||||||
|
|
||||||
|
// HTTP-date formatting in the httpdate crate does not support pre-epoch times.
|
||||||
|
assert!(!res.headers().contains_key(header::LAST_MODIFIED));
|
||||||
|
|
||||||
|
let body = test::read_body(res).await;
|
||||||
|
assert_eq!(&body[..], b"hello");
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue