refactored configuration, implemented basic request

This commit is contained in:
Kyattsukuro 2025-08-17 20:26:16 +02:00
parent 8ee4fe14ff
commit 22a6c8818b
13 changed files with 360 additions and 170 deletions

45
.vscode/launch.json vendored Normal file
View File

@ -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}"
}
]
}

1
Cargo.lock generated
View File

@ -227,6 +227,7 @@ dependencies = [
"clap",
"custom_error",
"diesel",
"enum_dispatch",
"log",
"reqwest",
"rocket",

View File

@ -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"
custom_error = "1.9.2"
enum_dispatch = "0.3.13"

View File

@ -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

View File

@ -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>, query_key: &str) -> Option<String> {
@ -25,36 +23,38 @@ fn get_query_value(query: &Option<Query>, query_key: &str) -> Option<String> {
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<Self, Self::Error> {
//let db_conn = req.guard::<&State<DbConn>>().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())
}
}
}

View File

@ -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;
}

View File

@ -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<String, String> {
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<APIProviderRequest<'_>, 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();
}

View File

@ -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<String>,
pub(crate) platform: Platform,
pub(crate) ssh_known_host_file: String,
pub(crate) db_file: String,
pub(crate) beggars: Vec<Beggars>, // We request to unlock owned services
pub(crate) providers: Vec<Providers>, // 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<Vec<Providers>, 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<Vec<Beggars>, 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<Self, AutoDecryptError> {
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<AppConfig> = 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");
}

View File

@ -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<u16>,
pub (super) addresses: Option<Vec<String>>,
pub (super) platform: Option<Platform>,
pub (super) ssh_known_host_file: Option<String>,
pub (super) db_file: Option<String>,
pub (super) beggars: Option<HashMap<String, Beggars>>,
pub (super) providers: Option<HashMap<String, TomlProviders>>
}
#[derive(Debug, Deserialize)]
pub(crate) enum Platform {
OMV,
TrueNas,
}

100
src/app_config/mod.rs Normal file
View File

@ -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<String>,
pub(crate) platform: Platform,
pub(crate) ssh_known_host_file: String,
pub(crate) db_file: String,
pub(crate) beggars: HashMap<String, Beggars>, // We request to unlock owned services
pub(crate) providers: HashMap<String, Providers>, // 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<TomlAppConfig>, 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::<HashMap<String, Providers>>())
).unwrap_or(default.providers),
};
}
return default;
}
}
impl AppConfig {
pub fn load_from_file(path: &Path ) -> Result<Self, AutoDecryptError> {
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::<TomlAppConfig>(&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<AppConfig> = 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");
}

View File

@ -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<String>,
pub(crate) execution_action: ActionType,
pub(crate) consent_methode: Option<ConsentMethode>,
}
impl TomlProviders {
pub(super) fn to_internal(mut self, base_config_dir: &Path) -> Result<Providers, AutoDecryptError> {
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,
})
}
}

View File

@ -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}",
}

View File

@ -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<Vec<(String, String)>>,
@ -37,7 +48,7 @@ struct HTTPRequest {
}
#[async_trait]
impl ServiceAction for HTTPRequest {
impl ProviderAction for HTTPAction {
async fn execute(&self) -> Result<String, String> {
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<String, String> {
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<String, String>;
#[derive(Debug, Deserialize)]
pub struct EncryptedAction{
pub(crate) filename: String,
pub(crate) content: Option<String>
}
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<ActionType, AutoDecryptError> {
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<String, String> {
return Err("We are not directly executable!".to_string())
}
}