summaryrefslogtreecommitdiff
path: root/src
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 /src
parentc3371bd7e3eb40fad374fe85a994806c2d27488e (diff)
downloadgnix-a3fb25019336ab9238d73f29a004b71cfc31a032.tar
gnix-a3fb25019336ab9238d73f29a004b71cfc31a032.tar.bz2
gnix-a3fb25019336ab9238d73f29a004b71cfc31a032.tar.zst
support range requests
Diffstat (limited to 'src')
-rw-r--r--src/error.rs9
-rw-r--r--src/files.rs100
-rw-r--r--src/main.rs7
3 files changed, 90 insertions, 26 deletions
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}")
}
});