diff options
author | metamuffin <metamuffin@disroot.org> | 2023-02-26 16:18:45 +0100 |
---|---|---|
committer | metamuffin <metamuffin@disroot.org> | 2023-02-26 16:18:45 +0100 |
commit | a3fb25019336ab9238d73f29a004b71cfc31a032 (patch) | |
tree | 783b91075f61f4b89eef35f08064ca15b0ddb2f9 | |
parent | c3371bd7e3eb40fad374fe85a994806c2d27488e (diff) | |
download | gnix-a3fb25019336ab9238d73f29a004b71cfc31a032.tar gnix-a3fb25019336ab9238d73f29a004b71cfc31a032.tar.bz2 gnix-a3fb25019336ab9238d73f29a004b71cfc31a032.tar.zst |
support range requests
-rw-r--r-- | Cargo.lock | 106 | ||||
-rw-r--r-- | Cargo.toml | 2 | ||||
-rw-r--r-- | src/error.rs | 9 | ||||
-rw-r--r-- | src/files.rs | 100 | ||||
-rw-r--r-- | src/main.rs | 7 |
5 files changed, 197 insertions, 27 deletions
@@ -25,6 +25,12 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" @@ -36,6 +42,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] +name = "block-buffer" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +dependencies = [ + "generic-array", +] + +[[package]] name = "bumpalo" version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -60,6 +75,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] +name = "cpufeatures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] name = "env_logger" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -152,6 +196,16 @@ dependencies = [ ] [[package]] +name = "generic-array" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] name = "gnix" version = "0.1.0" dependencies = [ @@ -159,12 +213,14 @@ dependencies = [ "bytes", "env_logger", "futures-util", + "headers", "http-body-util", "humansize", "hyper", "log", "markup", "mime_guess", + "percent-encoding", "rustls", "rustls-pemfile", "serde", @@ -201,6 +257,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] +name = "headers" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" +dependencies = [ + "base64 0.13.1", + "bitflags", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http", +] + +[[package]] name = "hermit-abi" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -492,6 +573,12 @@ dependencies = [ ] [[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] name = "pin-project-lite" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -594,7 +681,7 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" dependencies = [ - "base64", + "base64 0.21.0", ] [[package]] @@ -643,6 +730,17 @@ dependencies = [ ] [[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] name = "signal-hook-registry" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -836,6 +934,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] name = "unicase" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -8,6 +8,8 @@ edition = "2021" # HTTP hyper = { version = "1.0.0-rc.2", features = ["full"] } http-body-util = "0.1.0-rc.2" +headers = "0.3.8" +percent-encoding = "2.2.0" # TLS rustls-pemfile = "1.0.2" diff --git a/src/error.rs b/src/error.rs index cbeb6a6..9a83aff 100644 --- a/src/error.rs +++ b/src/error.rs @@ -10,6 +10,10 @@ pub enum ServiceError { NotFound, #[error("io error: {0}")] Io(std::io::Error), + #[error("bad range")] + BadRange, + #[error("bad utf8")] + BadUtf8, #[error("ohh. i didn't expect that this error can be generated.")] Other, } @@ -19,3 +23,8 @@ impl From<std::io::Error> for ServiceError { Self::Io(e) } } +impl From<std::str::Utf8Error> for ServiceError { + fn from(_: std::str::Utf8Error) -> Self { + Self::BadUtf8 + } +} diff --git a/src/files.rs b/src/files.rs index 7ae34ea..555e8ae 100644 --- a/src/files.rs +++ b/src/files.rs @@ -1,6 +1,7 @@ use crate::{config::FileserverConfig, ServiceError}; use bytes::{Bytes, BytesMut}; use futures_util::{future, future::Either, ready, stream, FutureExt, Stream, StreamExt}; +use headers::{AcceptRanges, ContentLength, ContentRange, ContentType, HeaderMapExt}; use http_body_util::{combinators::BoxBody, BodyExt, StreamBody}; use humansize::FormatSizeOptions; use hyper::{ @@ -11,7 +12,8 @@ use hyper::{ }; use log::debug; use markup::Render; -use std::{fs::Metadata, io, path::Path, pin::Pin, task::Poll}; +use percent_encoding::percent_decode_str; +use std::{fs::Metadata, io, ops::Range, path::Path, pin::Pin, task::Poll}; use tokio::{ fs::{read_to_string, File}, io::AsyncSeekExt, @@ -26,17 +28,19 @@ pub async fn serve_files( let mut path = config.root.clone(); for seg in rpath.split("/") { + let seg = percent_decode_str(seg).decode_utf8()?; if seg == "" || seg == ".." { continue; // not ideal } - path.push(seg) + path.push(seg.as_ref()) } - if !path.exists() { return Err(ServiceError::NotFound); } - if path.is_dir() { + let metadata = path.metadata()?; + + if metadata.file_type().is_dir() { if !config.index { return Err(ServiceError::NotFound); } @@ -59,24 +63,30 @@ pub async fn serve_files( }); } + let range = req.headers().typed_get::<headers::Range>(); + let range = bytes_range(range, metadata.len())?; + let file = File::open(path.clone()).await?; let mut r = Response::new(BoxBody::new(StreamBody::new( - StreamBody::new(file_stream(file, 4096, (0, u64::MAX))) + StreamBody::new(file_stream(file, 4096, range.clone())) .map(|e| e.map(|e| Frame::data(e)).map_err(ServiceError::Io)), ))); - r.headers_mut().insert( - CONTENT_TYPE, - HeaderValue::from_str( - // no allocation possible here? - &mime_guess::from_path(path) - .first() - .map(|m| m.to_string()) - .unwrap_or("text/plain".to_string()), - ) - .unwrap(), - ); + if range.end - range.start != metadata.len() { + *r.status_mut() = StatusCode::PARTIAL_CONTENT; + r.headers_mut().typed_insert( + ContentRange::bytes(range.clone(), metadata.len()).expect("valid ContentRange"), + ); + } + + let mime = mime_guess::from_path(path).first_or_octet_stream(); + + r.headers_mut() + .typed_insert(ContentLength(range.end - range.start)); + r.headers_mut().typed_insert(ContentType::from(mime)); + r.headers_mut().typed_insert(AcceptRanges::bytes()); + Ok(r) } @@ -84,13 +94,13 @@ pub async fn serve_files( fn file_stream( mut file: File, buf_size: usize, - (start, end): (u64, u64), + range: Range<u64>, ) -> impl Stream<Item = Result<Bytes, io::Error>> + Send { use std::io::SeekFrom; let seek = async move { - if start != 0 { - file.seek(SeekFrom::Start(start)).await?; + if range.start != 0 { + file.seek(SeekFrom::Start(range.start)).await?; } Ok(file) }; @@ -98,7 +108,7 @@ fn file_stream( seek.into_stream() .map(move |result| { let mut buf = BytesMut::new(); - let mut len = end - start; + let mut len = range.end - range.start; let mut f = match result { Ok(f) => f, Err(f) => return Either::Left(stream::once(future::err(f))), @@ -137,6 +147,49 @@ fn file_stream( .flatten() } +// Also adapted from warp +fn bytes_range(range: Option<headers::Range>, max_len: u64) -> Result<Range<u64>, ServiceError> { + use std::ops::Bound; + + let range = if let Some(range) = range { + range + } else { + return Ok(0..max_len); + }; + + let ret = range + .iter() + .map(|(start, end)| { + let start = match start { + Bound::Unbounded => 0, + Bound::Included(s) => s, + Bound::Excluded(s) => s + 1, + }; + + let end = match end { + Bound::Unbounded => max_len, + Bound::Included(s) => { + // For the special case where s == the file size + if s == max_len { + s + } else { + s + 1 + } + } + Bound::Excluded(s) => s, + }; + + if start < end && end <= max_len { + Ok(start..end) + } else { + Err(ServiceError::BadRange) + } + }) + .next() + .unwrap_or(Ok(0..max_len)); + ret +} + fn reserve_at_least(buf: &mut BytesMut, cap: usize) { if buf.capacity() - buf.len() < cap { buf.reserve(cap); @@ -169,6 +222,7 @@ markup::define! { @markup::doctype() html { head { + meta[charset="UTF-8"]; title { "Index of " @path } } body { @@ -179,15 +233,15 @@ markup::define! { } hr; table { - @if path != "/" { + @if path != "/" { tr { td { b { a[href=".."] { "../" } } } } } @for (name, meta) in files { tr { - td { a[href=name] { + td { a[href=name] { @name @if meta.file_type().is_dir() { "/" } } } - td { + td { @if meta.file_type().is_dir() { i { "directory" } } else { diff --git a/src/main.rs b/src/main.rs index 422341d..e39cf58 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ #![feature(try_trait_v2)] +#![feature(exclusive_range_pattern)] pub mod config; pub mod error; @@ -21,7 +22,7 @@ use hyper::{ service::service_fn, Request, Response, StatusCode, }; -use log::{debug, info, warn}; +use log::{debug, error, info, warn}; use std::{fs::File, io::BufReader, net::SocketAddr, path::Path, sync::Arc}; use tokio::{ io::{AsyncRead, AsyncWrite}, @@ -42,12 +43,12 @@ async fn main() -> anyhow::Result<()> { tokio::spawn(async move { if let Err(e) = serve_http(config).await { - panic!("{e}") + error!("{e}") } }); tokio::spawn(async move { if let Err(e) = serve_https(config2).await { - panic!("{e}") + error!("{e}") } }); |