aboutsummaryrefslogtreecommitdiff
path: root/src/filters
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2024-05-30 00:09:11 +0200
committermetamuffin <metamuffin@disroot.org>2024-05-30 00:09:11 +0200
commit532cc431d1c5ca1ffcf429a4ccb94edc7848fe7a (patch)
treec4422c4d54e01f63bae391cd95788cad74f59fbb /src/filters
parent8b39940a58c28bc1bbe291eb5229e9ce1444e33c (diff)
downloadgnix-532cc431d1c5ca1ffcf429a4ccb94edc7848fe7a.tar
gnix-532cc431d1c5ca1ffcf429a4ccb94edc7848fe7a.tar.bz2
gnix-532cc431d1c5ca1ffcf429a4ccb94edc7848fe7a.tar.zst
rename filters dir
Diffstat (limited to 'src/filters')
-rw-r--r--src/filters/accesslog.rs81
-rw-r--r--src/filters/auth/basic.rs68
-rw-r--r--src/filters/auth/cookie.rs182
-rw-r--r--src/filters/auth/login.html35
-rw-r--r--src/filters/auth/mod.rs90
-rw-r--r--src/filters/error.rs32
-rw-r--r--src/filters/file.rs62
-rw-r--r--src/filters/files.rs390
-rw-r--r--src/filters/hosts.rs45
-rw-r--r--src/filters/mod.rs54
-rw-r--r--src/filters/proxy.rs98
11 files changed, 0 insertions, 1137 deletions
diff --git a/src/filters/accesslog.rs b/src/filters/accesslog.rs
deleted file mode 100644
index 1da6e5d..0000000
--- a/src/filters/accesslog.rs
+++ /dev/null
@@ -1,81 +0,0 @@
-use super::{Node, NodeContext, NodeKind, NodeRequest, NodeResponse};
-use crate::{config::DynNode, error::ServiceError};
-use futures::Future;
-use log::error;
-use serde::Deserialize;
-use std::{path::PathBuf, pin::Pin, sync::Arc, time::SystemTime};
-use tokio::{
- fs::{File, OpenOptions},
- io::{AsyncWriteExt, BufWriter},
- sync::RwLock,
-};
-
-pub struct AccessLogKind;
-
-#[derive(Deserialize)]
-struct AccessLogConfig {
- file: PathBuf,
- #[serde(default)]
- flush: bool,
- #[serde(default)]
- reject_on_fail: bool,
- next: DynNode,
-}
-
-struct AccessLog {
- config: AccessLogConfig,
- file: RwLock<Option<BufWriter<File>>>,
-}
-
-impl NodeKind for AccessLogKind {
- fn name(&self) -> &'static str {
- "access_log"
- }
- fn instanciate(&self, config: serde_yaml::Value) -> anyhow::Result<Arc<dyn Node>> {
- Ok(Arc::new(AccessLog {
- config: serde_yaml::from_value::<AccessLogConfig>(config)?,
- file: Default::default(),
- }))
- }
-}
-
-impl Node for AccessLog {
- fn handle<'a>(
- &'a self,
- context: &'a mut NodeContext,
- request: NodeRequest,
- ) -> Pin<Box<dyn Future<Output = Result<NodeResponse, ServiceError>> + Send + Sync + 'a>> {
- Box::pin(async move {
- let mut g = self.file.write().await;
- let log = match g.as_mut() {
- Some(r) => r,
- None => g.insert(BufWriter::new(
- OpenOptions::new()
- .append(true)
- .create(true)
- .open(&self.config.file)
- .await?,
- )),
- };
-
- let method = request.method().as_str();
- let time = SystemTime::UNIX_EPOCH.elapsed().unwrap().as_micros();
- let addr = context.addr;
- let mut res = log
- .write_all(format!("{time}\t{addr}\t{method}\t{:?}\n", request.uri()).as_bytes())
- .await;
-
- if self.config.flush && res.is_ok() {
- res = log.flush().await;
- }
-
- if self.config.reject_on_fail {
- res?
- } else if let Err(e) = res {
- error!("failed to write log: {e:?}")
- }
-
- self.config.next.handle(context, request).await
- })
- }
-}
diff --git a/src/filters/auth/basic.rs b/src/filters/auth/basic.rs
deleted file mode 100644
index a7a74c8..0000000
--- a/src/filters/auth/basic.rs
+++ /dev/null
@@ -1,68 +0,0 @@
-use crate::{
- config::DynNode,
- error::ServiceError,
- filters::{Node, NodeContext, NodeKind, NodeRequest, NodeResponse},
-};
-use base64::Engine;
-use futures::Future;
-use http_body_util::{combinators::BoxBody, BodyExt};
-use hyper::{
- header::{HeaderValue, AUTHORIZATION, WWW_AUTHENTICATE},
- Response, StatusCode,
-};
-use log::debug;
-use serde::Deserialize;
-use serde_yaml::Value;
-use std::{collections::HashSet, pin::Pin, sync::Arc};
-
-pub struct HttpBasicAuthKind;
-impl NodeKind for HttpBasicAuthKind {
- fn name(&self) -> &'static str {
- "http_basic_auth"
- }
- fn instanciate(&self, config: Value) -> anyhow::Result<Arc<dyn Node>> {
- Ok(Arc::new(serde_yaml::from_value::<HttpBasicAuth>(config)?))
- }
-}
-
-#[derive(Deserialize)]
-pub struct HttpBasicAuth {
- realm: String,
- valid: HashSet<String>,
- next: DynNode,
-}
-
-impl Node for HttpBasicAuth {
- fn handle<'a>(
- &'a self,
- context: &'a mut NodeContext,
- request: NodeRequest,
- ) -> Pin<Box<dyn Future<Output = Result<NodeResponse, ServiceError>> + Send + Sync + 'a>> {
- Box::pin(async move {
- if let Some(auth) = request.headers().get(AUTHORIZATION) {
- let k = auth
- .as_bytes()
- .strip_prefix(b"Basic ")
- .ok_or(ServiceError::BadAuth)?;
- let k = base64::engine::general_purpose::STANDARD.decode(k)?;
- let k = String::from_utf8(k)?;
- if self.valid.contains(&k) {
- debug!("valid auth");
- return self.next.handle(context, request).await;
- } else {
- debug!("invalid auth");
- }
- }
- debug!("unauthorized; sending auth challenge");
- let mut r = Response::new(BoxBody::<_, ServiceError>::new(
- String::new().map_err(|_| unreachable!()),
- ));
- *r.status_mut() = StatusCode::UNAUTHORIZED;
- r.headers_mut().insert(
- WWW_AUTHENTICATE,
- HeaderValue::from_str(&format!("Basic realm=\"{}\"", self.realm)).unwrap(),
- );
- Ok(r)
- })
- }
-}
diff --git a/src/filters/auth/cookie.rs b/src/filters/auth/cookie.rs
deleted file mode 100644
index 620911d..0000000
--- a/src/filters/auth/cookie.rs
+++ /dev/null
@@ -1,182 +0,0 @@
-use crate::{
- config::{return_true, DynNode},
- error::ServiceError,
- filters::{Node, NodeContext, NodeKind, NodeRequest, NodeResponse},
-};
-use aes_gcm_siv::{
- aead::{Aead, Payload},
- Nonce,
-};
-use base64::Engine;
-use futures::Future;
-use headers::{Cookie, HeaderMapExt};
-use http_body_util::{combinators::BoxBody, BodyExt};
-use hyper::{
- header::{HeaderValue, LOCATION, REFERER, SET_COOKIE},
- Method, Response, StatusCode,
-};
-use log::debug;
-use percent_encoding::{percent_decode_str, percent_encode, NON_ALPHANUMERIC};
-use rand::random;
-use serde::Deserialize;
-use serde_yaml::Value;
-use std::fmt::Write;
-use std::{pin::Pin, sync::Arc, time::SystemTime};
-
-use super::Credentials;
-
-pub struct CookieAuthKind;
-impl NodeKind for CookieAuthKind {
- fn name(&self) -> &'static str {
- "cookie_auth"
- }
- fn instanciate(&self, config: Value) -> anyhow::Result<Arc<dyn Node>> {
- Ok(Arc::new(serde_yaml::from_value::<CookieAuth>(config)?))
- }
-}
-
-#[derive(Deserialize)]
-pub struct CookieAuth {
- users: Credentials,
- expire: Option<u64>,
- #[serde(default = "return_true")]
- secure: bool,
- next: DynNode,
- fail: DynNode,
-}
-
-impl Node for CookieAuth {
- fn handle<'a>(
- &'a self,
- context: &'a mut NodeContext,
- request: NodeRequest,
- ) -> Pin<Box<dyn Future<Output = Result<NodeResponse, ServiceError>> + Send + Sync + 'a>> {
- Box::pin(async move {
- if request.method() == Method::POST && request.uri().path() == "/_gnix_login" {
- let referrer = request.headers().get(REFERER).cloned();
- let d = request
- .into_body()
- .collect()
- .await
- .map_err(|_| todo!())
- .unwrap();
- let d = String::from_utf8(d.to_bytes().to_vec()).unwrap();
-
- // TODO proper parser
- let mut username = "user";
- let mut password = "";
- for kv in d.split("&") {
- let (key, value) = kv.split_once("=").ok_or(ServiceError::BadAuth)?;
- match key {
- "username" => username = value,
- "password" => password = value,
- _ => (),
- }
- }
- let mut r = Response::new(BoxBody::<_, ServiceError>::new(
- String::new().clone().map_err(|_| unreachable!()),
- ));
- *r.status_mut() = StatusCode::FOUND;
- debug!("login attempt for {username:?}");
- if self.users.authentificate(username, password) {
- debug!("login success");
- let nonce = [(); 12].map(|_| random::<u8>());
- let plaintext = unix_seconds().to_le_bytes();
- let mut ciphertext = context
- .state
- .crypto_key
- .encrypt(
- Nonce::from_slice(&nonce),
- Payload {
- msg: &plaintext,
- aad: username.as_bytes(),
- },
- )
- .unwrap();
-
- ciphertext.extend(nonce);
- let auth = base64::engine::general_purpose::URL_SAFE.encode(ciphertext);
-
- let mut cookie_opts = String::new();
- if let Some(e) = self.expire {
- write!(cookie_opts, "; Expire={e}").unwrap();
- }
- if self.secure {
- write!(cookie_opts, "; Secure").unwrap();
- }
-
- r.headers_mut().append(
- SET_COOKIE,
- HeaderValue::from_str(&format!(
- "gnix_username={}{}",
- percent_encode(username.as_bytes(), NON_ALPHANUMERIC),
- cookie_opts
- ))
- .unwrap(),
- );
- r.headers_mut().append(
- SET_COOKIE,
- HeaderValue::from_str(&format!("gnix_auth={}{}", auth, cookie_opts))
- .unwrap(),
- );
- } else {
- debug!("login fail");
- }
- r.headers_mut()
- .append(LOCATION, referrer.unwrap_or(HeaderValue::from_static("/")));
-
- Ok(r)
- } else {
- if let Some(cookie) = request.headers().typed_get::<Cookie>() {
- if let Some(auth) = cookie.get("gnix_auth") {
- let username =
- percent_decode_str(cookie.get("gnix_username").unwrap_or("user"))
- .decode_utf8()?;
-
- let auth = base64::engine::general_purpose::URL_SAFE.decode(auth)?;
- if auth.len() < 12 {
- return Err(ServiceError::BadAuth);
- }
- let (msg, nonce) = auth.split_at(auth.len() - 12);
- let plaintext = context.state.crypto_key.decrypt(
- Nonce::from_slice(nonce),
- Payload {
- msg,
- aad: username.as_bytes(),
- },
- );
- if let Ok(plaintext) = plaintext {
- let created = u64::from_le_bytes(plaintext[0..8].try_into().unwrap());
-
- if self
- .expire
- .map(|e| created + e > unix_seconds())
- .unwrap_or(true)
- {
- debug!("valid auth for {username:?}");
- return self.next.handle(context, request).await;
- } else {
- debug!("auth expired");
- }
- } else {
- debug!("aead invalid");
- }
- } else {
- debug!("no auth cookie");
- }
- }
- debug!("unauthorized");
- let mut r = self.fail.handle(context, request).await?;
- *r.status_mut() = StatusCode::UNAUTHORIZED;
- Ok(r)
- }
- })
- }
-}
-
-fn unix_seconds() -> u64 {
- SystemTime::now()
- .duration_since(SystemTime::UNIX_EPOCH)
- .unwrap()
- .as_secs()
-}
diff --git a/src/filters/auth/login.html b/src/filters/auth/login.html
deleted file mode 100644
index c7782bd..0000000
--- a/src/filters/auth/login.html
+++ /dev/null
@@ -1,35 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
- <head>
- <meta charset="UTF-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
- <title>Gnix Login</title>
- <style>
- body {
- background-color: grey;
- }
- form {
- margin: auto;
- width: 200px;
- padding: 20px;
- border: 2px solid black;
- background-color: white;
- }
- input[type="text"],
- input[type="password"] {
- box-sizing: border-box;
- width: 100%;
- margin-bottom: 1em;
- }
- </style>
- </head>
- <body>
- <form action="/_gnix_login" method="post">
- <label for="username">Username: </label><br />
- <input type="text" name="username" id="username" /><br />
- <label for="password">Password: </label><br />
- <input type="password" name="password" id="password" /><br />
- <input type="submit" value="Login" />
- </form>
- </body>
-</html>
diff --git a/src/filters/auth/mod.rs b/src/filters/auth/mod.rs
deleted file mode 100644
index d6e1a35..0000000
--- a/src/filters/auth/mod.rs
+++ /dev/null
@@ -1,90 +0,0 @@
-use argon2::PasswordVerifier;
-use argon2::{
- password_hash::{Encoding, PasswordHashString},
- Algorithm, Argon2, Params, PasswordHash, Version,
-};
-use serde::de::MapAccess;
-use serde::{
- de::{value, Error, Visitor},
- Deserialize,
-};
-use std::{collections::HashMap, fmt, fs::read_to_string};
-
-pub mod basic;
-pub mod cookie;
-
-struct Credentials {
- wrong_user: PasswordHashString,
- hashes: HashMap<String, PasswordHashString>,
-}
-
-impl Credentials {
- fn get(&self, usernamme: &str) -> &PasswordHashString {
- self.hashes.get(usernamme).unwrap_or(&self.wrong_user)
- }
- pub fn authentificate(&self, usernamme: &str, password: &str) -> bool {
- let algo = Argon2::new(Algorithm::Argon2id, Version::V0x13, Params::default());
- let hash = self.get(usernamme);
- match hash.algorithm().as_str() {
- "argon2id" => algo
- .verify_password(
- password.as_bytes(),
- &PasswordHash::parse(hash.as_str(), hash.encoding()).unwrap(),
- )
- .is_ok(),
- "never" => false,
- _ => false,
- }
- }
-}
-
-impl<'de> Deserialize<'de> for Credentials {
- fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
- where
- D: serde::Deserializer<'de>,
- {
- struct StringOrMap;
- impl<'de> Visitor<'de> for StringOrMap {
- type Value = HashMap<String, String>;
- fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
- formatter.write_str("credentials map or file path")
- }
- fn visit_str<E>(self, val: &str) -> Result<Self::Value, E>
- where
- E: Error,
- {
- let path = String::deserialize(value::StrDeserializer::new(val))?;
- let c = serde_yaml::from_str(&read_to_string(path).map_err(|io| {
- serde::de::Error::custom(format!("cannot read creds file: {io:?}"))
- })?)
- .map_err(|e| serde::de::Error::custom(format!("cannot parse creds file: {e:?}")))?;
- Ok(c)
- }
- fn visit_map<A>(self, val: A) -> Result<Self::Value, A::Error>
- where
- A: MapAccess<'de>,
- {
- Ok(HashMap::deserialize(value::MapAccessDeserializer::new(
- val,
- ))?)
- }
- }
- let k = deserializer.deserialize_any(StringOrMap)?;
- Ok(Credentials {
- wrong_user: PasswordHashString::parse("$never", Encoding::B64).unwrap(),
- hashes: k
- .into_iter()
- .map(|(k, v)| {
- let hash = PasswordHash::parse(&v, Encoding::B64)
- .map_err(|e| {
- serde::de::Error::custom(format!(
- "phc string for user {k:?} is invalid: {e:?}"
- ))
- })?
- .serialize();
- Ok((k, hash))
- })
- .try_collect()?,
- })
- }
-}
diff --git a/src/filters/error.rs b/src/filters/error.rs
deleted file mode 100644
index 504802f..0000000
--- a/src/filters/error.rs
+++ /dev/null
@@ -1,32 +0,0 @@
-use crate::error::ServiceError;
-
-use super::{Node, NodeContext, NodeKind, NodeRequest, NodeResponse};
-use futures::Future;
-use serde::Deserialize;
-use serde_yaml::Value;
-use std::{pin::Pin, sync::Arc};
-
-pub struct ErrorKind;
-
-#[derive(Deserialize)]
-#[serde(transparent)]
-struct Error(String);
-
-impl NodeKind for ErrorKind {
- fn name(&self) -> &'static str {
- "error"
- }
- fn instanciate(&self, config: Value) -> anyhow::Result<Arc<dyn Node>> {
- Ok(Arc::new(serde_yaml::from_value::<Error>(config)?))
- }
-}
-
-impl Node for Error {
- fn handle<'a>(
- &'a self,
- _context: &'a mut NodeContext,
- _request: NodeRequest,
- ) -> Pin<Box<dyn Future<Output = Result<NodeResponse, ServiceError>> + Send + Sync + 'a>> {
- Box::pin(async move { Err(ServiceError::Custom(self.0.clone())) })
- }
-}
diff --git a/src/filters/file.rs b/src/filters/file.rs
deleted file mode 100644
index 53c27f4..0000000
--- a/src/filters/file.rs
+++ /dev/null
@@ -1,62 +0,0 @@
-use super::{Node, NodeContext, NodeKind, NodeRequest, NodeResponse};
-use crate::error::ServiceError;
-use futures::Future;
-use http_body_util::{combinators::BoxBody, BodyExt};
-use hyper::{
- header::{HeaderValue, CONTENT_TYPE},
- Response,
-};
-use serde::Deserialize;
-use serde_yaml::Value;
-use std::{fs::read_to_string, path::PathBuf, pin::Pin, sync::Arc};
-
-pub struct FileKind;
-
-#[derive(Debug, Deserialize)]
-struct FileConfig {
- path: Option<PathBuf>,
- content: Option<String>,
- r#type: Option<String>,
-}
-
-#[derive(Debug, Deserialize)]
-struct File {
- content: String,
- r#type: String,
-}
-
-impl NodeKind for FileKind {
- fn name(&self) -> &'static str {
- "file"
- }
- fn instanciate(&self, config: Value) -> anyhow::Result<Arc<dyn Node>> {
- let conf = serde_yaml::from_value::<FileConfig>(config)?;
- Ok(Arc::new(File {
- content: conf
- .content
- .or(conf
- .path
- .map(|p| Ok::<_, ServiceError>(read_to_string(p)?))
- .transpose()?)
- .unwrap_or_default(),
- r#type: conf.r#type.unwrap_or("text/html".to_string()), // TODO infer mime from ext
- }))
- }
-}
-
-impl Node for File {
- fn handle<'a>(
- &'a self,
- _context: &'a mut NodeContext,
- _request: NodeRequest,
- ) -> Pin<Box<dyn Future<Output = Result<NodeResponse, ServiceError>> + Send + Sync + 'a>> {
- Box::pin(async move {
- let mut r = Response::new(BoxBody::<_, ServiceError>::new(
- self.content.clone().map_err(|_| unreachable!()),
- ));
- r.headers_mut()
- .insert(CONTENT_TYPE, HeaderValue::from_str(&self.r#type).unwrap());
- Ok(r)
- })
- }
-}
diff --git a/src/filters/files.rs b/src/filters/files.rs
deleted file mode 100644
index 4fdd5cd..0000000
--- a/src/filters/files.rs
+++ /dev/null
@@ -1,390 +0,0 @@
-use super::{Node, NodeContext, NodeKind, NodeRequest, NodeResponse};
-use crate::{config::return_true, ServiceError};
-use bytes::{Bytes, BytesMut};
-use futures::Future;
-use futures_util::{future, future::Either, ready, stream, FutureExt, Stream, StreamExt};
-use headers::{
- AcceptRanges, CacheControl, ContentLength, ContentRange, ContentType, HeaderMapExt,
- LastModified,
-};
-use http_body_util::{combinators::BoxBody, BodyExt, StreamBody};
-use humansize::FormatSizeOptions;
-use hyper::{
- body::Frame,
- header::{CONTENT_TYPE, LOCATION},
- http::HeaderValue,
- Response, StatusCode,
-};
-use log::debug;
-use markup::Render;
-use percent_encoding::percent_decode_str;
-use serde::Deserialize;
-use serde_yaml::Value;
-use std::{
- fs::Metadata,
- io,
- ops::Range,
- path::{Path, PathBuf},
- pin::Pin,
- sync::Arc,
- task::Poll,
-};
-use tokio::{
- fs::{read_to_string, File},
- io::AsyncSeekExt,
-};
-use tokio_util::io::poll_read_buf;
-
-pub struct FilesKind;
-pub struct FileKind;
-
-#[derive(Debug, Deserialize)]
-struct Files {
- root: PathBuf,
- #[serde(default)]
- index: bool,
- #[serde(default = "return_true")]
- last_modified: bool,
- // #[serde(default = "return_true")]
- // etag: bool,
- #[serde(default)]
- cache: CacheMode,
-}
-#[derive(Debug, Default, Deserialize)]
-#[serde(rename_all = "snake_case")]
-enum CacheMode {
- #[default]
- Public,
- Private,
- NoStore,
-}
-
-impl NodeKind for FilesKind {
- fn name(&self) -> &'static str {
- "files"
- }
- fn instanciate(&self, config: Value) -> anyhow::Result<Arc<dyn Node>> {
- Ok(Arc::new(serde_yaml::from_value::<Files>(config)?))
- }
-}
-
-impl Node for Files {
- fn handle<'a>(
- &'a self,
- _context: &'a mut NodeContext,
- request: NodeRequest,
- ) -> Pin<Box<dyn Future<Output = Result<NodeResponse, ServiceError>> + Send + Sync + 'a>> {
- Box::pin(async move {
- let rpath = request.uri().path();
-
- let mut path = self.root.clone();
- let mut user_path_depth = 0;
- for seg in rpath.split("/") {
- let seg = percent_decode_str(seg).decode_utf8()?;
-
- if seg == "" || seg == "." {
- continue;
- }
-
- if seg == ".." {
- if user_path_depth <= 0 {
- return Err(ServiceError::BadPath);
- }
- path.pop();
- user_path_depth -= 1;
- } else {
- path.push(seg.as_ref());
- user_path_depth += 1;
- }
- }
- if !path.exists() {
- return Err(ServiceError::NotFound);
- }
-
- let metadata = path.metadata()?;
-
- if metadata.file_type().is_dir() {
- debug!("sending index for {path:?}");
- if let Ok(indexhtml) = read_to_string(path.join("index.html")).await {
- return Ok(html_string_response(indexhtml));
- }
-
- if self.index {
- if !rpath.ends_with("/") {
- let mut r = Response::new(String::new());
- *r.status_mut() = StatusCode::FOUND;
- r.headers_mut().insert(
- LOCATION,
- HeaderValue::from_str(&format!("{}/", rpath))
- .map_err(|_| ServiceError::Other)?,
- );
- return Ok(r.map(|b| b.map_err(|e| match e {}).boxed()));
- }
-
- return index(&path, rpath.to_string())
- .await
- .map(html_string_response);
- } else {
- return Err(ServiceError::NotFound);
- }
- }
-
- let modified = metadata.modified()?;
-
- let not_modified = if self.last_modified {
- request
- .headers()
- .typed_get::<headers::IfModifiedSince>()
- .map(|if_modified_since| {
- Ok::<_, ServiceError>(!if_modified_since.is_modified(modified))
- })
- .transpose()?
- .unwrap_or_default()
- } else {
- false
- };
-
- // let etag = ETag::from_str(&calc_etag(modified)).map_err(|_| ServiceError::Other)?;
- // let etag_matches = if self.etag {
- // request.headers()
- // .typed_get::<headers::IfNoneMatch>()
- // .map(|if_none_match| if_none_match.precondition_passes(&etag))
- // .unwrap_or_default()
- // } else {
- // false
- // };
-
- let range = request.headers().typed_get::<headers::Range>();
- let range = bytes_range(range, metadata.len())?;
-
- debug!("sending file {path:?}");
- let file = File::open(path.clone()).await?;
-
- // let skip_body = not_modified || etag_matches;
- let skip_body = not_modified;
- let mut r = if skip_body {
- Response::new("".to_string()).map(|b| b.map_err(|e| match e {}).boxed())
- } else {
- Response::new(BoxBody::new(StreamBody::new(
- StreamBody::new(file_stream(file, 4096, range.clone()))
- .map(|e| e.map(|e| Frame::data(e)).map_err(ServiceError::Io)),
- )))
- };
-
- if !skip_body {
- 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"),
- );
- }
- }
- // if not_modified || etag_matches {
- if not_modified {
- *r.status_mut() = StatusCode::NOT_MODIFIED;
- }
-
- r.headers_mut().typed_insert(AcceptRanges::bytes());
- r.headers_mut()
- .typed_insert(ContentLength(range.end - range.start));
-
- let mime = mime_guess::from_path(path).first_or_octet_stream();
- r.headers_mut().typed_insert(ContentType::from(mime));
-
- r.headers_mut().typed_insert(match self.cache {
- CacheMode::Public => CacheControl::new().with_public(),
- CacheMode::Private => CacheControl::new().with_private(),
- CacheMode::NoStore => CacheControl::new().with_no_store(),
- });
-
- // if self.etag {
- // r.headers_mut().typed_insert(etag);
- // }
- if self.last_modified {
- r.headers_mut().typed_insert(LastModified::from(modified));
- }
-
- Ok(r)
- })
- }
-}
-
-// Adapted from warp (https://github.com/seanmonstar/warp/blob/master/src/filters/fs.rs). Thanks!
-fn file_stream(
- mut file: File,
- buf_size: usize,
- range: Range<u64>,
-) -> impl Stream<Item = Result<Bytes, io::Error>> + Send {
- use std::io::SeekFrom;
-
- let seek = async move {
- if range.start != 0 {
- file.seek(SeekFrom::Start(range.start)).await?;
- }
- Ok(file)
- };
-
- seek.into_stream()
- .map(move |result| {
- let mut buf = BytesMut::new();
- 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))),
- };
-
- Either::Right(stream::poll_fn(move |cx| {
- if len == 0 {
- return Poll::Ready(None);
- }
- reserve_at_least(&mut buf, buf_size);
-
- let n = match ready!(poll_read_buf(Pin::new(&mut f), cx, &mut buf)) {
- Ok(n) => n as u64,
- Err(err) => {
- debug!("file read error: {}", err);
- return Poll::Ready(Some(Err(err)));
- }
- };
-
- if n == 0 {
- debug!("file read found EOF before expected length");
- return Poll::Ready(None);
- }
-
- let mut chunk = buf.split().freeze();
- if n > len {
- chunk = chunk.split_to(len as usize);
- len = 0;
- } else {
- len -= n;
- }
-
- Poll::Ready(Some(Ok(chunk)))
- }))
- })
- .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
- .satisfiable_ranges(max_len)
- .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);
- }
-}
-
-async fn index(path: &Path, rpath: String) -> Result<String, ServiceError> {
- let files = path
- .read_dir()?
- .map(|e| e.and_then(|e| Ok((e.file_name().into_string().unwrap(), e.metadata()?))))
- .filter(|e| e.as_ref().map(|(e, _)| !e.starts_with(".")).unwrap_or(true))
- .collect::<Result<Vec<_>, _>>()?;
- let banner = read_to_string(path.join("index.banner.html")).await.ok();
- let mut s = String::new();
- IndexTemplate {
- files,
- banner,
- path: rpath,
- }
- .render(&mut s)
- .unwrap();
- Ok(s)
-}
-
-fn html_string_response(s: String) -> hyper::Response<BoxBody<Bytes, ServiceError>> {
- let mut r = Response::new(s);
- r.headers_mut()
- .insert(CONTENT_TYPE, HeaderValue::from_static("text/html"));
- r.map(|b| b.map_err(|e| match e {}).boxed())
-}
-
-markup::define! {
- IndexTemplate(path: String, banner: Option<String>, files: Vec<(String, Metadata)>) {
- @markup::doctype()
- html {
- head {
- meta[charset="UTF-8"];
- title { "Index of " @path }
- }
- body {
- @if let Some(banner) = banner {
- @markup::raw(banner)
- } else {
- h1 { "Index of " @path }
- }
- hr;
- table {
- @if path != "/" {
- tr { td { b { a[href=".."] { "../" } } } }
- }
- @for (name, meta) in files { tr {
- td { a[href=name] {
- @name
- @if meta.file_type().is_dir() { "/" }
- } }
- td {
- @if meta.file_type().is_dir() {
- i { "directory" }
- } else {
- @humansize::format_size(meta.len(), FormatSizeOptions::default())
- }
- }
- } }
- }
- hr;
- footer { sub { "served by " a[href="https://codeberg.org/metamuffin/gnix"] { "gnix" } } }
- }
- }
- }
-}
-
-// fn calc_etag(s: SystemTime) -> String {
-// // TODO: make this not change after server restart but still unguessable
-// let mut hasher = DefaultHasher::new();
-// s.hash(&mut hasher);
-// let hash = hasher.finish();
-// hex::encode(hash.to_le_bytes())
-// }
diff --git a/src/filters/hosts.rs b/src/filters/hosts.rs
deleted file mode 100644
index 286d478..0000000
--- a/src/filters/hosts.rs
+++ /dev/null
@@ -1,45 +0,0 @@
-use super::{Node, NodeContext, NodeKind, NodeRequest, NodeResponse};
-use crate::{config::DynNode, error::ServiceError};
-use futures::Future;
-use hyper::header::HOST;
-use serde::Deserialize;
-use serde_yaml::Value;
-use std::{collections::HashMap, pin::Pin, sync::Arc};
-
-#[derive(Deserialize)]
-#[serde(transparent)]
-struct Hosts(HashMap<String, DynNode>);
-
-pub struct HostsKind;
-impl NodeKind for HostsKind {
- fn name(&self) -> &'static str {
- "hosts"
- }
- fn instanciate(&self, config: Value) -> anyhow::Result<Arc<dyn Node>> {
- Ok(Arc::new(serde_yaml::from_value::<Hosts>(config)?))
- }
-}
-impl Node for Hosts {
- fn handle<'a>(
- &'a self,
- context: &'a mut NodeContext,
- request: NodeRequest,
- ) -> Pin<Box<dyn Future<Output = Result<NodeResponse, ServiceError>> + Send + Sync + 'a>> {
- Box::pin(async move {
- let host = request
- .headers()
- .get(HOST)
- .and_then(|e| e.to_str().ok())
- .ok_or(ServiceError::NoHost)?;
-
- let host = remove_port(&host);
- let node = self.0.get(host).ok_or(ServiceError::UnknownHost)?;
-
- node.handle(context, request).await
- })
- }
-}
-
-pub fn remove_port(s: &str) -> &str {
- s.split_once(":").map(|(s, _)| s).unwrap_or(s)
-}
diff --git a/src/filters/mod.rs b/src/filters/mod.rs
deleted file mode 100644
index 2bee8e3..0000000
--- a/src/filters/mod.rs
+++ /dev/null
@@ -1,54 +0,0 @@
-use crate::error::ServiceError;
-use crate::State;
-use accesslog::AccessLogKind;
-use auth::{basic::HttpBasicAuthKind, cookie::CookieAuthKind};
-use bytes::Bytes;
-use error::ErrorKind;
-use file::FileKind;
-use files::FilesKind;
-use futures::Future;
-use hosts::HostsKind;
-use http_body_util::combinators::BoxBody;
-use hyper::{body::Incoming, Request, Response};
-use proxy::ProxyKind;
-use serde_yaml::Value;
-use std::{net::SocketAddr, pin::Pin, sync::Arc};
-
-pub mod accesslog;
-pub mod auth;
-pub mod error;
-pub mod file;
-pub mod files;
-pub mod hosts;
-pub mod proxy;
-
-pub type NodeRequest = Request<Incoming>;
-pub type NodeResponse = Response<BoxBody<Bytes, ServiceError>>;
-
-pub static MODULES: &'static [&'static dyn NodeKind] = &[
- &HttpBasicAuthKind,
- &CookieAuthKind,
- &ProxyKind,
- &HostsKind,
- &FilesKind,
- &FileKind,
- &AccessLogKind,
- &ErrorKind,
-];
-
-pub struct NodeContext {
- pub state: Arc<State>,
- pub addr: SocketAddr,
-}
-
-pub trait NodeKind: Send + Sync + 'static {
- fn name(&self) -> &'static str;
- fn instanciate(&self, config: Value) -> anyhow::Result<Arc<dyn Node>>;
-}
-pub trait Node: Send + Sync + 'static {
- fn handle<'a>(
- &'a self,
- context: &'a mut NodeContext,
- request: NodeRequest,
- ) -> Pin<Box<dyn Future<Output = Result<NodeResponse, ServiceError>> + Send + Sync + 'a>>;
-}
diff --git a/src/filters/proxy.rs b/src/filters/proxy.rs
deleted file mode 100644
index ce72f65..0000000
--- a/src/filters/proxy.rs
+++ /dev/null
@@ -1,98 +0,0 @@
-use super::{Node, NodeContext, NodeKind, NodeRequest, NodeResponse};
-use crate::{helper::TokioIo, ServiceError};
-use futures::Future;
-use http_body_util::BodyExt;
-use hyper::{http::HeaderValue, upgrade::OnUpgrade, StatusCode};
-use log::{debug, warn};
-use serde::Deserialize;
-use serde_yaml::Value;
-use std::{net::SocketAddr, pin::Pin, sync::Arc};
-use tokio::net::TcpStream;
-
-#[derive(Default)]
-pub struct ProxyKind;
-
-#[derive(Debug, Deserialize)]
-struct Proxy {
- backend: SocketAddr,
-}
-
-impl NodeKind for ProxyKind {
- fn name(&self) -> &'static str {
- "proxy"
- }
- fn instanciate(&self, config: Value) -> anyhow::Result<Arc<dyn Node>> {
- Ok(Arc::new(serde_yaml::from_value::<Proxy>(config)?))
- }
-}
-impl Node for Proxy {
- fn handle<'a>(
- &'a self,
- context: &'a mut NodeContext,
- mut request: NodeRequest,
- ) -> Pin<Box<dyn Future<Output = Result<NodeResponse, ServiceError>> + Send + Sync + 'a>> {
- Box::pin(async move {
- request.headers_mut().insert(
- "x-real-ip",
- HeaderValue::from_str(&format!("{}", context.addr.ip())).unwrap(),
- );
-
- let on_upgrade_downstream = request.extensions_mut().remove::<OnUpgrade>();
-
- let _limit_guard = context.state.l_outgoing.try_acquire()?;
- debug!("\tforwarding to {}", self.backend);
- let mut resp = {
- let client_stream = TokioIo(
- TcpStream::connect(self.backend)
- .await
- .map_err(|_| ServiceError::CantConnect)?,
- );
-
- let (mut sender, conn) = hyper::client::conn::http1::handshake(client_stream)
- .await
- .map_err(ServiceError::Hyper)?;
- tokio::task::spawn(async move {
- if let Err(err) = conn.with_upgrades().await {
- warn!("connection failed: {:?}", err);
- }
- });
- sender
- .send_request(request)
- .await
- .map_err(ServiceError::Hyper)?
- };
-
- if resp.status() == StatusCode::SWITCHING_PROTOCOLS {
- let on_upgrade_upstream = resp
- .extensions_mut()
- .remove::<OnUpgrade>()
- .ok_or(ServiceError::UpgradeFailed)?;
- let on_upgrade_downstream =
- on_upgrade_downstream.ok_or(ServiceError::UpgradeFailed)?;
- tokio::task::spawn(async move {
- debug!("about to upgrade connection");
- match (on_upgrade_upstream.await, on_upgrade_downstream.await) {
- (Ok(upgraded_upstream), Ok(upgraded_downstream)) => {
- debug!("upgrade successful");
- match tokio::io::copy_bidirectional(
- &mut TokioIo(upgraded_downstream),
- &mut TokioIo(upgraded_upstream),
- )
- .await
- {
- Ok((from_client, from_server)) => {
- debug!("proxy socket terminated: {from_server} sent, {from_client} received")
- }
- Err(e) => warn!("proxy socket error: {e}"),
- }
- }
- (a, b) => warn!("upgrade error: upstream={a:?} downstream={b:?}"),
- }
- });
- }
-
- let resp = resp.map(|b| b.map_err(ServiceError::Hyper).boxed());
- Ok(resp)
- })
- }
-}