diff --git a/Cargo.toml b/Cargo.toml index babda86..61d58da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ axum-extra = { version = "0.9.1", features = ["typed-header", "query", "async-re axum-macros = "0.4.0" axum-server = { version = "0.6.0", features = ["tls-rustls"] } axum-range = "0.4" -clap = { version = "4.4.2", features = ["derive"] } +clap = { version = "4.4", features = ["derive"] } #enum_dispatch = "0.3.12" futures = "0.3" futures-util = "0.3" diff --git a/src/acl.rs b/src/acl.rs index cdfe600..a672df9 100644 --- a/src/acl.rs +++ b/src/acl.rs @@ -1,8 +1,8 @@ use crate::error::ErrorKind; use crate::handlers::path_analysis::TPE_LOCKS; -use anyhow::Result; +use anyhow::{Context, Result}; use once_cell::sync::OnceCell; -use serde_derive::Deserialize; +use serde_derive::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs; use std::path::PathBuf; @@ -24,7 +24,7 @@ pub fn init_acl(state: Acl) -> Result<(), ErrorKind> { Ok(()) } // Access Types -#[derive(Debug, Clone, PartialEq, PartialOrd, serde_derive::Deserialize)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Serialize, Deserialize)] pub enum AccessType { Nothing, Read, @@ -32,24 +32,6 @@ pub enum AccessType { Modify, } -// #[derive(Debug, Clone)] -// #[enum_dispatch] -// pub(crate) enum AclCheckerEnum { -// Acl(Acl), -// } -// -// impl AclCheckerEnum { -// pub fn acl_from_file( -// append_only: bool, -// private_repo: bool, -// file_path: Option, -// ) -> Result { -// let acl = Acl::from_file(append_only, private_repo, file_path)?; -// Ok(AclCheckerEnum::Acl(acl)) -// } -// } - -//#[enum_dispatch(AclCheckerEnum)] pub trait AclChecker: Send + Sync + 'static { fn allowed(&self, user: &str, path: &str, tpe: &str, access: AccessType) -> bool; } @@ -58,7 +40,7 @@ pub trait AclChecker: Send + Sync + 'static { type RepoAcl = HashMap; // Acl holds ACLs for all repos -#[derive(Clone, Deserialize, Debug)] +#[derive(Clone, Serialize, Deserialize, Debug)] pub struct Acl { repos: HashMap, append_only: bool, @@ -107,6 +89,37 @@ impl Acl { repos, }) } + + // The default repo has not been removed from the self.repos list, so we do not need to add here + // But we still need to remove the ""-tag that was added during the from_file() + pub fn to_file(&self, pth: &PathBuf) -> Result<()> { + let mut clone = self.repos.clone(); + clone.remove(""); + let toml_string = + toml::to_string(&clone).context("Could not serialize ACL config to TOML value")?; + fs::write(&pth, toml_string).context("Could not write ACL file!")?; + Ok(()) + } + + // If we do not have a key with ""-value then "default" is also not a key + // Since we guarantee this during the reading of a acl-file + pub fn default_repo_access(&mut self, user: &str, access: AccessType) { + if !self.repos.contains_key("") { + let mut acl = RepoAcl::new(); + acl.insert(user.into(), access); + self.repos.insert("default".to_owned(), acl.clone()); + self.repos.insert("".to_owned(), acl); + } else { + self.repos + .get_mut("default") + .unwrap() + .insert(user.into(), access.clone()); + self.repos + .get_mut("") + .unwrap() + .insert(user.into(), access.clone()); + } + } } impl AclChecker for Acl { diff --git a/src/auth.rs b/src/auth.rs index 19fb4a1..e1c17c2 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -26,26 +26,12 @@ pub(crate) fn init_auth(state: Auth) -> Result<(), ErrorKind> { Ok(()) } -// #[enum_dispatch] -// #[derive(Debug, Clone)] -// pub(crate) enum AuthCheckerEnum { -// Auth(Auth), -// } -// -// impl AuthCheckerEnum { -// pub fn auth_from_file(no_auth: bool, path: &PathBuf) -> io::Result { -// let auth = Auth::from_file(no_auth, path)?; -// Ok(AuthCheckerEnum::Auth(auth)) -// } -// } - -//#[enum_dispatch(AuthCheckerEnum)] pub trait AuthChecker: Send + Sync + 'static { fn verify(&self, user: &str, passwd: &str) -> bool; } -// read_htpasswd is a helper func that reads the given file in .httpasswd format -// into a Hashmap mapping each user to the whole passwd line +/// read_htpasswd is a helper func that reads the given file in .httpasswd format +/// into a Hashmap mapping each user to the whole passwd line fn read_htpasswd(file_path: &PathBuf) -> io::Result> { let s = fs::read_to_string(file_path)?; // make the contents static in memory @@ -59,17 +45,11 @@ fn read_htpasswd(file_path: &PathBuf) -> io::Result>, } -impl Default for Auth { - fn default() -> Self { - Self { users: None } - } -} - impl Auth { pub fn from_file(no_auth: bool, path: &PathBuf) -> io::Result { Ok(Self { @@ -97,7 +77,7 @@ impl AuthChecker for Auth { #[derive(Deserialize)] pub struct AuthFromRequest { pub(crate) user: String, - pub(crate) password: String, + pub(crate) _password: String, } #[async_trait::async_trait] @@ -114,12 +94,12 @@ impl FromRequestParts for AuthFromRequest { return match auth_result { Ok(auth) => { let AuthBasic((user, passw)) = auth; - let password = match passw { - None => "".to_string(), - Some(p) => p, - }; + let password = passw.unwrap_or_else(|| "".to_string()); if checker.verify(user.as_str(), password.as_str()) { - Ok(Self { user, password }) + Ok(Self { + user, + _password: password, + }) } else { Err(ErrorKind::UserAuthenticationError(user)) } @@ -129,7 +109,7 @@ impl FromRequestParts for AuthFromRequest { if checker.verify("", "") { return Ok(Self { user, - password: "".to_string(), + _password: "".to_string(), }); } Err(ErrorKind::AuthenticationHeaderError) @@ -142,18 +122,19 @@ impl FromRequestParts for AuthFromRequest { mod test { use super::*; use crate::auth::Auth; - use crate::test_server::{init_mutex, TestServer, WAIT_DELAY, WEB}; + use crate::test_helpers::{basic_auth_header_value, init_test_environment}; use anyhow::Result; - use axum::http::StatusCode; + use axum::body::Body; + use axum::http::{Method, Request, StatusCode}; use axum::routing::get; use axum::Router; + use http_body_util::BodyExt; use std::env; - use std::net::SocketAddr; use std::path::PathBuf; - use tokio::net::TcpListener; + use tower::ServiceExt; #[test] - fn test_htaccess_account() -> Result<()> { + fn test_auth() -> Result<()> { let cwd = env::current_dir()?; let htaccess = PathBuf::new().join(cwd).join("test_data").join("htaccess"); let auth = Auth::from_file(false, &htaccess)?; @@ -164,7 +145,7 @@ mod test { } #[test] - fn test_static_htaccess() { + fn test_auth_from_file() { let cwd = env::current_dir().unwrap(); let htaccess = PathBuf::new().join(cwd).join("test_data").join("htaccess"); @@ -178,108 +159,109 @@ mod test { assert!(!auth.verify("test", "__test_pw")); } - #[tokio::test] - async fn server_auth_tester() -> Result<()> { - init_mutex(); - let _r = WEB.get().take().unwrap(); - - let app = Router::new() - .route("/basic", get(tester_basic)) - .route("/rustic_server", get(tester_rustic_server)); - - let mut server = TestServer::new(app); - server.launch().await; - - // Tests - good().await; - wrong_authentication().await; - nothing().await; - - server.stop_server().await; - - Ok(()) - } - - async fn tester_basic(AuthBasic((id, password)): AuthBasic) -> String { + async fn test_handler_basic(AuthBasic((id, password)): AuthBasic) -> String { format!("Got {} and {:?}", id, password) } - async fn tester_rustic_server(auth: AuthFromRequest) -> String { + async fn test_handler_from_request(auth: AuthFromRequest) -> String { format!("User = {}", auth.user) } - /// The requests which should be returned fine - async fn good() { + /// The requests which should be returned OK + #[tokio::test] + async fn test_authentication() { + init_test_environment(); + + // ----------------------------------------- // Try good basic - let client = reqwest::Client::new(); - let resp = client - .get(TestServer::url("/basic")) - .basic_auth("My Username", Some("My Password")) - .send() - .await + // ----------------------------------------- + let app = Router::new().route("/basic", get(test_handler_basic)); + + let request = Request::builder() + .uri("/basic") + .method(Method::GET) + .header( + "Authorization", + basic_auth_header_value("My Username", Some("My Password")), + ) + .body(Body::empty()) .unwrap(); + + let resp = app.oneshot(request).await.unwrap(); + assert_eq!(resp.status().as_u16(), StatusCode::OK.as_u16()); + let body = resp.into_parts().1; + let byte_vec = body.into_data_stream().collect().await.unwrap().to_bytes(); + let body_str = String::from_utf8(byte_vec.to_vec()).unwrap(); assert_eq!( - resp.text().await.unwrap(), + body_str, String::from("Got My Username and Some(\"My Password\")") ); - // Try good rustic_server - let client = reqwest::Client::new(); - let resp = client - .get(TestServer::url("/rustic_server")) - .basic_auth("test", Some("test_pw")) - .send() - .await + // ----------------------------------------- + // Try good using auth struct + // ----------------------------------------- + let app = Router::new().route("/rustic_server", get(test_handler_from_request)); + + let request = Request::builder() + .uri("/rustic_server") + .method(Method::GET) + .header( + "Authorization", + basic_auth_header_value("test", Some("test_pw")), + ) + .body(Body::empty()) .unwrap(); + + let resp = app.oneshot(request).await.unwrap(); + assert_eq!(resp.status().as_u16(), StatusCode::OK.as_u16()); - assert_eq!(resp.text().await.unwrap(), String::from("User = test")); + let body = resp.into_parts().1; + let byte_vec = body.collect().await.unwrap().to_bytes(); + let body_str = String::from_utf8(byte_vec.to_vec()).unwrap(); + assert_eq!(body_str, String::from("User = test")); } - async fn wrong_authentication() { - // Try bearer authetication method in basic - let client = reqwest::Client::new(); - let resp = client - .get(TestServer::url("/basic")) - .bearer_auth("123124nfienrign") - .send() - .await - .unwrap(); - assert_eq!(resp.status().as_u16(), StatusCode::BAD_REQUEST.as_u16()); - assert_eq!( - resp.text().await.unwrap(), - String::from("`Authorization` header must be for basic authentication") - ); + #[tokio::test] + async fn test_authentication_errors() { + init_test_environment(); + // ----------------------------------------- // Try wrong password rustic_server - let client = reqwest::Client::new(); - let resp = client - .get(TestServer::url("/rustic_server")) - .basic_auth("test", Some("__test_pw")) - .send() - .await + // ----------------------------------------- + let app = Router::new().route("/rustic_server", get(test_handler_from_request)); + + let request = Request::builder() + .uri("/rustic_server") + .method(Method::GET) + .header( + "Authorization", + basic_auth_header_value("test", Some("__test_pw")), + ) + .body(Body::empty()) .unwrap(); - assert_eq!(resp.status().as_u16(), StatusCode::FORBIDDEN.as_u16()); - assert_eq!( - resp.text().await.unwrap(), - String::from("Failed to authenticate user: \"test\"") - ); - } - /// Sees if we can get nothing from basic or bearer successfully - async fn nothing() { - // Try basic - let client = reqwest::Client::new(); - let resp = client - .get(TestServer::url("/basic")) - .basic_auth("", Some("")) - .send() - .await + let resp = app.oneshot(request).await.unwrap(); + + assert_eq!(resp.status(), StatusCode::FORBIDDEN); + + // ----------------------------------------- + // Try without authentication header + // ----------------------------------------- + let app = Router::new().route("/rustic_server", get(test_handler_from_request)); + + let request = Request::builder() + .uri("/rustic_server") + .method(Method::GET) + .header( + "Authorization", + basic_auth_header_value("test", Some("__test_pw")), + ) + .body(Body::empty()) .unwrap(); - assert_eq!(resp.status().as_u16(), StatusCode::OK.as_u16()); - assert_eq!( - resp.text().await.unwrap(), - String::from("Got and Some(\"\")") - ); + + let resp = app.oneshot(request).await.unwrap(); + + assert_eq!(resp.status().as_u16(), StatusCode::FORBIDDEN); } } diff --git a/src/bin/rustic-server.rs b/src/bin/rustic-server.rs index 5536121..c28a123 100644 --- a/src/bin/rustic-server.rs +++ b/src/bin/rustic-server.rs @@ -1,23 +1,52 @@ use anyhow::Result; -use clap::Parser; -use rustic_server::log::init_tracing; -use rustic_server::{acl::Acl, auth::Auth, storage::LocalStorage, web, Opts}; -use std::net::SocketAddr; +use clap::{Parser, Subcommand}; +use rustic_server::commands::serve::{serve, Opts}; use std::str::FromStr; #[tokio::main] async fn main() -> Result<()> { - let opts = Opts::parse(); + let cmd = RusticServer::parse(); + cmd.exec().await?; + Ok(()) +} - init_tracing(); +/// rustic_server +/// A REST server built in rust for use with rustic and restic. +#[derive(Parser)] +#[command(version, bin_name = "rustic_server", disable_help_subcommand = false)] +struct RusticServer { + #[command(subcommand)] + command: Commands, +} - let storage = LocalStorage::try_new(&opts.path)?; - let auth = Auth::from_file(opts.no_auth, &opts.path.join(".htpasswd"))?; - let acl = Acl::from_file(opts.append_only, opts.private_repo, opts.acl)?; +#[derive(Subcommand)] +enum Commands { + /// Start the REST web-server. + Serve(Opts), + // Modify credentials in the .htaccess file. + //Auth(HtAccessCmd), + // Create a configuration from scratch. + //Config, +} - let sa = SocketAddr::from_str(&opts.listen)?; - web::web_browser(acl, auth, storage, sa, opts.tls, opts.cert, opts.key) - .await - .unwrap(); - Ok(()) +/// The server configuration file should point us to the `.htaccess` file. +/// If not we complain to the user. +/// +/// To be nice, if the `.htaccess` file pointed to does not exist, then we create it. +/// We do so, even if it is not called `.htaccess`. +impl RusticServer { + pub async fn exec(self) -> Result<()> { + match self.command { + // Commands::Auth(cmd) => { + // cmd.exec()?; + // } + // Commands::Config => { + // rustic_server_configuration()?; + // } + Commands::Serve(opts) => { + serve(opts).await.unwrap(); + } + } + Ok(()) + } } diff --git a/src/commands.rs b/src/commands.rs new file mode 100644 index 0000000..ac2b204 --- /dev/null +++ b/src/commands.rs @@ -0,0 +1 @@ +pub mod serve; diff --git a/src/commands/serve.rs b/src/commands/serve.rs new file mode 100644 index 0000000..a317050 --- /dev/null +++ b/src/commands/serve.rs @@ -0,0 +1,121 @@ +use crate::acl::Acl; +use crate::auth::Auth; +use crate::config::server_config::ServerConfig; +use crate::error::{ErrorKind, Result}; +use crate::log::init_tracing; +use crate::storage::LocalStorage; +use crate::web::start_web_server; +use clap::Parser; +use std::net::SocketAddr; +use std::path::PathBuf; +use std::str::FromStr; + +// FIXME: we should not return crate::error::Result here; maybe anyhow::Result, + +pub async fn serve(opts: Opts) -> Result<()> { + init_tracing(); + + match &opts.config { + Some(config) => { + let config_path = PathBuf::new().join(&config); + let server_config = ServerConfig::from_file(&config_path) + .unwrap_or_else(|_| panic!("Can not load server configuration file: {}", &config)); + + // Repository storage + let storage_path = PathBuf::new().join(server_config.repos.storage_path); + let storage = match LocalStorage::try_new(&storage_path) { + Ok(s) => s, + Err(e) => return Err(ErrorKind::InternalError(e.to_string())), + }; + + // Authorization user/password + let auth_config = server_config.authorization; + let no_auth = !auth_config.use_auth; + let path = match auth_config.auth_path { + None => PathBuf::new(), + Some(p) => PathBuf::new().join(p), + }; + let auth = match Auth::from_file(no_auth, &path) { + Ok(s) => s, + Err(e) => return Err(ErrorKind::InternalError(e.to_string())), + }; + + // Access control to the repositories + let acl_config = server_config.accesscontrol; + let path = acl_config.acl_path.map(|p| PathBuf::new().join(p)); + let acl = match Acl::from_file(acl_config.append_only, acl_config.private_repo, path) { + Ok(s) => s, + Err(e) => return Err(ErrorKind::InternalError(e.to_string())), + }; + + // Server definition + let socket = SocketAddr::from_str(&opts.listen).unwrap(); + start_web_server(acl, auth, storage, socket, false, None, opts.key).await + } + None => { + let storage = match LocalStorage::try_new(&opts.path) { + Ok(s) => s, + Err(e) => return Err(ErrorKind::InternalError(e.to_string())), + }; + let auth = match Auth::from_file(opts.no_auth, &opts.path.join(".htpasswd")) { + Ok(s) => s, + Err(e) => return Err(ErrorKind::InternalError(e.to_string())), + }; + let acl = match Acl::from_file(opts.append_only, opts.private_repo, None) { + Ok(s) => s, + Err(e) => return Err(ErrorKind::InternalError(e.to_string())), + }; + + start_web_server( + acl, + auth, + storage, + SocketAddr::from_str(&opts.listen).unwrap(), + false, + None, + opts.key, + ) + .await + } + } +} + +/// A REST server build in rust for use with restic +#[derive(Parser)] +#[command(name = "rustic-server")] +#[command(bin_name = "rustic-server")] +pub struct Opts { + /// Server configuration file + #[arg(short, long)] + pub config: Option, + /// listen address + #[arg(short, long, default_value = "localhost:8000")] + pub listen: String, + /// data directory + #[arg(short, long, default_value = "/tmp/restic")] + pub path: PathBuf, + /// disable .htpasswd authentication + #[arg(long)] + pub no_auth: bool, + /// file to read per-repo ACLs from + #[arg(long)] + pub acl: Option, + /// set standard acl to append only mode + #[arg(long)] + pub append_only: bool, + /// set standard acl to only access private repos + #[arg(long)] + pub private_repo: bool, + /// turn on TLS support + #[arg(long)] + pub tls: bool, + /// TLS certificate path + #[arg(long)] + pub cert: Option, + /// TLS key path + #[arg(long)] + pub key: Option, + /// logging level (Off/Error/Warn/Info/Debug/Trace) + #[arg(long, default_value = "Info")] + pub log: String, +} diff --git a/src/config.rs b/src/config.rs index 8276f32..84108bf 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,2 +1,3 @@ +pub mod auth_config; pub mod auth_file; pub mod server_config; diff --git a/src/config/__mod.rs b/src/config/__mod.rs deleted file mode 100644 index f0bb00c..0000000 --- a/src/config/__mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod auth_file; -pub mod server_config; \ No newline at end of file diff --git a/src/config/auth_config.rs b/src/config/auth_config.rs new file mode 100644 index 0000000..1a062a0 --- /dev/null +++ b/src/config/auth_config.rs @@ -0,0 +1,174 @@ +use anyhow::Result; +use htpasswd_verify::md5::{format_hash, md5_apr1_encode}; +use rand::distributions::Alphanumeric; +use rand::{thread_rng, Rng}; +use std::collections::HashMap; +use std::fmt::{Display, Formatter}; +use std::fs; +use std::fs::read_to_string; +use std::io::Write; +use std::path::PathBuf; + +const SALT_LEN: usize = 8; + +#[derive(Clone)] +pub struct HtAccess { + pub path: PathBuf, + pub credentials: HashMap, +} + +impl HtAccess { + pub fn from_file(pth: &PathBuf) -> Result { + let mut c: HashMap = HashMap::new(); + if pth.exists() { + read_to_string(pth)? + .lines() // split the string into an iterator of string slices + .map(String::from) // make each slice into a string + .for_each(|line| match Credential::from_line(line) { + None => {} + Some(cred) => { + c.insert(cred.name.clone(), cred); + } + }) + } + return Ok(HtAccess { + path: pth.clone(), + credentials: c, + }); + } + + pub fn get(&self, name: &str) -> Option<&Credential> { + self.credentials.get(name) + } + + pub fn users(&self) -> Vec { + let ret: Vec = self.credentials.keys().cloned().collect(); + return ret; + } + + /// Update can be used for both new, and existing credentials + pub fn update(&mut self, name: &str, pass: &str) { + let cred = Credential::new(name, pass); + self.insert(cred); + } + + /// Removes one credential by user name + pub fn delete(&mut self, name: &str) { + self.credentials.remove(name); + } + + fn insert(&mut self, cred: Credential) { + self.credentials.insert(cred.name.clone(), cred); + } + + /// FIXME: Nicer error logging for when we can not write file ... + pub fn to_file(&mut self) -> Result<()> { + let mut file = fs::OpenOptions::new() + .create(true) + .truncate(false) + .write(true) + .open(&self.path)?; + + for (_n, c) in self.credentials.iter() { + file.write(c.to_line().as_bytes()).unwrap(); + } + Ok(()) + } +} + +#[derive(Clone)] +pub struct Credential { + name: String, + hash_val: Option, + pw: Option, +} + +impl Credential { + pub fn new(name: &str, pass: &str) -> Self { + let salt: String = thread_rng() + .sample_iter(&Alphanumeric) + .take(SALT_LEN) + .map(char::from) + .collect(); + let hash = md5_apr1_encode(pass, salt.as_str()); + let hash = format_hash(&hash.as_str(), &salt.as_str()); + + Credential { + name: name.into(), + hash_val: Some(hash), + pw: Some(pass.into()), + } + } + + /// Returns a credential struct from a htaccess file line + /// Of cause without password :-) + pub fn from_line(line: String) -> Option { + let spl: Vec<&str> = line.split(':').collect(); + if !spl.is_empty() { + return Some(Credential { + name: spl.get(0).unwrap().to_string(), + hash_val: Some(spl.get(1).unwrap().to_string()), + pw: None, + }); + } + None + } + + pub fn to_line(&self) -> String { + if self.hash_val.is_some() { + return format!( + "{}:{}\n", + self.name.as_str(), + self.hash_val.as_ref().unwrap() + ); + } else { + "".into() + } + } +} + +impl Display for Credential { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "Struct: Credential\n")?; + write!(f, "\tUser: {}\n", self.name.as_str())?; + write!(f, "\tHash: {}\n", self.hash_val.as_ref().unwrap())?; + if self.pw.is_none() { + write!(f, "\tPassword: None\n")?; + } else { + write!(f, "\tPassword: {}\n", &self.pw.as_ref().unwrap())?; + } + Ok(()) + } +} + +#[cfg(test)] +mod test { + use crate::auth::{Auth, AuthChecker}; + use crate::config::auth_config::HtAccess; + use anyhow::Result; + use std::fs; + use std::path::Path; + + #[test] + fn test_htaccess() -> Result<()> { + let htaccess_pth = Path::new("tmp_test_data").join("rustic"); + fs::create_dir_all(&htaccess_pth).unwrap(); + + let ht_file = htaccess_pth.join(".htaccess"); + + let mut ht = HtAccess::from_file(&ht_file)?; + ht.update("Administrator", "stuff"); + ht.update("backup-user", "itsme"); + ht.to_file()?; + + let ht = HtAccess::from_file(&ht_file)?; + assert!(ht.get(&"Administrator").is_some()); + assert!(ht.get(&"backup-user").is_some()); + + let auth = Auth::from_file(false, &ht_file).unwrap(); + assert!(auth.verify("Administrator", "stuff")); + assert!(auth.verify(&"backup-user", "itsme")); + + Ok(()) + } +} diff --git a/src/handlers/__mod.rs b/src/handlers/__mod.rs deleted file mode 100644 index f57d08e..0000000 --- a/src/handlers/__mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -// web server response handler modules -pub(crate) mod repository; -pub(crate) mod files_list; -pub(crate) mod file_length; -pub(crate) mod file_exchange; -pub(crate) mod file_config; - -// Support modules -pub(crate) mod path_analysis; -pub(crate) mod file_helpers; -mod access_check; -//mod ranged_stream; diff --git a/src/handlers/file_config.rs b/src/handlers/file_config.rs index 1ecf057..40051e0 100644 --- a/src/handlers/file_config.rs +++ b/src/handlers/file_config.rs @@ -7,16 +7,13 @@ use crate::handlers::file_exchange::{check_name, get_file, get_save_file, save_b use crate::handlers::path_analysis::{decompose_path, ArchivePathEnum, DEFAULT_PATH}; use crate::storage::STORAGE; use axum::extract::Request; -use axum::http::HeaderMap; use axum::{extract::Path as PathExtract, response::IntoResponse}; use axum_extra::headers::Range; use axum_extra::TypedHeader; use std::path::{Path, PathBuf}; -//============================================================================== -// has_config -// Interface: HEAD {path}/config -//============================================================================== +/// has_config +/// Interface: HEAD {path}/config pub(crate) async fn has_config( auth: AuthFromRequest, path: Option>, @@ -44,11 +41,8 @@ pub(crate) async fn has_config( } } -//============================================================================== -// get_config -// Interface: GET {path}/config -//============================================================================== - +/// get_config +/// Interface: GET {path}/config pub(crate) async fn get_config( auth: AuthFromRequest, path: Option>, @@ -62,14 +56,11 @@ pub(crate) async fn get_config( // assert_eq!( &tpe, TPE_CONFIG); // tracing::debug!("[get_config] path: {p_str:?}, tpe: {tpe}, name: config"); - return get_file(auth, path, range).await; + get_file(auth, path, range).await } -//============================================================================== -// add_config -// Interface: POST {path}/config -//============================================================================== - +/// add_config +/// Interface: POST {path}/config pub(crate) async fn add_config( auth: AuthFromRequest, path: Option>, @@ -80,7 +71,7 @@ pub(crate) async fn add_config( let p_str = archive_path.path; let tpe = archive_path.tpe; let name = archive_path.name; - assert_eq!(&archive_path.path_type, &ArchivePathEnum::CONFIG); + assert_eq!(&archive_path.path_type, &ArchivePathEnum::Config); assert_eq!(&name, "config"); tracing::debug!("[add_config] path: {p_str}, tpe: {tpe}, name: {name}"); @@ -92,13 +83,9 @@ pub(crate) async fn add_config( Ok(()) } -//============================================================================== -// delete_config -// Interface: DELETE {path}/config -// FIXME: The original restic spec does not define delete_config --> but rustic did ?? -//============================================================================== - -//#[debug_handler] +/// delete_config +/// Interface: DELETE {path}/config +/// FIXME: The original restic spec does not define delete_config --> but rustic did ?? pub(crate) async fn delete_config( auth: AuthFromRequest, path: Option>, @@ -108,7 +95,7 @@ pub(crate) async fn delete_config( let p_str = archive_path.path; let tpe = archive_path.tpe; let name = archive_path.name; - assert_eq!(&archive_path.path_type, &ArchivePathEnum::CONFIG); + assert_eq!(&archive_path.path_type, &ArchivePathEnum::Config); tracing::debug!("[delete_config] path: {p_str}, tpe: {tpe}, name: {name}"); check_name(tpe.as_str(), &name)?; @@ -127,7 +114,9 @@ pub(crate) async fn delete_config( mod test { use crate::handlers::file_config::{add_config, delete_config, get_config, has_config}; use crate::handlers::repository::{create_repository, delete_repository}; - use crate::test_server::{basic_auth, init_test_environment, print_request_response}; + use crate::test_helpers::{ + basic_auth_header_value, init_test_environment, print_request_response, + }; use axum::http::Method; use axum::routing::{delete, get, head, post}; use axum::{ @@ -135,8 +124,6 @@ mod test { http::{Request, StatusCode}, }; use axum::{middleware, Router}; - use axum_extra::headers::Range; - use axum_extra::TypedHeader; use http_body_util::BodyExt; use std::path::PathBuf; use std::{env, fs}; @@ -156,7 +143,10 @@ mod test { let request = Request::builder() .uri("/test_repo/data/config") .method(Method::HEAD) - .header("Authorization", basic_auth("test", Some("test_pw"))) + .header( + "Authorization", + basic_auth_header_value("test", Some("test_pw")), + ) .body(Body::empty()) .unwrap(); @@ -174,7 +164,10 @@ mod test { let request = Request::builder() .uri("/test_repo/config") .method(Method::HEAD) - .header("Authorization", basic_auth("test", Some("test_pw"))) + .header( + "Authorization", + basic_auth_header_value("test", Some("test_pw")), + ) .body(Body::empty()) .unwrap(); @@ -214,7 +207,10 @@ mod test { let request = Request::builder() .uri(&repo_name_uri) .method(Method::POST) - .header("Authorization", basic_auth("test", Some("test_pw"))) + .header( + "Authorization", + basic_auth_header_value("test", Some("test_pw")), + ) .body(Body::empty()) .unwrap(); @@ -236,7 +232,10 @@ mod test { let request = Request::builder() .uri(&uri) .method(Method::POST) - .header("Authorization", basic_auth("test", Some("test_pw"))) + .header( + "Authorization", + basic_auth_header_value("test", Some("test_pw")), + ) .body(body) .unwrap(); @@ -258,7 +257,10 @@ mod test { let request = Request::builder() .uri(&uri) .method(Method::GET) - .header("Authorization", basic_auth("test", Some("test_pw"))) + .header( + "Authorization", + basic_auth_header_value("test", Some("test_pw")), + ) .body(Body::empty()) .unwrap(); @@ -281,7 +283,10 @@ mod test { let request = Request::builder() .uri(&uri) .method(Method::HEAD) - .header("Authorization", basic_auth("test", Some("test_pw"))) + .header( + "Authorization", + basic_auth_header_value("test", Some("test_pw")), + ) .body(Body::empty()) .unwrap(); @@ -299,7 +304,10 @@ mod test { let request = Request::builder() .uri(&uri) .method(Method::DELETE) - .header("Authorization", basic_auth("test", Some("test_pw"))) + .header( + "Authorization", + basic_auth_header_value("test", Some("test_pw")), + ) .body(Body::empty()) .unwrap(); @@ -320,7 +328,10 @@ mod test { let request = Request::builder() .uri(&repo_name_uri) .method(Method::POST) - .header("Authorization", basic_auth("test", Some("test_pw"))) + .header( + "Authorization", + basic_auth_header_value("test", Some("test_pw")), + ) .body(Body::empty()) .unwrap(); diff --git a/src/handlers/file_exchange.rs b/src/handlers/file_exchange.rs index 5b8e344..eb4c379 100644 --- a/src/handlers/file_exchange.rs +++ b/src/handlers/file_exchange.rs @@ -33,7 +33,7 @@ pub(crate) async fn add_file( let p_str = archive_path.path; let tpe = archive_path.tpe; let name = archive_path.name; - assert_ne!(archive_path.path_type, ArchivePathEnum::CONFIG); + assert_ne!(archive_path.path_type, ArchivePathEnum::Config); assert_ne!(&name, ""); tracing::debug!("[get_file] path: {p_str}, tpe: {tpe}, name: {name}"); @@ -105,49 +105,6 @@ pub(crate) async fn get_file( let body = KnownSize::file(file).await.unwrap(); let range = range.map(|TypedHeader(range)| range); Ok(Ranged::new(range, body).into_response()) - - // let mut len = match file.metadata().await { - // Ok(val) => val.len(), - // Err(_) => { - // return Err(ErrorKind::GettingFileMetadataFailed); - // } - // }; - // - // let mut status = StatusCode::OK; - // if let Some(header_value) = headers.get(header::RANGE) { - // let header_value = match header_value.to_str() { - // Ok(val) => val, - // Err(_) => return Err(ErrorKind::RangeNotValid) - // }; - // match HttpRange::parse(header_value, len, ) { - // Ok(range) if range.len() == 1 => { - // tracing::debug!("[get_file] range: {:?}", &range[0]); - // if file.seek(Start(range[0].start)).await.is_err() { - // return Err(ErrorKind::SeekingFileFailed); - // }; - // len = range[0].length; - // status = StatusCode::PARTIAL_CONTENT; - // } - // Ok(_) => return Err(ErrorKind::MultipartRangeNotImplemented), - // Err(_) => return Err(ErrorKind::GeneralRange), - // } - // }; - // - // tracing::debug!("[get_file] length: {:?}", &len); - // - // // From: https://github.com/tokio-rs/axum/discussions/608#discussioncomment-1789020 - // let stream = ReaderStream::with_capacity( - // file, - // match len.try_into() { - // Ok(val) => val, - // Err(_) => return Err(ErrorKind::ConversionToU64Failed), - // }, - // ); - // - // let body = Body::from_stream(stream); - // - // let headers = AppendHeaders([(header::CONTENT_TYPE, "application/octet-stream")]); - // Ok((status, headers, body)) } //============================================================================== @@ -210,7 +167,7 @@ where } #[cfg(test)] -fn check_string_sha256(name: &str) -> bool { +fn check_string_sha256(_name: &str) -> bool { true } @@ -239,7 +196,9 @@ pub(crate) fn check_name(tpe: &str, name: &str) -> Result { #[cfg(test)] mod test { use crate::handlers::file_exchange::{add_file, delete_file, get_file}; - use crate::test_server::{basic_auth, init_test_environment, print_request_response}; + use crate::test_helpers::{ + basic_auth_header_value, init_test_environment, print_request_response, + }; use axum::http::{header, Method}; use axum::routing::{delete, get, put}; use axum::{ @@ -285,7 +244,10 @@ mod test { let request = Request::builder() .uri(uri) .method(Method::PUT) - .header("Authorization", basic_auth("test", Some("test_pw"))) + .header( + "Authorization", + basic_auth_header_value("test", Some("test_pw")), + ) .body(body) .unwrap(); @@ -308,7 +270,10 @@ mod test { let request = Request::builder() .uri(uri) .method(Method::DELETE) - .header("Authorization", basic_auth("test", Some("test_pw"))) + .header( + "Authorization", + basic_auth_header_value("test", Some("test_pw")), + ) .body(body) .unwrap(); @@ -353,7 +318,10 @@ mod test { let request = Request::builder() .uri(uri) .method(Method::PUT) - .header("Authorization", basic_auth("test", Some("test_pw"))) + .header( + "Authorization", + basic_auth_header_value("test", Some("test_pw")), + ) .body(body) .unwrap(); @@ -376,7 +344,10 @@ mod test { let request = Request::builder() .uri(uri) .method(Method::GET) - .header("Authorization", basic_auth("test", Some("test_pw"))) + .header( + "Authorization", + basic_auth_header_value("test", Some("test_pw")), + ) .body(body) .unwrap(); @@ -398,7 +369,10 @@ mod test { .uri(uri) .method(Method::GET) .header(header::RANGE, "bytes=6-12") - .header("Authorization", basic_auth("test", Some("test_pw"))) + .header( + "Authorization", + basic_auth_header_value("test", Some("test_pw")), + ) .body(Body::empty()) .unwrap(); @@ -425,7 +399,10 @@ mod test { let request = Request::builder() .uri(uri) .method(Method::DELETE) - .header("Authorization", basic_auth("test", Some("test_pw"))) + .header( + "Authorization", + basic_auth_header_value("test", Some("test_pw")), + ) .body(Body::empty()) .unwrap(); diff --git a/src/handlers/file_length.rs b/src/handlers/file_length.rs index 4f1bcf2..2ff70a3 100644 --- a/src/handlers/file_length.rs +++ b/src/handlers/file_length.rs @@ -8,11 +8,8 @@ use axum::{extract::Path as PathExtract, http::header, response::IntoResponse}; use axum_extra::headers::HeaderMap; use std::path::Path; -//============================================================================== -// Length -// Interface: HEAD {path}/{type}/{name} -//============================================================================== - +/// Length +/// Interface: HEAD {path}/{type}/{name} pub(crate) async fn file_length( auth: AuthFromRequest, path: Option>, @@ -22,7 +19,7 @@ pub(crate) async fn file_length( let p_str = archive_path.path; let tpe = archive_path.tpe; let name = archive_path.name; - assert_ne!(archive_path.path_type, ArchivePathEnum::CONFIG); + assert_ne!(archive_path.path_type, ArchivePathEnum::Config); tracing::debug!("[length] path: {p_str}, tpe: {tpe}, name: {name}"); let path = Path::new(&p_str); @@ -55,7 +52,9 @@ pub(crate) async fn file_length( #[cfg(test)] mod test { use crate::handlers::file_length::file_length; - use crate::test_server::{basic_auth, init_test_environment, print_request_response}; + use crate::test_helpers::{ + basic_auth_header_value, init_test_environment, print_request_response, + }; use axum::http::{header, Method}; use axum::routing::head; use axum::{ @@ -80,7 +79,10 @@ mod test { let request = Request::builder() .uri("/test_repo/keys/2e734da3fccb98724ece44efca027652ba7a335c224448a68772b41c0d9229d5") .method(Method::HEAD) - .header("Authorization", basic_auth("test", Some("test_pw"))) + .header( + "Authorization", + basic_auth_header_value("test", Some("test_pw")), + ) .body(Body::empty()) .unwrap(); @@ -116,7 +118,10 @@ mod test { let request = Request::builder() .uri("/test_repo/keys/__I_do_not_exist__") .method(Method::HEAD) - .header("Authorization", basic_auth("test", Some("test_pw"))) + .header( + "Authorization", + basic_auth_header_value("test", Some("test_pw")), + ) .body(Body::empty()) .unwrap(); diff --git a/src/handlers/files_list.rs b/src/handlers/files_list.rs index 9322c25..0e10f76 100644 --- a/src/handlers/files_list.rs +++ b/src/handlers/files_list.rs @@ -11,7 +11,6 @@ use axum::{ Json, }; use axum_extra::headers::HeaderMap; -use axum_macros::debug_handler; use serde_derive::{Deserialize, Serialize}; use std::path::Path; @@ -26,7 +25,6 @@ struct RepoPathEntry { size: u64, } -#[debug_handler] pub(crate) async fn list_files( auth: AuthFromRequest, path: Option>, @@ -36,7 +34,7 @@ pub(crate) async fn list_files( let archive_path = decompose_path(path_string)?; let p_str = archive_path.path; let tpe = archive_path.tpe; - assert_ne!(archive_path.path_type, ArchivePathEnum::CONFIG); + assert_ne!(archive_path.path_type, ArchivePathEnum::Config); assert_eq!(archive_path.name, "".to_string()); tracing::debug!("[list_files] path: {p_str}, tpe: {tpe}"); @@ -88,7 +86,9 @@ pub(crate) async fn list_files( #[cfg(test)] mod test { use crate::handlers::files_list::{list_files, RepoPathEntry, API_V1, API_V2}; - use crate::test_server::{basic_auth, init_test_environment, print_request_response}; + use crate::test_helpers::{ + basic_auth_header_value, init_test_environment, print_request_response, + }; use axum::http::header::{ACCEPT, CONTENT_TYPE}; use axum::routing::get; use axum::{ @@ -111,7 +111,10 @@ mod test { let request = Request::builder() .uri("/test_repo/keys") .header(ACCEPT, API_V1) - .header("Authorization", basic_auth("test", Some("test_pw"))) + .header( + "Authorization", + basic_auth_header_value("test", Some("test_pw")), + ) .body(Body::empty()) .unwrap(); @@ -151,7 +154,10 @@ mod test { let requrest = Request::builder() .uri("/test_repo/keys") .header(ACCEPT, API_V2) - .header("Authorization", basic_auth("test", Some("test_pw"))) + .header( + "Authorization", + basic_auth_header_value("test", Some("test_pw")), + ) .body(Body::empty()) .unwrap(); diff --git a/src/handlers/path_analysis.rs b/src/handlers/path_analysis.rs index f3ee19f..3c77270 100644 --- a/src/handlers/path_analysis.rs +++ b/src/handlers/path_analysis.rs @@ -16,13 +16,13 @@ pub(crate) const TYPES: [&str; 5] = [TPE_DATA, TPE_KEYS, TPE_LOCKS, TPE_SNAPSHOT #[derive(Debug, PartialEq)] pub(crate) enum ArchivePathEnum { - DATA, - KEYS, - LOCKS, - SNAPSHOTS, - INDEX, - CONFIG, - NONE, + Data, + Keys, + Locks, + Snapshots, + Index, + Config, + None, } pub(crate) struct ArchivePath { @@ -53,7 +53,7 @@ pub(crate) fn decompose_path(path: String) -> Result { tracing::debug!("[decompose_path] elem = {:?}", &elem); let mut ap = ArchivePath { - path_type: ArchivePathEnum::NONE, + path_type: ArchivePathEnum::None, tpe: "".to_string(), path: "".to_string(), name: "".to_string(), @@ -67,7 +67,7 @@ pub(crate) fn decompose_path(path: String) -> Result { // Analyse tail of the path to find name and type values let tmp = elem.pop().unwrap(); let (tpe, name) = if tmp.eq(TPE_CONFIG) { - ap.path_type = ArchivePathEnum::CONFIG; + ap.path_type = ArchivePathEnum::Config; tracing::debug!("[decompose_path] ends with config"); if length > 1 { let tpe = elem.pop().unwrap(); @@ -90,13 +90,13 @@ pub(crate) fn decompose_path(path: String) -> Result { ap.path_type = get_path_type(&tpe); (tpe, tmp) // path = /:path/:tpe/:name } else { - ap.path_type = ArchivePathEnum::NONE; + ap.path_type = ArchivePathEnum::None; elem.push(tpe); elem.push(tmp); ("".to_string(), "".to_string()) // path = /:path --> with length (>1) } } else { - ap.path_type = ArchivePathEnum::NONE; + ap.path_type = ArchivePathEnum::None; elem.push(tmp); ("".to_string(), "".to_string()) // path = /:path --> with length (1) }; @@ -112,22 +112,22 @@ pub(crate) fn decompose_path(path: String) -> Result { fn get_path_type(s: &str) -> ArchivePathEnum { match s { - TPE_CONFIG => ArchivePathEnum::CONFIG, - TPE_DATA => ArchivePathEnum::DATA, - TPE_KEYS => ArchivePathEnum::KEYS, - TPE_LOCKS => ArchivePathEnum::LOCKS, - TPE_SNAPSHOTS => ArchivePathEnum::SNAPSHOTS, - TPE_INDEX => ArchivePathEnum::INDEX, - _ => ArchivePathEnum::NONE, + TPE_CONFIG => ArchivePathEnum::Config, + TPE_DATA => ArchivePathEnum::Data, + TPE_KEYS => ArchivePathEnum::Keys, + TPE_LOCKS => ArchivePathEnum::Locks, + TPE_SNAPSHOTS => ArchivePathEnum::Snapshots, + TPE_INDEX => ArchivePathEnum::Index, + _ => ArchivePathEnum::None, } } #[cfg(test)] mod test { use crate::error::Result; - use crate::handlers::path_analysis::ArchivePathEnum::CONFIG; + use crate::handlers::path_analysis::ArchivePathEnum::Config; use crate::handlers::path_analysis::{decompose_path, TPE_DATA, TPE_LOCKS}; - use crate::log::init_tracing; + use crate::test_helpers::init_tracing; #[test] fn archive_path_struct() -> Result<()> { @@ -159,21 +159,21 @@ mod test { let path = "/a/b/data/config".to_string(); let ap = decompose_path(path)?; - assert_eq!(ap.path_type, CONFIG); + assert_eq!(ap.path_type, Config); assert_eq!(ap.tpe, TPE_DATA); assert_eq!(ap.name, "config".to_string()); assert_eq!(ap.path, "a/b"); let path = "/a/b/config".to_string(); let ap = decompose_path(path)?; - assert_eq!(ap.path_type, CONFIG); + assert_eq!(ap.path_type, Config); assert_eq!(ap.tpe, "".to_string()); assert_eq!(ap.name, "config".to_string()); assert_eq!(ap.path, "a/b"); let path = "/config".to_string(); let ap = decompose_path(path)?; - assert_eq!(ap.path_type, CONFIG); + assert_eq!(ap.path_type, Config); assert_eq!(ap.tpe, "".to_string()); assert_eq!(ap.name, "config".to_string()); assert_eq!(ap.path, ""); diff --git a/src/handlers/repository.rs b/src/handlers/repository.rs index d45545a..e99b120 100644 --- a/src/handlers/repository.rs +++ b/src/handlers/repository.rs @@ -9,10 +9,8 @@ use axum::{extract::Path as PathExtract, http::StatusCode, response::IntoRespons use serde_derive::Deserialize; use std::path::Path; -//============================================================================== -// Create_repository -// Interface: POST {path}?create=true -//============================================================================== +/// Create_repository +/// Interface: POST {path}?create=true #[derive(Default, Deserialize)] #[serde(default)] pub(crate) struct Create { @@ -29,7 +27,7 @@ pub(crate) async fn create_repository( let archive_path = decompose_path(path_string)?; let p_str = archive_path.path; let tpe = archive_path.tpe; - assert_eq!(&archive_path.path_type, &ArchivePathEnum::NONE); + assert_eq!(&archive_path.path_type, &ArchivePathEnum::None); assert_eq!(&tpe, ""); tracing::debug!("[create_repository] repo_path: {p_str:?}"); @@ -58,12 +56,9 @@ pub(crate) async fn create_repository( } } -//============================================================================== -// Delete_repository -// Interface: Delete {path} -//============================================================================== - -// FIXME: The input path should at least NOT point to a file in any repository +/// Delete_repository +/// Interface: Delete {path} +/// FIXME: The input path should at least NOT point to a file in any repository pub(crate) async fn delete_repository( auth: AuthFromRequest, path: Option>, @@ -72,7 +67,7 @@ pub(crate) async fn delete_repository( let archive_path = decompose_path(path_string)?; let p_str = archive_path.path; let tpe = archive_path.tpe; - assert_eq!(archive_path.path_type, ArchivePathEnum::NONE); + assert_eq!(archive_path.path_type, ArchivePathEnum::None); assert_eq!(&tpe, ""); tracing::debug!("[delete_repository] repo_path: {p_str:?}"); @@ -94,7 +89,9 @@ pub(crate) async fn delete_repository( #[cfg(test)] mod test { use crate::handlers::repository::{create_repository, delete_repository}; - use crate::test_server::{basic_auth, init_test_environment, print_request_response}; + use crate::test_helpers::{ + basic_auth_header_value, init_test_environment, print_request_response, + }; use axum::http::Method; use axum::routing::post; use axum::{ @@ -104,7 +101,7 @@ mod test { use axum::{middleware, Router}; use std::path::PathBuf; use std::{env, fs}; - use tower::ServiceExt; // for `call`, `oneshot`, and `ready` + use tower::ServiceExt; /// The acl.toml test allows the create of "repo_remove_me" /// for user test with the correct password @@ -146,7 +143,10 @@ mod test { let request = Request::builder() .uri(&repo_name_uri) .method(Method::POST) - .header("Authorization", basic_auth("test", Some("test_pw"))) + .header( + "Authorization", + basic_auth_header_value("test", Some("test_pw")), + ) .body(Body::empty()) .unwrap(); @@ -166,7 +166,10 @@ mod test { let request = Request::builder() .uri(&repo_name_uri) .method(Method::POST) - .header("Authorization", basic_auth("test", Some("test_pw"))) + .header( + "Authorization", + basic_auth_header_value("test", Some("test_pw")), + ) .body(Body::empty()) .unwrap(); @@ -188,7 +191,7 @@ mod test { .method(Method::POST) .header( "Authorization", - basic_auth("test", Some("__wrong_password__")), + basic_auth_header_value("test", Some("__wrong_password__")), ) .body(Body::empty()) .unwrap(); @@ -210,7 +213,10 @@ mod test { let request = Request::builder() .uri(&repo_name_uri) .method(Method::POST) - .header("Authorization", basic_auth("test", Some("test_pw"))) + .header( + "Authorization", + basic_auth_header_value("test", Some("test_pw")), + ) .body(Body::empty()) .unwrap(); diff --git a/src/lib.rs b/src/lib.rs index 1003fb2..1c394ec 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,6 @@ -use clap::Parser; -use std::path::PathBuf; - pub mod acl; pub mod auth; +pub mod commands; pub mod config; pub mod error; pub mod handlers; @@ -11,41 +9,4 @@ pub mod storage; pub mod web; #[cfg(test)] -pub mod test_server; - -/// A REST server build in rust for use with restic -#[derive(Parser)] -#[command(name = "rustic-server")] -#[command(bin_name = "rustic-server")] -pub struct Opts { - /// listen address - #[arg(short, long, default_value = "localhost:8000")] - pub listen: String, - /// data directory - #[arg(short, long, default_value = "/tmp/restic")] - pub path: PathBuf, - /// disable .htpasswd authentication - #[arg(long)] - pub no_auth: bool, - /// file to read per-repo ACLs from - #[arg(long)] - pub acl: Option, - /// set standard acl to append only mode - #[arg(long)] - pub append_only: bool, - /// set standard acl to only access private repos - #[arg(long)] - pub private_repo: bool, - /// turn on TLS support - #[arg(long)] - pub tls: bool, - /// TLS certificate path - #[arg(long)] - pub cert: Option, - /// TLS key path - #[arg(long)] - pub key: Option, - /// logging level (Off/Error/Warn/Info/Debug/Trace) - #[arg(long, default_value = "Info")] - pub log: String, -} +pub mod test_helpers; diff --git a/src/log.rs b/src/log.rs index 6f2d93d..e973bac 100644 --- a/src/log.rs +++ b/src/log.rs @@ -1,25 +1,11 @@ -use once_cell::sync::OnceCell; -use std::sync::Mutex; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; -//FIXME: The MUTEX is only here for the test environment. --> move to test code -// , and execute without mutex here - -//When starting a server we fetch the mutex to force serial testing -pub(crate) static TRACER: OnceCell> = OnceCell::new(); -pub(crate) fn init_mutex() { - TRACER.get_or_init(|| { - tracing_subscriber::registry() - .with( - tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| "RUSTIC_SERVER_LOG_LEVEL=debug".into()), - ) - .with(tracing_subscriber::fmt::layer()) - .init(); - Mutex::new(0) - }); -} - pub fn init_tracing() { - init_mutex(); + tracing_subscriber::registry() + .with( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| "RUSTIC_SERVER_LOG_LEVEL=debug".into()), + ) + .with(tracing_subscriber::fmt::layer()) + .init(); } diff --git a/src/storage.rs b/src/storage.rs index 082b832..412f163 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -12,9 +12,9 @@ use walkdir::WalkDir; //Static storage of our credentials pub static STORAGE: OnceCell> = OnceCell::new(); -pub(crate) fn init_storage(state: impl Storage) -> Result<(), ErrorKind> { +pub(crate) fn init_storage(storage: impl Storage) -> Result<(), ErrorKind> { if STORAGE.get().is_none() { - let storage = Arc::new(state); + let storage = Arc::new(storage); match STORAGE.set(storage) { Ok(_) => {} Err(_) => { diff --git a/src/test_helpers.rs b/src/test_helpers.rs new file mode 100644 index 0000000..b6d4a55 --- /dev/null +++ b/src/test_helpers.rs @@ -0,0 +1,138 @@ +use crate::acl::{init_acl, Acl}; +use crate::auth::{init_auth, Auth}; +use crate::error::ErrorKind; +use crate::storage::{init_storage, LocalStorage}; +use axum::body::{Body, Bytes}; +use axum::extract::Request; +use axum::http::HeaderValue; +use axum::middleware::Next; +use axum::response::{IntoResponse, Response}; +use http_body_util::BodyExt; +use once_cell::sync::OnceCell; +use std::env; +use std::path::PathBuf; +use std::sync::Mutex; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + +pub fn test_init_static_htaccess() { + let cwd = env::current_dir().unwrap(); + let htaccess = PathBuf::new().join(cwd).join("test_data").join("htaccess"); + + let auth = Auth::from_file(false, &htaccess).unwrap(); + init_auth(auth).unwrap(); +} + +pub fn test_init_static_auth() { + let cwd = env::current_dir().unwrap(); + let acl_path = PathBuf::new().join(cwd).join("test_data").join("acl.toml"); + + let acl = Acl::from_file(false, true, Some(acl_path)).unwrap(); + init_acl(acl).unwrap(); +} + +pub fn test_init_static_storage() { + let cwd = env::current_dir().unwrap(); + let repo_path = PathBuf::new() + .join(cwd) + .join("test_data") + .join("test_repos"); + + let local_storage = LocalStorage::try_new(&repo_path).unwrap(); + init_storage(local_storage).unwrap(); +} + +/// When we initialise the global tracing subscriber, this must only happen once. +/// During tests, each test will initialise, to make sure we have at least tracing once. +/// This means that the init() call must be robust for this. +/// Since we do not need this in production code, it is located in the test code. +static TRACER: OnceCell> = OnceCell::new(); +pub(crate) fn init_mutex() { + TRACER.get_or_init(|| { + tracing_subscriber::registry() + .with( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| "RUSTIC_SERVER_LOG_LEVEL=debug".into()), + ) + .with(tracing_subscriber::fmt::layer()) + .init(); + Mutex::new(0) + }); +} + +pub fn init_tracing() { + init_mutex(); +} + +pub fn init_test_environment() { + init_mutex(); + + test_init_static_htaccess(); + test_init_static_auth(); + test_init_static_storage(); +} + +pub fn basic_auth_header_value(username: U, password: Option

) -> HeaderValue +where + U: std::fmt::Display, + P: std::fmt::Display, +{ + use base64::prelude::BASE64_STANDARD; + use base64::write::EncoderWriter; + use std::io::Write; + + let mut buf = b"Basic ".to_vec(); + { + let mut encoder = EncoderWriter::new(&mut buf, &BASE64_STANDARD); + let _ = write!(encoder, "{}:", username); + if let Some(password) = password { + let _ = write!(encoder, "{}", password); + } + } + let mut header = HeaderValue::from_bytes(&buf).expect("base64 is always valid HeaderValue"); + header.set_sensitive(true); + header +} + +pub async fn print_request_response( + req: Request, + next: Next, +) -> Result { + let (parts, body) = req.into_parts(); + for (k, v) in parts.headers.iter() { + tracing::debug!("request-header: {k:?} -> {v:?} "); + } + let bytes = buffer_and_print("request", body).await?; + let req = Request::from_parts(parts, Body::from(bytes)); + + let res = next.run(req).await; + + let (parts, body) = res.into_parts(); + for (k, v) in parts.headers.iter() { + tracing::debug!("reply-header: {k:?} -> {v:?} "); + } + let bytes = buffer_and_print("response", body).await?; + let res = Response::from_parts(parts, Body::from(bytes)); + + Ok(res) +} + +async fn buffer_and_print(direction: &str, body: B) -> Result +where + B: axum::body::HttpBody, + B::Error: std::fmt::Display, +{ + let bytes = match body.collect().await { + Ok(collected) => collected.to_bytes(), + Err(err) => { + return Err(ErrorKind::BadRequest(format!( + "failed to read {direction} body: {err}" + ))); + } + }; + + if let Ok(body) = std::str::from_utf8(&bytes) { + tracing::debug!("{direction} body = {body:?}"); + } + + Ok(bytes) +} diff --git a/src/test_server.rs b/src/test_server.rs deleted file mode 100644 index 39a59ac..0000000 --- a/src/test_server.rs +++ /dev/null @@ -1,194 +0,0 @@ -use crate::acl::{init_acl, Acl}; -use crate::auth::init_auth; -use crate::auth::Auth; -use crate::error::ErrorKind; -use crate::log::init_tracing; -use crate::storage::{init_storage, LocalStorage}; -use axum::http::HeaderValue; -/// FIXME: Should we keep the server to allow a test to run in the test folder -/// For example using rustic to fill a backup over this web server to localhost?? -use axum::{ - body::{Body, Bytes}, - extract::Request, - middleware::Next, - response::{IntoResponse, Response}, - routing::post, - Router, ServiceExt, -}; -use http_body_util::BodyExt; -use once_cell::sync::OnceCell; -use std::env; -use std::net::SocketAddr; -use std::path::PathBuf; -use std::sync::{Arc, Mutex}; -use tokio::net::TcpListener; -use tokio::sync::oneshot::{Receiver, Sender}; - -pub(crate) const WAIT_DELAY: u64 = 250; //Delay in ms to wait for server start - -//When starting a server we fetch the mutex to force serial testing -pub(crate) static WEB: OnceCell>> = OnceCell::new(); -pub(crate) fn init_mutex() { - WEB.get_or_init(|| Arc::new(Mutex::new(0))); -} - -pub struct TestServer { - tx: Option>, - router: Router, -} - -impl TestServer { - pub fn new(router: Router) -> Self { - TestServer { tx: None, router } - } - - pub async fn stop_server(self) { - if self.tx.is_some() { - self.tx - .unwrap() - .send(()) - .expect("Failed to stop the test server"); - } - tokio::time::sleep(tokio::time::Duration::from_millis(WAIT_DELAY)).await; - } - - pub async fn launch(&mut self) { - init_mutex(); - let _r = WEB.get().take().unwrap(); - - let routes = self.router.clone(); - let (tx, rx) = tokio::sync::oneshot::channel::<()>(); - self.tx = Some(tx); - tokio::task::spawn(TestServer::launcher(rx, routes)); - tokio::time::sleep(tokio::time::Duration::from_millis(WAIT_DELAY)).await; - } - - /// Launches spin-off axum instance - pub async fn launcher(rx: Receiver<()>, app: Router) { - //FIXME: Is this the best place for this during test? - init_tracing(); - - TestServer::test_init_static_htaccess(); - TestServer::test_init_static_auth(); - TestServer::test_init_static_storage(); - - // Launch - let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); - - axum::serve( - TcpListener::bind(addr).await.unwrap(), - app.into_make_service(), - ) - .with_graceful_shutdown(async { - rx.await.ok(); - }) - .await - .unwrap(); - } - - pub fn url(path: &str) -> String { - format!("http://127.0.0.1:3000{}", path) - } - - pub fn test_init_static_htaccess() { - let cwd = env::current_dir().unwrap(); - let htaccess = PathBuf::new().join(cwd).join("test_data").join("htaccess"); - - let auth = Auth::from_file(false, &htaccess).unwrap(); - init_auth(auth).unwrap(); - } - - pub fn test_init_static_auth() { - let cwd = env::current_dir().unwrap(); - let acl_path = PathBuf::new().join(cwd).join("test_data").join("acl.toml"); - - let acl = Acl::from_file(false, true, Some(acl_path)).unwrap(); - init_acl(acl).unwrap(); - } - - pub fn test_init_static_storage() { - let cwd = env::current_dir().unwrap(); - let repo_path = PathBuf::new() - .join(cwd) - .join("test_data") - .join("test_repos"); - - let local_storage = LocalStorage::try_new(&repo_path).unwrap(); - init_storage(local_storage).unwrap(); - } -} - -pub fn init_test_environment() { - //FIXME: Is this the best place for this during test? - init_tracing(); - - TestServer::test_init_static_htaccess(); - TestServer::test_init_static_auth(); - TestServer::test_init_static_storage(); -} - -pub fn basic_auth(username: U, password: Option

) -> HeaderValue -where - U: std::fmt::Display, - P: std::fmt::Display, -{ - use base64::prelude::BASE64_STANDARD; - use base64::write::EncoderWriter; - use std::io::Write; - - let mut buf = b"Basic ".to_vec(); - { - let mut encoder = EncoderWriter::new(&mut buf, &BASE64_STANDARD); - let _ = write!(encoder, "{}:", username); - if let Some(password) = password { - let _ = write!(encoder, "{}", password); - } - } - let mut header = HeaderValue::from_bytes(&buf).expect("base64 is always valid HeaderValue"); - header.set_sensitive(true); - header -} - -pub async fn print_request_response( - req: Request, - next: Next, -) -> Result { - let (parts, body) = req.into_parts(); - for (k, v) in parts.headers.iter() { - tracing::debug!("request-header: {k:?} -> {v:?} "); - } - let bytes = buffer_and_print("request", body).await?; - let req = Request::from_parts(parts, Body::from(bytes)); - - let res = next.run(req).await; - - let (parts, body) = res.into_parts(); - for (k, v) in parts.headers.iter() { - tracing::debug!("reply-header: {k:?} -> {v:?} "); - } - let bytes = buffer_and_print("response", body).await?; - let res = Response::from_parts(parts, Body::from(bytes)); - - Ok(res) -} - -async fn buffer_and_print(direction: &str, body: B) -> Result -where - B: axum::body::HttpBody, - B::Error: std::fmt::Display, -{ - let bytes = match body.collect().await { - Ok(collected) => collected.to_bytes(), - Err(err) => { - return Err(ErrorKind::BadRequest(format!( - "failed to read {direction} body: {err}" - ))); - } - }; - - if let Ok(body) = std::str::from_utf8(&bytes) { - tracing::debug!("{direction} body = {body:?}"); - } - - Ok(bytes) -} diff --git a/src/web.rs b/src/web.rs index 52650bc..9cf61eb 100644 --- a/src/web.rs +++ b/src/web.rs @@ -8,7 +8,7 @@ // auth - for user authentication // acl - for access control -use axum::routing::{delete, get, head, post}; +use axum::routing::{get, head, post}; use axum::Router; use axum_server::tls_rustls::RustlsConfig; use std::net::SocketAddr; @@ -26,7 +26,7 @@ use crate::storage::init_storage; use crate::{error::Result, storage::Storage}; /// FIXME: Routes are checked in order of adding them to the Router (right?) -pub async fn web_browser( +pub async fn start_web_server( acl: Acl, auth: Auth, storage: impl Storage, diff --git a/tests/test_server.rs b/tests/test_server.rs index 8b13789..b61989e 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -1 +1,93 @@ - +// use crate::acl::{init_acl, Acl}; +// use crate::auth::init_auth; +// use crate::auth::Auth; +// use crate::error::ErrorKind; +// use crate::log::init_tracing; +// use crate::storage::{init_storage, LocalStorage}; +// use axum::http::HeaderValue; +// /// FIXME: Should we keep the server to allow a test to run in the test folder +// /// For example using rustic to fill a backup over this web server to localhost?? +// use axum::{ +// body::{Body, Bytes}, +// extract::Request, +// middleware::Next, +// response::{IntoResponse, Response}, +// routing::post, +// Router, ServiceExt, +// }; +// use http_body_util::BodyExt; +// use once_cell::sync::OnceCell; +// use std::env; +// use std::net::SocketAddr; +// use std::path::PathBuf; +// use std::sync::{Arc, Mutex}; +// use tokio::net::TcpListener; +// use tokio::sync::oneshot::{Receiver, Sender}; +// use rustic_server::log::init_tracing; +// +// pub(crate) const WAIT_DELAY: u64 = 250; //Delay in ms to wait for server start +// +// //When starting a server we fetch the mutex to force serial testing +// pub(crate) static WEB: OnceCell>> = OnceCell::new(); +// pub(crate) fn init_mutex() { +// WEB.get_or_init(|| Arc::new(Mutex::new(0))); +// } +// +// pub struct TestServer { +// tx: Option>, +// router: Router, +// } +// +// impl TestServer { +// pub fn new(router: Router) -> Self { +// TestServer { tx: None, router } +// } +// +// pub async fn stop_server(self) { +// if self.tx.is_some() { +// self.tx +// .unwrap() +// .send(()) +// .expect("Failed to stop the test server"); +// } +// tokio::time::sleep(tokio::time::Duration::from_millis(WAIT_DELAY)).await; +// } +// +// pub async fn launch(&mut self) { +// init_mutex(); +// let _r = WEB.get().take().unwrap(); +// +// let routes = self.router.clone(); +// let (tx, rx) = tokio::sync::oneshot::channel::<()>(); +// self.tx = Some(tx); +// tokio::task::spawn(TestServer::launcher(rx, routes)); +// tokio::time::sleep(tokio::time::Duration::from_millis(WAIT_DELAY)).await; +// } +// +// /// Launches spin-off axum instance +// pub async fn launcher(rx: Receiver<()>, app: Router) { +// //FIXME: Is this the best place for this during test? +// init_tracing(); +// +// TestServer::test_init_static_htaccess(); +// TestServer::test_init_static_auth(); +// TestServer::test_init_static_storage(); +// +// // Launch +// let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); +// +// axum::serve( +// TcpListener::bind(addr).await.unwrap(), +// app.into_make_service(), +// ) +// .with_graceful_shutdown(async { +// rx.await.ok(); +// }) +// .await +// .unwrap(); +// } +// +// pub fn url(path: &str) -> String { +// format!("http://127.0.0.1:3000{}", path) +// } +//