Added some documentation

This commit is contained in:
Thorsten Sommer 2024-11-05 21:38:39 +01:00
parent e6789dcfeb
commit c3039c2b5e
Signed by: tsommer
GPG Key ID: 371BBA77A02C0108
12 changed files with 66 additions and 10 deletions

View File

@ -5,6 +5,7 @@ use rocket::http::Status;
use rocket::Request;
use rocket::request::FromRequest;
/// The API token used to authenticate requests.
pub static API_TOKEN: Lazy<APIToken> = Lazy::new(|| {
let mut token = [0u8; 32];
let mut rng = rand_chacha::ChaChaRng::from_entropy();
@ -16,11 +17,13 @@ pub static API_TOKEN: Lazy<APIToken> = Lazy::new(|| {
token
});
/// The API token data structure used to authenticate requests.
pub struct APIToken {
hex_text: String,
}
impl APIToken {
/// Creates a new API token from a byte vector.
fn from_bytes(bytes: Vec<u8>) -> Self {
APIToken {
hex_text: bytes.iter().fold(String::new(), |mut result, byte| {
@ -30,6 +33,7 @@ impl APIToken {
}
}
/// Creates a new API token from a hexadecimal text.
fn from_hex_text(hex_text: &str) -> Self {
APIToken {
hex_text: hex_text.to_string(),
@ -40,17 +44,21 @@ impl APIToken {
self.hex_text.as_str()
}
/// Validates the received token against the valid token.
fn validate(&self, received_token: &Self) -> bool {
received_token.to_hex_text() == self.to_hex_text()
}
}
/// The request outcome type used to handle API token requests.
type RequestOutcome<R, T> = rocket::request::Outcome<R, T>;
/// The request outcome implementation for the API token.
#[rocket::async_trait]
impl<'r> FromRequest<'r> for APIToken {
type Error = APITokenError;
/// Handles the API token requests.
async fn from_request(request: &'r Request<'_>) -> RequestOutcome<Self, Self::Error> {
let token = request.headers().get_one("token");
match token {
@ -68,6 +76,7 @@ impl<'r> FromRequest<'r> for APIToken {
}
}
/// The API token error types.
#[derive(Debug)]
pub enum APITokenError {
Missing,

View File

@ -13,12 +13,13 @@ use crate::dotnet::stop_dotnet_server;
use crate::environment::{is_prod, CONFIG_DIRECTORY, DATA_DIRECTORY};
use crate::log::switch_to_file_logging;
// The Tauri main window.
/// The Tauri main window.
static MAIN_WINDOW: Lazy<Mutex<Option<Window>>> = Lazy::new(|| Mutex::new(None));
// The update response coming from the Tauri updater.
/// The update response coming from the Tauri updater.
static CHECK_UPDATE_RESPONSE: Lazy<Mutex<Option<UpdateResponse<tauri::Wry>>>> = Lazy::new(|| Mutex::new(None));
/// Starts the Tauri app.
pub fn start_tauri() {
info!("Starting Tauri app...");
let app = tauri::Builder::default()
@ -118,6 +119,7 @@ pub fn start_tauri() {
}
}
/// Changes the location of the main window to the given URL.
pub async fn change_location_to(url: &str) {
// Try to get the main window. If it is not available yet, wait for it:
let mut main_window_ready = false;
@ -149,6 +151,7 @@ pub async fn change_location_to(url: &str) {
}
}
/// Checks for updates.
#[get("/updates/check")]
pub async fn check_for_update(_token: APIToken) -> Json<CheckUpdateResponse> {
let app_handle = MAIN_WINDOW.lock().unwrap().as_ref().unwrap().app_handle();
@ -194,6 +197,7 @@ pub async fn check_for_update(_token: APIToken) -> Json<CheckUpdateResponse> {
}
}
/// The response to the check for update request.
#[derive(Serialize)]
pub struct CheckUpdateResponse {
update_is_available: bool,
@ -202,6 +206,7 @@ pub struct CheckUpdateResponse {
changelog: String,
}
/// Installs the update.
#[get("/updates/install")]
pub async fn install_update(_token: APIToken) {
let cloned_response_option = CHECK_UPDATE_RESPONSE.lock().unwrap().clone();

View File

@ -3,10 +3,16 @@ use log::info;
use rcgen::generate_simple_self_signed;
use sha2::{Sha256, Digest};
/// The certificate used for the runtime API server.
pub static CERTIFICATE: OnceLock<Vec<u8>> = OnceLock::new();
/// The private key used for the certificate of the runtime API server.
pub static CERTIFICATE_PRIVATE_KEY: OnceLock<Vec<u8>> = OnceLock::new();
/// The fingerprint of the certificate used for the runtime API server.
pub static CERTIFICATE_FINGERPRINT: OnceLock<String> = OnceLock::new();
/// Generates a TLS certificate for the runtime API server.
pub fn generate_certificate() {
info!("Try to generate a TLS certificate for the runtime API server...");

View File

@ -6,6 +6,7 @@ use serde::Serialize;
use crate::api_token::APIToken;
use crate::encryption::{EncryptedText, ENCRYPTION};
/// Sets the clipboard text to the provided encrypted text.
#[post("/clipboard/set", data = "<encrypted_text>")]
pub fn set_clipboard(_token: APIToken, encrypted_text: EncryptedText) -> Json<SetClipboardResponse> {
@ -53,6 +54,7 @@ pub fn set_clipboard(_token: APIToken, encrypted_text: EncryptedText) -> Json<Se
}
}
/// The response for setting the clipboard text.
#[derive(Serialize)]
pub struct SetClipboardResponse {
success: bool,

View File

@ -26,12 +26,15 @@ static DOTNET_SERVER_PORT: Lazy<u16> = Lazy::new(|| get_available_port().unwrap(
static DOTNET_INITIALIZED: Lazy<Mutex<bool>> = Lazy::new(|| Mutex::new(false));
/// Returns the desired port of the .NET server. Our .NET app calls this endpoint to get
/// the port where the .NET server should listen to.
#[get("/system/dotnet/port")]
pub fn dotnet_port(_token: APIToken) -> String {
let dotnet_server_port = *DOTNET_SERVER_PORT;
format!("{dotnet_server_port}")
}
/// Starts the .NET server in a separate process.
pub fn start_dotnet_server() {
// Get the secret password & salt and convert it to a base64 string:
@ -128,6 +131,7 @@ pub fn start_dotnet_server() {
});
}
/// This endpoint is called by the .NET server to signal that the server is ready.
#[get("/system/dotnet/ready")]
pub async fn dotnet_ready(_token: APIToken) {
@ -158,6 +162,7 @@ pub async fn dotnet_ready(_token: APIToken) {
change_location_to(url.as_str()).await;
}
/// Stops the .NET server process.
pub fn stop_dotnet_server() {
if let Some(server_process) = DOTNET_SERVER.lock().unwrap().take() {
let server_kill_result = server_process.kill();

View File

@ -21,6 +21,7 @@ type Aes256CbcDec = cbc::Decryptor<aes::Aes256>;
type DataOutcome<'r, T> = data::Outcome<'r, T>;
/// The encryption instance used for the IPC channel.
pub static ENCRYPTION: Lazy<Encryption> = Lazy::new(|| {
//
// Generate a secret key & salt for the AES encryption for the IPC channel:
@ -41,6 +42,7 @@ pub static ENCRYPTION: Lazy<Encryption> = Lazy::new(|| {
Encryption::new(&secret_key, &secret_key_salt).unwrap()
});
/// The encryption struct used for the IPC channel.
pub struct Encryption {
key: [u8; 32],
iv: [u8; 16],
@ -58,6 +60,7 @@ impl Encryption {
// algorithms we chose, requires a fixed key length, and our password is too long.
const ITERATIONS: u32 = 100;
/// Initializes the encryption with the given secret password and salt.
pub fn new(secret_password: &[u8], secret_key_salt: &[u8]) -> Result<Self, String> {
if secret_password.len() != 512 {
return Err("The secret password must be 512 bytes long.".to_string());
@ -92,6 +95,7 @@ impl Encryption {
Ok(encryption)
}
/// Encrypts the given data.
pub fn encrypt(&self, data: &str) -> Result<EncryptedText, String> {
let cipher = Aes256CbcEnc::new(&self.key.into(), &self.iv.into());
let encrypted = cipher.encrypt_padded_vec_mut::<Pkcs7>(data.as_bytes());
@ -100,6 +104,7 @@ impl Encryption {
Ok(EncryptedText::new(result))
}
/// Decrypts the given data.
pub fn decrypt(&self, encrypted_data: &EncryptedText) -> Result<String, String> {
let decoded = BASE64_STANDARD.decode(encrypted_data.get_encrypted()).map_err(|e| format!("Error decoding base64: {e}"))?;
@ -119,14 +124,18 @@ impl Encryption {
}
}
/// Represents encrypted text.
#[derive(Clone, Serialize, Deserialize)]
pub struct EncryptedText(String);
impl EncryptedText {
/// Creates a new encrypted text instance.
pub fn new(encrypted_data: String) -> Self {
EncryptedText(encrypted_data)
}
/// Returns the encrypted data.
pub fn get_encrypted(&self) -> &str {
&self.0
}
@ -144,11 +153,13 @@ impl fmt::Display for EncryptedText {
}
}
// Use Case: When we receive encrypted text from the client as body (e.g., in a POST request).
// We must interpret the body as EncryptedText.
/// Use Case: When we receive encrypted text from the client as body (e.g., in a POST request).
/// We must interpret the body as EncryptedText.
#[rocket::async_trait]
impl<'r> data::FromData<'r> for EncryptedText {
type Error = String;
/// Parses the data as EncryptedText.
async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> DataOutcome<'r, Self> {
let content_type = req.content_type();
if content_type.map_or(true, |ct| !ct.is_text()) {

View File

@ -2,10 +2,13 @@ use std::sync::OnceLock;
use rocket::get;
use crate::api_token::APIToken;
/// The data directory where the application stores its data.
pub static DATA_DIRECTORY: OnceLock<String> = OnceLock::new();
/// The config directory where the application stores its configuration.
pub static CONFIG_DIRECTORY: OnceLock<String> = OnceLock::new();
/// Returns the config directory.
#[get("/system/directories/config")]
pub fn get_config_directory(_token: APIToken) -> String {
match CONFIG_DIRECTORY.get() {
@ -14,6 +17,7 @@ pub fn get_config_directory(_token: APIToken) -> String {
}
}
/// Returns the data directory.
#[get("/system/directories/data")]
pub fn get_data_directory(_token: APIToken) -> String {
match DATA_DIRECTORY.get() {
@ -22,10 +26,12 @@ pub fn get_data_directory(_token: APIToken) -> String {
}
}
/// Returns true if the application is running in development mode.
pub fn is_dev() -> bool {
cfg!(debug_assertions)
}
/// Returns true if the application is running in production mode.
pub fn is_prod() -> bool {
!is_dev()
}

View File

@ -11,6 +11,7 @@ use crate::environment::is_dev;
static LOGGER: OnceLock<RuntimeLoggerHandle> = OnceLock::new();
/// Initialize the logging system.
pub fn init_logging() {
//
@ -60,6 +61,7 @@ pub fn init_logging() {
LOGGER.set(runtime_logger).expect("Cannot set LOGGER");
}
/// Switch the logging system to a file-based output.
pub fn switch_to_file_logging(logger_path: PathBuf) -> Result<(), Box<dyn Error>>{
LOGGER.get().expect("No LOGGER was set").handle.reset_flw(&FileLogWriter::builder(
FileSpec::default()
@ -137,7 +139,7 @@ fn terminal_colored_logger_format(
write!(w, "{}", flexi_logger::style(level).paint(record.args().to_string()))
}
// Custom LOGGER format for the log files:
/// Custom LOGGER format for the log files:
fn file_logger_format(
w: &mut dyn std::io::Write,
now: &mut DeferredNow,

View File

@ -28,7 +28,6 @@ async fn main() {
let app_commit_hash = metadata_lines.next().unwrap();
init_logging();
info!("Starting MindWork AI Studio:");
let working_directory = std::env::current_dir().unwrap();

View File

@ -1,5 +1,6 @@
use std::net::TcpListener;
/// Returns an available port on the local machine.
pub fn get_available_port() -> Option<u16> {
TcpListener::bind(("127.0.0.1", 0))
.map(|listener| listener.local_addr().unwrap().port())

View File

@ -8,10 +8,10 @@ use crate::certificate::{CERTIFICATE, CERTIFICATE_PRIVATE_KEY};
use crate::environment::is_dev;
use crate::network::get_available_port;
// The port used for the runtime API server. In the development environment, we use a fixed
// port, in the production environment we use the next available port. This differentiation
// is necessary because we cannot communicate the port to the .NET server in the development
// environment.
/// The port used for the runtime API server. In the development environment, we use a fixed
/// port, in the production environment we use the next available port. This differentiation
/// is necessary because we cannot communicate the port to the .NET server in the development
/// environment.
pub static API_SERVER_PORT: Lazy<u16> = Lazy::new(|| {
if is_dev() {
5000
@ -20,6 +20,8 @@ pub static API_SERVER_PORT: Lazy<u16> = Lazy::new(|| {
}
});
/// Starts the runtime API server. The server is used to communicate with the .NET server and
/// to provide additional functionality to the Tauri app.
pub fn start_runtime_api() {
let api_port = *API_SERVER_PORT;
info!("Try to start the API server on 'http://localhost:{api_port}'...");

View File

@ -7,6 +7,7 @@ use keyring::error::Error::NoEntry;
use crate::api_token::APIToken;
use crate::encryption::{EncryptedText, ENCRYPTION};
/// Stores a secret in the secret store using the operating system's keyring.
#[post("/secrets/store", data = "<request>")]
pub fn store_secret(_token: APIToken, request: Json<StoreSecret>) -> Json<StoreSecretResponse> {
let user_name = request.user_name.as_str();
@ -43,6 +44,7 @@ pub fn store_secret(_token: APIToken, request: Json<StoreSecret>) -> Json<StoreS
}
}
/// The structure of the request to store a secret.
#[derive(Deserialize)]
pub struct StoreSecret {
destination: String,
@ -50,12 +52,14 @@ pub struct StoreSecret {
secret: EncryptedText,
}
/// The structure of the response to storing a secret.
#[derive(Serialize)]
pub struct StoreSecretResponse {
success: bool,
issue: String,
}
/// Retrieves a secret from the secret store using the operating system's keyring.
#[post("/secrets/get", data = "<request>")]
pub fn get_secret(_token: APIToken, request: Json<RequestSecret>) -> Json<RequestedSecret> {
let user_name = request.user_name.as_str();
@ -100,6 +104,7 @@ pub fn get_secret(_token: APIToken, request: Json<RequestSecret>) -> Json<Reques
}
}
/// The structure of the request to retrieve a secret.
#[derive(Deserialize)]
pub struct RequestSecret {
destination: String,
@ -107,6 +112,7 @@ pub struct RequestSecret {
is_trying: bool,
}
/// The structure of the response to retrieving a secret.
#[derive(Serialize)]
pub struct RequestedSecret {
success: bool,
@ -114,6 +120,7 @@ pub struct RequestedSecret {
issue: String,
}
/// Deletes a secret from the secret store using the operating system's keyring.
#[post("/secrets/delete", data = "<request>")]
pub fn delete_secret(_token: APIToken, request: Json<RequestSecret>) -> Json<DeleteSecretResponse> {
let user_name = request.user_name.as_str();
@ -151,6 +158,7 @@ pub fn delete_secret(_token: APIToken, request: Json<RequestSecret>) -> Json<Del
}
}
/// The structure of the response to deleting a secret.
#[derive(Serialize)]
pub struct DeleteSecretResponse {
success: bool,