AI-Studio/runtime/src/main.rs

379 lines
13 KiB
Rust
Raw Normal View History

2024-05-12 12:34:29 +00:00
// Prevents an additional console window on Windows in release, DO NOT REMOVE!!
2024-03-28 21:26:48 +00:00
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
2024-09-01 18:10:03 +00:00
extern crate rocket;
extern crate core;
2024-11-05 14:14:17 +00:00
use std::collections::{HashMap, HashSet};
use std::net::TcpListener;
2024-09-01 18:10:03 +00:00
use std::sync::{Arc, Mutex, OnceLock};
2024-11-04 19:42:12 +00:00
use std::time::Duration;
2024-06-30 13:26:28 +00:00
use once_cell::sync::Lazy;
use arboard::Clipboard;
2024-09-01 18:10:03 +00:00
use base64::Engine;
use base64::prelude::BASE64_STANDARD;
2024-04-05 20:23:01 +00:00
use keyring::Entry;
2024-09-01 18:10:03 +00:00
use serde::{Deserialize, Serialize};
use tauri::{Manager, Url, Window};
use tauri::api::process::{Command, CommandChild, CommandEvent};
use tokio::time;
use keyring::error::Error::NoEntry;
2024-11-05 14:14:17 +00:00
use log::{debug, error, info, warn};
2024-09-01 18:10:03 +00:00
use rand::{RngCore, SeedableRng};
use rcgen::generate_simple_self_signed;
use rocket::figment::Figment;
2024-11-04 19:42:12 +00:00
use rocket::{get, post, routes, Request};
2024-09-01 18:10:03 +00:00
use rocket::config::{Shutdown};
use rocket::http::Status;
use rocket::request::{FromRequest};
use rocket::serde::json::Json;
2024-11-04 19:42:12 +00:00
use sha2::{Sha256, Digest};
2024-06-30 13:26:28 +00:00
use tauri::updater::UpdateResponse;
2024-11-04 19:42:12 +00:00
use mindwork_ai_studio::encryption::{EncryptedText, ENCRYPTION};
2024-11-05 14:14:17 +00:00
use mindwork_ai_studio::environment::{is_dev, is_prod};
use mindwork_ai_studio::log::{init_logging, switch_to_file_logging};
2024-09-01 18:10:03 +00:00
// 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.
static API_SERVER_PORT: Lazy<u16> = Lazy::new(|| {
if is_dev() {
5000
} else {
get_available_port().unwrap()
}
});
#[tokio::main]
async fn main() {
2024-05-21 16:55:35 +00:00
let metadata = include_str!("../../metadata.txt");
let mut metadata_lines = metadata.lines();
let app_version = metadata_lines.next().unwrap();
let build_time = metadata_lines.next().unwrap();
let build_number = metadata_lines.next().unwrap();
let dotnet_sdk_version = metadata_lines.next().unwrap();
let dotnet_version = metadata_lines.next().unwrap();
let rust_version = metadata_lines.next().unwrap();
let mud_blazor_version = metadata_lines.next().unwrap();
let tauri_version = metadata_lines.next().unwrap();
let app_commit_hash = metadata_lines.next().unwrap();
2024-11-05 14:14:17 +00:00
init_logging();
2024-05-21 16:55:35 +00:00
info!("Starting MindWork AI Studio:");
2024-09-03 14:13:57 +00:00
let working_directory = std::env::current_dir().unwrap();
info!(".. The working directory is: '{working_directory:?}'");
2024-05-21 16:55:35 +00:00
info!(".. Version: v{app_version} (commit {app_commit_hash}, build {build_number})");
info!(".. Build time: {build_time}");
info!(".. .NET SDK: v{dotnet_sdk_version}");
info!(".. .NET: v{dotnet_version}");
info!(".. Rust: v{rust_version}");
info!(".. MudBlazor: v{mud_blazor_version}");
info!(".. Tauri: v{tauri_version}");
if is_dev() {
warn!("Running in development mode.");
} else {
info!("Running in production mode.");
}
2024-09-01 18:10:03 +00:00
info!("Try to generate a TLS certificate for the runtime API server...");
2024-09-01 18:10:03 +00:00
let subject_alt_names = vec!["localhost".to_string()];
let certificate_data = generate_simple_self_signed(subject_alt_names).unwrap();
let certificate_binary_data = certificate_data.cert.der().to_vec();
let certificate_fingerprint = Sha256::digest(certificate_binary_data).to_vec();
let certificate_fingerprint = certificate_fingerprint.iter().fold(String::new(), |mut result, byte| {
result.push_str(&format!("{:02x}", byte));
result
});
let certificate_fingerprint = certificate_fingerprint.to_uppercase();
info!("Certificate fingerprint: '{certificate_fingerprint}'.");
info!("Done generating certificate for the runtime API server.");
2024-09-01 18:10:03 +00:00
let api_port = *API_SERVER_PORT;
info!("Try to start the API server on 'http://localhost:{api_port}'...");
// The shutdown configuration for the runtime API server:
let mut shutdown = Shutdown {
// We do not want to use the Ctrl+C signal to stop the server:
ctrlc: false,
2024-09-03 14:13:57 +00:00
// Everything else is set to default for now:
..Shutdown::default()
};
#[cfg(unix)]
{
// We do not want to use the termination signal to stop the server.
// This option, however, is only available on Unix systems:
shutdown.signals = HashSet::new();
}
2024-09-01 18:10:03 +00:00
// Configure the runtime API server:
let figment = Figment::from(rocket::Config::release_default())
2024-09-01 18:10:03 +00:00
// We use the next available port which was determined before:
.merge(("port", api_port))
2024-09-01 18:10:03 +00:00
// The runtime API server should be accessible only from the local machine:
.merge(("address", "127.0.0.1"))
2024-09-01 18:10:03 +00:00
// We do not want to use the Ctrl+C signal to stop the server:
.merge(("ctrlc", false))
2024-09-01 18:10:03 +00:00
// Set a name for the server:
.merge(("ident", "AI Studio Runtime API"))
// Set the maximum number of workers and blocking threads:
.merge(("workers", 3))
.merge(("max_blocking", 12))
// No colors and emojis in the log output:
.merge(("cli_colors", false))
// Read the TLS certificate and key from the generated certificate data in-memory:
.merge(("tls.certs", certificate_data.cert.pem().as_bytes()))
.merge(("tls.key", certificate_data.key_pair.serialize_pem().as_bytes()))
// Set the shutdown configuration:
.merge(("shutdown", shutdown));
2024-09-01 18:10:03 +00:00
//
// Start the runtime API server in a separate thread. This is necessary
// because the server is blocking, and we need to run the Tauri app in
// parallel:
//
tauri::async_runtime::spawn(async move {
2024-10-18 08:40:19 +00:00
rocket::custom(figment)
2024-09-01 18:10:03 +00:00
.mount("/", routes![
2024-11-05 18:36:25 +00:00
mindwork_ai_studio::dotnet::dotnet_port,
mindwork_ai_studio::dotnet::dotnet_ready,
set_clipboard,
mindwork_ai_studio::app_window::check_for_update,
mindwork_ai_studio::app_window::install_update,
get_secret,
store_secret,
delete_secret,
mindwork_ai_studio::environment::get_data_directory,
mindwork_ai_studio::environment::get_config_directory,
2024-09-01 18:10:03 +00:00
])
.ignite().await.unwrap()
.launch().await.unwrap();
});
2024-09-01 18:10:03 +00:00
info!("Secret password for the IPC channel was generated successfully.");
2024-11-05 18:35:53 +00:00
start_dotnet_server(*API_SERVER_PORT, certificate_fingerprint);
start_tauri();
2024-06-30 13:26:28 +00:00
}
2024-09-01 18:10:03 +00:00
#[post("/secrets/store", data = "<request>")]
fn store_secret(_token: APIToken, request: Json<StoreSecret>) -> Json<StoreSecretResponse> {
let user_name = request.user_name.as_str();
let decrypted_text = match ENCRYPTION.decrypt(&request.secret) {
Ok(text) => text,
Err(e) => {
error!(Source = "Secret Store"; "Failed to decrypt the text: {e}.");
return Json(StoreSecretResponse {
success: false,
issue: format!("Failed to decrypt the text: {e}"),
})
},
};
let service = format!("mindwork-ai-studio::{}", request.destination);
let entry = Entry::new(service.as_str(), user_name).unwrap();
let result = entry.set_password(decrypted_text.as_str());
match result {
Ok(_) => {
2024-09-01 18:10:03 +00:00
info!(Source = "Secret Store"; "Secret for {service} and user {user_name} was stored successfully.");
Json(StoreSecretResponse {
success: true,
issue: String::from(""),
2024-09-01 18:10:03 +00:00
})
},
Err(e) => {
2024-09-01 18:10:03 +00:00
error!(Source = "Secret Store"; "Failed to store secret for {service} and user {user_name}: {e}.");
Json(StoreSecretResponse {
success: false,
issue: e.to_string(),
2024-09-01 18:10:03 +00:00
})
},
}
}
2024-09-01 18:10:03 +00:00
#[derive(Deserialize)]
struct StoreSecret {
destination: String,
user_name: String,
secret: EncryptedText,
}
#[derive(Serialize)]
struct StoreSecretResponse {
success: bool,
issue: String,
2024-04-05 20:23:01 +00:00
}
2024-09-01 18:10:03 +00:00
#[post("/secrets/get", data = "<request>")]
fn get_secret(_token: APIToken, request: Json<RequestSecret>) -> Json<RequestedSecret> {
let user_name = request.user_name.as_str();
let service = format!("mindwork-ai-studio::{}", request.destination);
let entry = Entry::new(service.as_str(), user_name).unwrap();
let secret = entry.get_password();
match secret {
Ok(s) => {
2024-09-01 18:10:03 +00:00
info!(Source = "Secret Store"; "Secret for '{service}' and user '{user_name}' was retrieved successfully.");
// Encrypt the secret:
let encrypted_secret = match ENCRYPTION.encrypt(s.as_str()) {
Ok(e) => e,
Err(e) => {
error!(Source = "Secret Store"; "Failed to encrypt the secret: {e}.");
return Json(RequestedSecret {
success: false,
secret: EncryptedText::new(String::from("")),
issue: format!("Failed to encrypt the secret: {e}"),
});
},
};
Json(RequestedSecret {
success: true,
2024-09-01 18:10:03 +00:00
secret: encrypted_secret,
issue: String::from(""),
2024-09-01 18:10:03 +00:00
})
},
Err(e) => {
if !request.is_trying {
error!(Source = "Secret Store"; "Failed to retrieve secret for '{service}' and user '{user_name}': {e}.");
}
2024-09-01 18:10:03 +00:00
Json(RequestedSecret {
success: false,
2024-09-01 18:10:03 +00:00
secret: EncryptedText::new(String::from("")),
issue: format!("Failed to retrieve secret for '{service}' and user '{user_name}': {e}"),
})
},
}
}
2024-09-01 18:10:03 +00:00
#[derive(Deserialize)]
struct RequestSecret {
destination: String,
user_name: String,
is_trying: bool,
2024-09-01 18:10:03 +00:00
}
#[derive(Serialize)]
struct RequestedSecret {
success: bool,
2024-09-01 18:10:03 +00:00
secret: EncryptedText,
issue: String,
}
2024-09-01 18:10:03 +00:00
#[post("/secrets/delete", data = "<request>")]
fn delete_secret(_token: APIToken, request: Json<RequestSecret>) -> Json<DeleteSecretResponse> {
let user_name = request.user_name.as_str();
let service = format!("mindwork-ai-studio::{}", request.destination);
let entry = Entry::new(service.as_str(), user_name).unwrap();
2024-08-24 18:05:39 +00:00
let result = entry.delete_credential();
match result {
Ok(_) => {
2024-09-01 18:10:03 +00:00
warn!(Source = "Secret Store"; "Secret for {service} and user {user_name} was deleted successfully.");
Json(DeleteSecretResponse {
success: true,
was_entry_found: true,
issue: String::from(""),
2024-09-01 18:10:03 +00:00
})
},
Err(NoEntry) => {
2024-09-01 18:10:03 +00:00
warn!(Source = "Secret Store"; "No secret for {service} and user {user_name} was found.");
Json(DeleteSecretResponse {
success: true,
was_entry_found: false,
issue: String::from(""),
2024-09-01 18:10:03 +00:00
})
}
Err(e) => {
2024-09-01 18:10:03 +00:00
error!(Source = "Secret Store"; "Failed to delete secret for {service} and user {user_name}: {e}.");
Json(DeleteSecretResponse {
success: false,
was_entry_found: false,
issue: e.to_string(),
2024-09-01 18:10:03 +00:00
})
},
}
}
#[derive(Serialize)]
struct DeleteSecretResponse {
success: bool,
was_entry_found: bool,
issue: String,
}
2024-09-01 18:10:03 +00:00
#[post("/clipboard/set", data = "<encrypted_text>")]
fn set_clipboard(_token: APIToken, encrypted_text: EncryptedText) -> Json<SetClipboardResponse> {
// Decrypt this text first:
let decrypted_text = match ENCRYPTION.decrypt(&encrypted_text) {
Ok(text) => text,
Err(e) => {
error!(Source = "Clipboard"; "Failed to decrypt the text: {e}.");
return Json(SetClipboardResponse {
success: false,
issue: e,
})
},
};
let clipboard_result = Clipboard::new();
let mut clipboard = match clipboard_result {
Ok(clipboard) => clipboard,
Err(e) => {
2024-09-01 18:10:03 +00:00
error!(Source = "Clipboard"; "Failed to get the clipboard instance: {e}.");
return Json(SetClipboardResponse {
success: false,
issue: e.to_string(),
2024-09-01 18:10:03 +00:00
})
},
};
2024-09-01 18:10:03 +00:00
let set_text_result = clipboard.set_text(decrypted_text);
match set_text_result {
Ok(_) => {
2024-09-01 18:10:03 +00:00
debug!(Source = "Clipboard"; "Text was set to the clipboard successfully.");
Json(SetClipboardResponse {
success: true,
issue: String::from(""),
2024-09-01 18:10:03 +00:00
})
},
Err(e) => {
2024-09-01 18:10:03 +00:00
error!(Source = "Clipboard"; "Failed to set text to the clipboard: {e}.");
Json(SetClipboardResponse {
success: false,
issue: e.to_string(),
2024-09-01 18:10:03 +00:00
})
},
}
}
#[derive(Serialize)]
struct SetClipboardResponse {
success: bool,
issue: String,
2024-04-05 20:23:01 +00:00
}