summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2023-02-26 16:18:45 +0100
committermetamuffin <metamuffin@disroot.org>2023-02-26 16:18:45 +0100
commita3fb25019336ab9238d73f29a004b71cfc31a032 (patch)
tree783b91075f61f4b89eef35f08064ca15b0ddb2f9
parentc3371bd7e3eb40fad374fe85a994806c2d27488e (diff)
downloadgnix-a3fb25019336ab9238d73f29a004b71cfc31a032.tar
gnix-a3fb25019336ab9238d73f29a004b71cfc31a032.tar.bz2
gnix-a3fb25019336ab9238d73f29a004b71cfc31a032.tar.zst
support range requests
-rw-r--r--Cargo.lock106
-rw-r--r--Cargo.toml2
-rw-r--r--src/error.rs9
-rw-r--r--src/files.rs100
-rw-r--r--src/main.rs7
5 files changed, 197 insertions, 27 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 97815d2..ee78064 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/Cargo.toml b/Cargo.toml
index c900bd7..022024f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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}")
}
});