From 22a6c8818b153d6fe6129bd843c36b96c766191d Mon Sep 17 00:00:00 2001 From: Kyattsukuro Date: Sun, 17 Aug 2025 20:26:16 +0200 Subject: [PATCH] refactored configuration, implemented basic request --- .vscode/launch.json | 45 ++++++++++++ Cargo.lock | 1 + Cargo.toml | 3 +- shell.nix | 6 +- src/api/guards.rs | 54 +++++++-------- src/api/mod.rs | 53 +++++++++++++- src/api/request.rs | 32 +++++++-- src/app_config.rs | 115 ------------------------------- src/app_config/general_config.rs | 20 ++++++ src/app_config/mod.rs | 100 +++++++++++++++++++++++++++ src/app_config/providers.rs | 32 +++++++++ src/errors.rs | 2 +- src/services/providers.rs | 67 ++++++++++++++---- 13 files changed, 360 insertions(+), 170 deletions(-) create mode 100644 .vscode/launch.json delete mode 100644 src/app_config.rs create mode 100644 src/app_config/general_config.rs create mode 100644 src/app_config/mod.rs create mode 100644 src/app_config/providers.rs diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..07dd6ed --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,45 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'auto-decrypt'", + "cargo": { + "args": [ + "build", + "--bin=auto-decrypt", + "--package=auto-decrypt" + ], + "filter": { + "name": "auto-decrypt", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'auto-decrypt'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=auto-decrypt", + "--package=auto-decrypt" + ], + "filter": { + "name": "auto-decrypt", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index fb2beae..2ca2c08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -227,6 +227,7 @@ dependencies = [ "clap", "custom_error", "diesel", + "enum_dispatch", "log", "reqwest", "rocket", diff --git a/Cargo.toml b/Cargo.toml index 85bb81d..2090296 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,4 +14,5 @@ log = "0.4.27" diesel = { version = "2.2.0", features = ["sqlite", "returning_clauses_for_sqlite_3_35"] } rocket = "0.5.1" url = "2" -custom_error = "1.9.2" \ No newline at end of file +custom_error = "1.9.2" +enum_dispatch = "0.3.13" \ No newline at end of file diff --git a/shell.nix b/shell.nix index e2d8c2f..5083e83 100644 --- a/shell.nix +++ b/shell.nix @@ -10,10 +10,12 @@ let in nixpkgs.mkShell { buildInputs = [ - (nixpkgs.rustChannelOf { + ((nixpkgs.rustChannelOf { channel = "stable"; date = null; # null means latest - }).rust + }).rust.override { + extensions = [ "rust-src" "rust-analysis" ]; + }) nixpkgs.openssl nixpkgs.pkg-config nixpkgs.sqlite diff --git a/src/api/guards.rs b/src/api/guards.rs index 5687334..2e9d989 100644 --- a/src/api/guards.rs +++ b/src/api/guards.rs @@ -1,3 +1,4 @@ +use clap::Error; use rocket::form::Strict; use rocket::request::{Outcome, Request, FromRequest}; use url::form_urlencoded; @@ -7,10 +8,7 @@ use crate::api::State; use rocket::http::uri::Query; use crate::services::providers::Providers; - -pub(super) struct APIProviderRequest<'r> { - pub share: &'r Providers, -} +use super::HttpResult; fn get_query_value(query: &Option, query_key: &str) -> Option { @@ -25,36 +23,38 @@ fn get_query_value(query: &Option, query_key: &str) -> Option { None } + +pub(super) struct APIProviderRequest<'r> { + pub share: &'r Providers, +} + #[rocket::async_trait] impl<'r> FromRequest<'r> for APIProviderRequest<'r> { - type Error = (); + type Error = HttpResult; async fn from_request(req: &'r Request<'_>) -> Outcome { - //let db_conn = req.guard::<&State>().await.unwrap(); - let path = req - .uri() - .path() - .segments(); + let path = req.uri().path().segments(); let share_name: &str = path.get(path.len() - 1).unwrap_or(&""); - let access_key: &str = &get_query_value(&req.uri().query(), "trigger_key").unwrap_or("".to_string()); - if share_name == "" { - Outcome::Error((rocket::http::Status::BadRequest, ())) - } else { - let share: Option<&Providers> = CONFIG.wait().providers.iter() - .find(|service| service.name == share_name) - .and_then(|service| { - if service.access_key == access_key { - Some(service) - } else { - None - } - }); - if share.is_none() { - return Outcome::Error((rocket::http::Status::NotFound, ())) - } + let access_key: &str = &get_query_value(&req.uri().query(), "trigger_key") + .unwrap_or_default(); - return Outcome::Success(APIProviderRequest { share: share.unwrap() }) + if share_name.is_empty() { + return Outcome::Error(HttpResult::ShareNotFound("No share name provided.".to_string()).into()); + } + + if let Some(service) = CONFIG + .wait() + .providers + .iter() + .find(|(name, service)| *name == share_name && service.access_key_hash == access_key) + { + Outcome::Success(APIProviderRequest { share: service.1 }) + } else { + Outcome::Error(HttpResult::ShareNotFound(format!( + "Share '{}' not found.", + share_name + )).into()) } } } \ No newline at end of file diff --git a/src/api/mod.rs b/src/api/mod.rs index d3c46dd..3a07dff 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -4,21 +4,68 @@ mod request; use crate::app_config::CONFIG; use crate::orm::DbConn; // Alternativly iport rocket manually: use rocket::get; -use rocket::fs::FileServer; +use rocket::{fs::FileServer, Config}; use rocket::response::Redirect; use rocket::fs::NamedFile; use rocket::State; +use serde::Deserialize; +use core::marker::{Send, Sync}; +use std::net::IpAddr; +use rocket::http::Status; +use rocket::Request; + + + +#[derive(Responder, Debug)] +pub(super) enum HttpResult { + UnlockingSucceeded(String), + AckAuthReq(String), + AuthFailiure(String), + ShareNotFound(String), + TimelimitExceeded(String), + TooManyRequests(String), + UnlockingFailed(String), +} + +impl Into<(Status, HttpResult)> for HttpResult { + fn into(self) -> (Status, HttpResult) { + let status = match &self { + HttpResult::UnlockingSucceeded(_) => Status::Ok, + HttpResult::AckAuthReq(_) => Status::Accepted, + HttpResult::AuthFailiure(_) => Status::Unauthorized, + HttpResult::ShareNotFound(_) => Status::NotFound, + HttpResult::TimelimitExceeded(_) => Status::RequestTimeout, + HttpResult::TooManyRequests(_) => Status::TooManyRequests, + HttpResult::UnlockingFailed(_) => Status::InternalServerError, + }; + (status, self) + } +} #[get("/", rank = 1)] async fn index() -> String { - "Welcome to the Auto-Decrypt API!".to_string() + "Auto-Decrypt API! DEV!".to_string() } + +#[catch(default)] +fn default(_status: Status, req: &Request) -> String { + format!("{:?}", req) +} + + #[rocket::main] pub(crate) async fn start_api() -> () { + let rocket_config = Config{ + port: CONFIG.wait().port, + ..Default::default()}; + let _ = rocket::build() - .manage(DbConn::establish_connection(&CONFIG.wait().db_file)) // Manage the state here + .manage(DbConn::establish_connection(&CONFIG.wait().db_file)) .mount("/", routes![index]) + .register("/", catchers![default]) + .mount("/request", routes![request::request_handler]) + .configure(rocket_config) .launch() .await; } \ No newline at end of file diff --git a/src/api/request.rs b/src/api/request.rs index 4a8d23c..e0d605f 100644 --- a/src/api/request.rs +++ b/src/api/request.rs @@ -1,25 +1,43 @@ use super::guards::APIProviderRequest; -use crate::services::providers::ConsentMethode; +use crate::api::guards; +use crate::services::providers::{ConsentMethode, ProviderAction, Providers}; use crate::app_config::CONFIG; use rocket::get; +use rocket::http::Status; +use super::HttpResult; + + +async fn execute_action(action: &Providers) -> Result { + let action: &dyn ProviderAction = &action.execution_action; + action.execute().await +} #[get("/<_>", rank = 1)] -async fn request_handler(guard: APIProviderRequest<'_>) -> String { +pub(super) async fn request_handler(guard: Result, HttpResult>) -> (Status, HttpResult) { // We assume the request guard verified th access_key against the service_name + if let Err(err) = guard { + // This wrapper is nessesary to avoid needing to write catchers, and recompute the error + // See: https://github.com/rwf2/Rocket/issues/749 + return err.into(); + } + let guard = guard.expect("A rejected request guard was able to perseed!"); - match guard.share.consent_methode { + let result: HttpResult = match guard.share.consent_methode { ConsentMethode::None => { // We do not wait for constent, direct feedback is impleied - todo!() + match execute_action(guard.share).await { + Ok(response) => HttpResult::UnlockingSucceeded(response), + Err(err) => HttpResult::UnlockingFailed(err), + } }, ConsentMethode::Boolean => { todo!() }, - ConsentMethode::Passkey(_) => { + ConsentMethode::PassheyHash(_) => { todo!() }, - } - + }; + return result.into(); } \ No newline at end of file diff --git a/src/app_config.rs b/src/app_config.rs deleted file mode 100644 index 83df1cf..0000000 --- a/src/app_config.rs +++ /dev/null @@ -1,115 +0,0 @@ -use std::sync::OnceLock; -use std::fs; -use std::path::Path; -use serde::{ser, Deserialize}; - -use crate::services::beggars::Beggars; -use crate::services::providers::Providers; -use crate::errors::AutoDecryptError; - -#[derive(Debug, Deserialize)] -enum Platform { - OMV, - TrueNas, -} - -#[derive(Debug, Deserialize)] -pub(crate) struct AppConfig { - pub(crate) port: u16, - pub(crate) addresses: Vec, - pub(crate) platform: Platform, - pub(crate) ssh_known_host_file: String, - - pub(crate) db_file: String, - - pub(crate) beggars: Vec, // We request to unlock owned services - pub(crate) providers: Vec, // We offer to unlock these services -} - -impl Default for AppConfig { - fn default() -> Self { - AppConfig { - port: 8080, - addresses: vec!["*".to_string()], - beggars: vec![], - providers: vec![], - platform: Platform::OMV, - ssh_known_host_file: "".to_string(), - db_file: "/var/auto-decrypt/db.sqlite".to_string(), - } - } -} - -impl AppConfig { - - fn load_provided_services(at_path: &Path) -> Result, AutoDecryptError> { - let mut provided_services = vec![]; - if let Ok(entries) = fs::read_dir(at_path) { - for entry in entries { - let entry = entry.map_err(|e| AutoDecryptError::ConfigurationErroro { comment: e.to_string() })?; - if entry.path().extension().and_then(|s| s.to_str()) == Some("toml") { - let service: Providers = toml::from_str(&fs::read_to_string(entry.path()).map_err(|e| AutoDecryptError::ConfigurationErroro { comment: e.to_string() })?) - .map_err(|e| AutoDecryptError::ConfigurationErroro { comment: e.to_string() })?; - provided_services.push(service); - } - } - } - Ok(provided_services) - } - - fn load_requesting_services(at_path: &Path) -> Result, AutoDecryptError> { - let mut requesting_services = vec![]; - if let Ok(entries) = fs::read_dir(at_path) { - for entry in entries { - let entry = entry.map_err(|e| AutoDecryptError::ConfigurationErroro { comment: e.to_string() })?; - if entry.path().extension().and_then(|s| s.to_str()) == Some("toml") { - let service: Beggars = toml::from_str(&fs::read_to_string(entry.path()).map_err(|e| AutoDecryptError::ConfigurationErroro { comment: e.to_string() })?) - .map_err(|e| AutoDecryptError::ConfigurationErroro { comment: e.to_string() })?; - requesting_services.push(service); - } - } - } - Ok(requesting_services) - } - - pub fn load_from_file(path: &Path ) -> Result { - if !path.is_dir(){ - return Err(AutoDecryptError::ConfigurationErroro { comment: ("The provided configuration path is not a Folder!".to_string()) }) - } - - let contents = fs::read_to_string(path.join("config.toml")) - .unwrap_or_else(|err| { - eprintln!("Failed to read config file: {err}. Using default configuration."); - return String::new(); // Return empty string to fall back to default - }); - - let mut basic_config = toml::from_str(&contents) - .unwrap_or_else(|err| { - eprintln!("Failed to parse config file: {err}. Using default configuration."); - Self::default() - }); - - - basic_config.beggars = Self::load_requesting_services(&path.join("requesting_services")) - .unwrap_or_else(|err| { - eprintln!("Failed to load requesting services: {err}. Using empty list."); - vec![] - }); - - basic_config.providers = Self::load_provided_services(&path.join("provided_services")) - .unwrap_or_else(|err| { - eprintln!("Failed to load provided services: {err}. Using empty list."); - vec![] - }); - - return Ok(basic_config); - } -} - -pub static CONFIG: OnceLock = OnceLock::new(); - -pub fn init_config(file: &str) { - let config = AppConfig::load_from_file(Path::new(file)) - .expect("Failed to load configuration file"); - CONFIG.set(config).expect("AppConfig can only be initialized once"); -} diff --git a/src/app_config/general_config.rs b/src/app_config/general_config.rs new file mode 100644 index 0000000..a70811e --- /dev/null +++ b/src/app_config/general_config.rs @@ -0,0 +1,20 @@ +use serde::Deserialize; +use std::collections::HashMap; +use crate::{app_config::providers::TomlProviders, services::beggars::Beggars}; + +#[derive(Debug, Deserialize)] +pub (super) struct TomlAppConfig { + pub (super) port: Option, + pub (super) addresses: Option>, + pub (super) platform: Option, + pub (super) ssh_known_host_file: Option, + pub (super) db_file: Option, + pub (super) beggars: Option>, + pub (super) providers: Option> +} + +#[derive(Debug, Deserialize)] +pub(crate) enum Platform { + OMV, + TrueNas, +} diff --git a/src/app_config/mod.rs b/src/app_config/mod.rs new file mode 100644 index 0000000..c5c62af --- /dev/null +++ b/src/app_config/mod.rs @@ -0,0 +1,100 @@ +mod general_config; +mod providers; + +use std::hash::Hash; +use std::{collections::HashMap, sync::OnceLock}; +use std::fs; +use std::path::Path; +use clap::builder::Str; +use serde::{ser, Deserialize}; + +use crate::services::beggars::Beggars; +use crate::services::providers::Providers; +use crate::errors::AutoDecryptError; + +use general_config::{Platform, TomlAppConfig}; + + + +#[derive(Debug, Deserialize)] +pub(crate) struct AppConfig { + pub(crate) port: u16, + pub(crate) addresses: Vec, + pub(crate) platform: Platform, + pub(crate) ssh_known_host_file: String, + + pub(crate) db_file: String, + + pub(crate) beggars: HashMap, // We request to unlock owned services + pub(crate) providers: HashMap, // We offer to unlock these services +} + +impl Default for AppConfig { + fn default() -> Self { + AppConfig { + port: 8080, + addresses: vec!["*".to_string()], + beggars: HashMap::new(), + providers: HashMap::new(), + platform: Platform::OMV, + ssh_known_host_file: "".to_string(), + db_file: "/var/auto-decrypt/db.sqlite".to_string(), + } + } +} + +impl AppConfig { + fn from_toml(toml_config: Option, base_config_dir: &Path) -> Self { + let default = AppConfig::default(); + if let Some(toml_config) = toml_config { + return AppConfig { + port: toml_config.port.unwrap_or(default.port), + addresses: toml_config.addresses.unwrap_or(default.addresses), + platform: toml_config.platform.unwrap_or(default.platform), + ssh_known_host_file: toml_config.ssh_known_host_file.unwrap_or(default.ssh_known_host_file), + db_file: toml_config.db_file.unwrap_or(default.db_file), + beggars: toml_config.beggars.unwrap_or(default.beggars), + providers: toml_config.providers.and_then(|providers| + Some(providers.into_iter().map(|(name, service)| + (name, service.to_internal(base_config_dir).expect("Err")) + ).collect::>()) + ).unwrap_or(default.providers), + }; + } + return default; + } +} + + +impl AppConfig { + + pub fn load_from_file(path: &Path ) -> Result { + if !path.is_dir(){ + return Err(AutoDecryptError::ConfigurationError { comment: ("The provided configuration path is not a Folder!".to_string()) }) + } + + let contents = fs::read_to_string(path.join("config.toml")) + .unwrap_or_else(|err| { + eprintln!("Failed to read config file: {err}. Using default configuration."); + return String::new(); // Return empty string to fall back to default + }); + + let basic_config = AppConfig::from_toml( + toml::from_str::(&contents) + .map_err(|err| { + eprintln!("Failed to parse config file: {err}. Using default configuration."); + err + }) + .ok() + , path); + return Ok(basic_config); + } +} + +pub static CONFIG: OnceLock = OnceLock::new(); + +pub fn init_config(file: &str) { + let config = AppConfig::load_from_file(Path::new(file)) + .expect("Failed to load configuration file"); + CONFIG.set(config).expect("AppConfig can only be initialized once"); +} diff --git a/src/app_config/providers.rs b/src/app_config/providers.rs new file mode 100644 index 0000000..013e4e7 --- /dev/null +++ b/src/app_config/providers.rs @@ -0,0 +1,32 @@ +use std::path::Path; + +use serde::Deserialize; +use crate::errors::AutoDecryptError; +use crate::services::providers::ConsentMethode; +use crate::services::providers::ActionType; +use crate::services::providers::Providers; + +#[derive(Debug, Deserialize)] +pub (super) struct TomlProviders { + pub(crate) access_key_hash: Option, + pub(crate) execution_action: ActionType, + pub(crate) consent_methode: Option, +} + + + +impl TomlProviders { + pub(super) fn to_internal(mut self, base_config_dir: &Path) -> Result { + if let ActionType::EncryptedRequest(ref mut action_type) = self.execution_action { + if action_type.content.is_none() { + action_type.load_from_file(base_config_dir)?; + } + } + + Ok(Providers { + access_key_hash: self.access_key_hash.unwrap_or("".to_string()), + consent_methode: self.consent_methode.unwrap_or(ConsentMethode::None), + execution_action: self.execution_action, + }) + } +} diff --git a/src/errors.rs b/src/errors.rs index 578d278..58fb3ab 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,7 +1,7 @@ use custom_error::custom_error; custom_error!{pub AutoDecryptError - ConfigurationErroro{comment:String} = "{comment}", + ConfigurationError{comment:String} = "{comment}", APIError{comment:String} = "{comment}", ORMError{comment:String} = "{comment}", } \ No newline at end of file diff --git a/src/services/providers.rs b/src/services/providers.rs index 8198e03..eeffa2f 100644 --- a/src/services/providers.rs +++ b/src/services/providers.rs @@ -1,35 +1,46 @@ +use std::path::Path; + use async_ssh2_tokio::Config; use reqwest::Client; use reqwest::header::{HeaderName, HeaderValue}; use async_trait::async_trait; use async_ssh2_tokio::client::{self, AuthMethod, Client as SSHClient, ServerCheckMethod}; +use rocket::futures::TryFutureExt; +use rocket::tokio::fs; use serde::Deserialize; +use enum_dispatch::enum_dispatch; use crate::app_config::CONFIG; +use crate::errors::AutoDecryptError; #[derive(Debug, Deserialize)] pub(crate) enum ConsentMethode { None, Boolean, - Passkey(String), + PassheyHash(String), } #[derive(Debug, Deserialize)] +#[enum_dispatch(ProviderAction)] pub(crate) enum ActionType { - HTTPRequest(HTTPRequest), - SSHRequest(SSHRequest), + HTTPRequest(HTTPAction), + SSHRequest(SSHAction), + EncryptedRequest(EncryptedAction) } + + #[derive(Debug, Deserialize)] pub struct Providers { - pub(crate) name: String, - pub(crate) access_key: String, + pub(crate) access_key_hash: String, pub(crate) consent_methode: ConsentMethode, - pub(crate) execution_action: ActionType, + pub(crate) execution_action: ActionType, + // if action in seperate file and constent methode is passkey, we try + // to decrypt the action file with the passkey } #[derive(Debug, Deserialize)] -struct HTTPRequest { +struct HTTPAction { pub(crate) method: String, pub(crate) uri: String, pub(crate) headers: Option>, @@ -37,7 +48,7 @@ struct HTTPRequest { } #[async_trait] -impl ServiceAction for HTTPRequest { +impl ProviderAction for HTTPAction { async fn execute(&self) -> Result { let client = Client::new(); let mut req_builder = client.request( @@ -55,12 +66,12 @@ impl ServiceAction for HTTPRequest { } let response = req_builder.send().await .map_err(|e| e.to_string())?; - response.text().await.map_err(|e| e.to_string()) + Ok(format!("Success! Server replied with: {}", response.text().await.map_err(|e| e.to_string())?)) } } #[derive(Debug, Deserialize)] -struct SSHRequest { +struct SSHAction { pub(crate) uri: String, pub(crate) port: u16, pub(crate) username: String, @@ -69,7 +80,7 @@ struct SSHRequest { } #[async_trait] -impl ServiceAction for SSHRequest { +impl ProviderAction for SSHAction { async fn execute(&self) -> Result { let auth_method = AuthMethod::PrivateKey { key_data: self.private_key.clone(), key_pass: None }; let client = SSHClient::connect( @@ -88,7 +99,35 @@ impl ServiceAction for SSHRequest { } } -#[async_trait] -trait ServiceAction: std::fmt::Debug + Send + Sync { - async fn execute(&self) -> Result; +#[derive(Debug, Deserialize)] +pub struct EncryptedAction{ + pub(crate) filename: String, + pub(crate) content: Option +} +impl ProviderAction for EncryptedAction {} +impl EncryptedAction { + pub(crate) fn load_from_file(&mut self, find_in_dir: &Path) -> Result<&mut Self, AutoDecryptError> { + let content = std::fs::read_to_string(find_in_dir.join(&self.filename)) + .map_err(|err| AutoDecryptError::ConfigurationError { comment: err.to_string() })?; + self.content = Some(content); + Ok(self) + } + + + async fn to_executable_action(&self, key: &str) -> Result { + todo!() + //let cleartext_toml = + //1. get Cleartext toml 2. load tomel as any other 3. return object + } +} + + + + +#[async_trait] +#[enum_dispatch] +pub (crate) trait ProviderAction: std::fmt::Debug + Send + Sync { + async fn execute(&self) -> Result { + return Err("We are not directly executable!".to_string()) + } }