mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-04 16:47:21 +00:00
Improved the security of the connection between the app window and AI Studio (#1146)
This commit is contained in:
1 parent
64dc621a7d
commit
faae2951f7
26 files changed
+1056
-88
No files matched your search
+19
-3
@@ -8,6 +8,7 @@ root, which applies here as well.
|
||||
|
||||
Key modules:
|
||||
- `app_window.rs` - Tauri window management, updater integration
|
||||
- `app_access.rs` - Session cookie which lets the app window into the .NET server
|
||||
- `dotnet.rs` - Launches and manages the .NET sidecar process
|
||||
- `runtime_api.rs` - Axum-based HTTPS API for .NET ↔ Rust communication
|
||||
- `certificate.rs` - Generates self-signed TLS certificates for secure IPC
|
||||
@@ -25,6 +26,21 @@ a route added afterward would be open to every process on the machine. The test
|
||||
`every_route_requires_the_api_token` reads the routes from `create_router` and fails for any route
|
||||
which answers without the token.
|
||||
|
||||
**Only the app window gets into the .NET server.** Every start makes a new UI token in `app_access.rs`.
|
||||
Rust passes it to .NET as `AI_STUDIO_UI_TOKEN` through `ipc_environment` in `dotnet.rs`, and hands it to
|
||||
the window as an HttpOnly session cookie before the window opens the app (`change_location_to` in
|
||||
`app_window.rs`); .NET refuses every request without it. Therefore:
|
||||
|
||||
- Never put the token into a URL, a log line, or `startup.env`. Only a development build writes a boot
|
||||
link, to `dev-boot-link.txt` in the repository root, so that another browser can get the cookie.
|
||||
- Keep the cookie `Lax`. A release shows `tauri://localhost` before it opens the app, and WKWebView on
|
||||
macOS withholds a `Strict` cookie on that navigation. A development build starts from the dev server
|
||||
of the Tauri CLI on `127.0.0.1`, the same site as the app, which hides the problem; start it with
|
||||
`cargo tauri dev --no-watch --no-dev-server` to see what a release sees.
|
||||
- The .NET server listens on `127.0.0.1` only, and the cookie belongs to that host. Change the host in
|
||||
`APP_HOST` and in `Program.cs` together, since no test compares them. `AppAccessRuntimeAgreementTests`
|
||||
only checks that both sides use the same cookie name and environment variable.
|
||||
|
||||
**Runtime API handlers never block.** All calls of the .NET app share one HTTP/2 connection, and the
|
||||
task driving it may wait on exactly the Tokio worker which a blocking handler occupies, so a single
|
||||
blocking handler can hold up the whole app. Therefore:
|
||||
@@ -47,8 +63,8 @@ blocking handler can hold up the whole app. Therefore:
|
||||
## IPC Communication Flow
|
||||
1. Rust runtime starts and generates TLS certificate
|
||||
2. Rust starts internal HTTPS API on random port
|
||||
3. Rust launches .NET sidecar, passing: API port, certificate fingerprint, API token, secret key
|
||||
3. Rust launches .NET sidecar, passing: API port, certificate fingerprint, API token, secret key, UI token
|
||||
4. .NET reads environment variables and establishes secure HTTPS connection to Rust
|
||||
5. .NET requests an app port from Rust, starts Blazor Server on that port
|
||||
6. Rust opens Tauri webview pointing to localhost:app_port
|
||||
5. .NET requests an app port from Rust, starts Blazor Server on 127.0.0.1 at that port
|
||||
6. Rust hands the session cookie to the Tauri webview and points it to 127.0.0.1:app_port
|
||||
7. Bi-directional communication: .NET ↔ Rust via HTTPS API
|
||||
@@ -4,7 +4,7 @@
|
||||
"description": "Default capability for MindWork AI Studio",
|
||||
"remote": {
|
||||
"urls": [
|
||||
"http://localhost:*"
|
||||
"http://127.0.0.1:*"
|
||||
]
|
||||
},
|
||||
"windows": [
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
use log::{error, info};
|
||||
use once_cell::sync::Lazy;
|
||||
use tauri::webview::Cookie;
|
||||
use tauri::{Url, WebviewWindow};
|
||||
use crate::api_token::{generate_api_token, APIToken};
|
||||
|
||||
/// The token which the app window presents to the .NET server as the value of its session cookie.
|
||||
/// Every start makes a new one.
|
||||
pub static UI_ACCESS_TOKEN: Lazy<APIToken> = Lazy::new(generate_api_token);
|
||||
|
||||
/// The name of the session cookie. The .NET server expects the same name.
|
||||
pub const SESSION_COOKIE_NAME: &str = "ai_studio_session";
|
||||
|
||||
/// The environment variable which carries the token to the .NET server.
|
||||
pub const DOTNET_ENV_UI_TOKEN: &str = "AI_STUDIO_UI_TOKEN";
|
||||
|
||||
/// The host of the app. The .NET server listens on this address only, and the session cookie
|
||||
/// belongs to it.
|
||||
pub const APP_HOST: &str = "127.0.0.1";
|
||||
|
||||
/// The file in the root of the repository which holds the boot link of a development build.
|
||||
#[cfg(debug_assertions)]
|
||||
const DEV_BOOT_LINK_FILE: &str = "dev-boot-link.txt";
|
||||
|
||||
/// Builds the session cookie of the app window.
|
||||
///
|
||||
/// The cookie is for the app host alone and lives as long as the window: it has no expiry date,
|
||||
/// and scripts cannot read it. The window never sends it with a request which a page of another
|
||||
/// site makes, such as a script the page loads or a form it posts.
|
||||
///
|
||||
/// The cookie is `Lax`, not `Strict`. Before the window opens the app, it shows a page of Tauri,
|
||||
/// `tauri://localhost` on macOS, which is another site than the app. WKWebView counts that page
|
||||
/// as the one which opened the app, and it would withhold a `Strict` cookie. A `Lax` cookie goes
|
||||
/// along when a whole page of the app opens. Development builds show the page of the Tauri dev
|
||||
/// server on the app host instead, which hides the difference.
|
||||
///
|
||||
/// We parse the cookie from text because the `cookie` crate, whose builder would need its
|
||||
/// `SameSite` type, is no direct dependency of ours.
|
||||
pub fn session_cookie() -> Cookie<'static> {
|
||||
let token = UI_ACCESS_TOKEN.to_hex_text();
|
||||
Cookie::parse(format!("{SESSION_COOKIE_NAME}={token}; Domain={APP_HOST}; Path=/; HttpOnly; SameSite=Lax"))
|
||||
.expect("The session cookie consists of fixed attributes and a hexadecimal token, which always parse.")
|
||||
}
|
||||
|
||||
/// Puts the session cookie into the cookie store of the given window, before the window opens
|
||||
/// the app at the given URL.
|
||||
///
|
||||
/// Setting the cookie does not wait for the webview, and WebView2 even adds it in the background.
|
||||
/// Reading the cookies back waits until the webview has handled everything sent before, so the
|
||||
/// cookie is in place once this function returns. That reading is also the only way to learn
|
||||
/// whether setting the cookie worked.
|
||||
///
|
||||
/// Blocks until the main thread has answered, so never call it on a worker of the async runtime.
|
||||
/// The log never shows the value of the cookie.
|
||||
pub fn hand_over_session_cookie(window: &WebviewWindow, app_url: &Url) {
|
||||
if let Err(error) = window.set_cookie(session_cookie()) {
|
||||
error!(Source = "App access"; "Failed to hand the session cookie to the app window: {error}");
|
||||
return;
|
||||
}
|
||||
|
||||
match app_host_cookies(window, app_url) {
|
||||
Ok(cookies) if cookies.iter().any(|cookie| cookie.name() == SESSION_COOKIE_NAME) => {
|
||||
info!(Source = "App access"; "The app window got its session cookie.");
|
||||
},
|
||||
|
||||
Ok(_) => {
|
||||
error!(Source = "App access"; "The app window lacks its session cookie after it was set.");
|
||||
},
|
||||
|
||||
Err(error) => {
|
||||
error!(Source = "App access"; "Failed to check the session cookie of the app window: {error}");
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads the cookies of the app host from the given window.
|
||||
///
|
||||
/// On macOS, wry picks the cookies for a URL by comparing their domain with the domain of the
|
||||
/// URL. An IP address such as the app host has no domain in that sense, so wry would never find
|
||||
/// a cookie of the app there. We read all cookies instead and pick those of the app host
|
||||
/// ourselves.
|
||||
#[cfg(target_os = "macos")]
|
||||
fn app_host_cookies(window: &WebviewWindow, _app_url: &Url) -> tauri::Result<Vec<Cookie<'static>>> {
|
||||
Ok(window.cookies()?.into_iter().filter(|cookie| cookie.domain() == Some(APP_HOST)).collect())
|
||||
}
|
||||
|
||||
/// Reads the cookies of the app host from the given window.
|
||||
///
|
||||
/// WebView2 and WebKitGTK pick the cookies for a URL by themselves. Reading all cookies instead
|
||||
/// would need WebKitGTK 2.42 or newer.
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn app_host_cookies(window: &WebviewWindow, app_url: &Url) -> tauri::Result<Vec<Cookie<'static>>> {
|
||||
window.cookies_for_url(app_url.clone())
|
||||
}
|
||||
|
||||
/// Returns the link with which another browser gets the session cookie of a development build,
|
||||
/// such as the browser of an agent which checks a change in the running app.
|
||||
#[cfg(debug_assertions)]
|
||||
fn dev_boot_link(port: u16) -> String {
|
||||
format!("http://{APP_HOST}:{port}/dev/boot?token={}", UI_ACCESS_TOKEN.to_hex_text())
|
||||
}
|
||||
|
||||
/// Writes the boot link of a development build into a file of its own in the root of the
|
||||
/// repository. It never goes into the startup env file: that file holds the secrets of the IPC,
|
||||
/// which a browser needs none of.
|
||||
///
|
||||
/// Only development builds contain this function. A release has no boot link, and its .NET
|
||||
/// server knows no route which would take one. The log names the file, never the link.
|
||||
#[cfg(debug_assertions)]
|
||||
pub fn write_dev_boot_link(port: u16) {
|
||||
let path = std::path::PathBuf::from("..").join(DEV_BOOT_LINK_FILE);
|
||||
match std::fs::write(&path, dev_boot_link(port)) {
|
||||
Ok(_) => info!(Source = "App access"; "Wrote the boot link of this development build to '{}'.", path.display()),
|
||||
Err(error) => error!(Source = "App access"; "Failed to write the boot link of this development build to '{}': {error}", path.display()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn the_session_cookie_carries_the_token() {
|
||||
let cookie = session_cookie();
|
||||
|
||||
assert_eq!(cookie.name(), SESSION_COOKIE_NAME);
|
||||
assert_eq!(cookie.value(), UI_ACCESS_TOKEN.to_hex_text());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_session_cookie_belongs_to_the_app_host_alone() {
|
||||
let cookie = session_cookie();
|
||||
|
||||
assert_eq!(cookie.domain(), Some(APP_HOST));
|
||||
assert_eq!(cookie.path(), Some("/"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_session_cookie_is_hidden_from_scripts_and_other_sites() {
|
||||
let cookie = session_cookie();
|
||||
|
||||
assert_eq!(cookie.http_only(), Some(true));
|
||||
assert!(cookie.same_site().is_some_and(|same_site| same_site.is_lax()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_session_cookie_lives_only_as_long_as_the_window() {
|
||||
let cookie = session_cookie();
|
||||
|
||||
assert!(cookie.expires().is_none());
|
||||
assert!(cookie.max_age().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(debug_assertions)]
|
||||
fn the_dev_boot_link_leads_to_the_boot_route_of_the_app_host() {
|
||||
let link = Url::parse(dev_boot_link(61153).as_str()).unwrap();
|
||||
|
||||
assert_eq!(link.host_str(), Some(APP_HOST));
|
||||
assert_eq!(link.port(), Some(61153));
|
||||
assert_eq!(link.path(), "/dev/boot");
|
||||
|
||||
let token = link.query_pairs().find(|(key, _)| key == "token").map(|(_, value)| value.into_owned());
|
||||
assert_eq!(token.as_deref(), Some(UI_ACCESS_TOKEN.to_hex_text()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_ui_token_differs_from_the_api_token() {
|
||||
assert!(!UI_ACCESS_TOKEN.validate(&APIToken::from_hex_text(crate::runtime_api_token::API_TOKEN.to_hex_text())));
|
||||
}
|
||||
}
|
||||
+27
-11
@@ -21,6 +21,7 @@ use tauri_plugin_updater::{UpdaterExt, Update};
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::time;
|
||||
use crate::app_access::hand_over_session_cookie;
|
||||
use crate::clipboard::shutdown_clipboard;
|
||||
use crate::data_restore::apply_pending_restore;
|
||||
use crate::dotnet::{cleanup_dotnet_server, start_dotnet_server, stop_dotnet_server};
|
||||
@@ -312,7 +313,7 @@ fn is_tauri_asset_url(url: &tauri::Url) -> bool {
|
||||
matches!(url.scheme(), "http" | "https") && is_tauri_asset_host(url.host_str())
|
||||
}
|
||||
|
||||
fn is_local_http_url(url: &tauri::Url) -> bool {
|
||||
pub(crate) fn is_local_http_url(url: &tauri::Url) -> bool {
|
||||
matches!(url.scheme(), "http" | "https") && is_local_host(url.host_str())
|
||||
}
|
||||
|
||||
@@ -677,8 +678,9 @@ pub fn send_event(event: Event) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Changes the location of the main window to the given URL.
|
||||
pub async fn change_location_to(url: &str) {
|
||||
/// Changes the location of the main window to the given URL. The window gets its session cookie
|
||||
/// first, so that the .NET server lets it in.
|
||||
pub async fn change_location_to(url: tauri::Url) {
|
||||
// Try to get the main window. If it is not available yet, wait for it:
|
||||
let mut main_window_ready = false;
|
||||
let mut main_window_status_reported = false;
|
||||
@@ -700,16 +702,30 @@ pub async fn change_location_to(url: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(parsed_url) = tauri::Url::parse(url) && is_local_http_url(&parsed_url) {
|
||||
*APPROVED_APP_URL.lock().unwrap() = Some(parsed_url);
|
||||
if is_local_http_url(&url) {
|
||||
*APPROVED_APP_URL.lock().unwrap() = Some(url.clone());
|
||||
}
|
||||
|
||||
let js_location_change = format!("window.location = '{url}';");
|
||||
let main_window = main_window_spawn_clone.lock().unwrap();
|
||||
let location_change_result = main_window.as_ref().unwrap().eval(js_location_change.as_str());
|
||||
match location_change_result {
|
||||
Ok(_) => info!("The app location was changed to {url}."),
|
||||
Err(e) => error!("Failed to change the app location to {url}: {e}."),
|
||||
//
|
||||
// The window navigates by itself instead of through a script of the page it shows. That page
|
||||
// comes from Tauri, which is another site than the app; app_access::session_cookie explains
|
||||
// why the session cookie goes along all the same. We take the window out of the lock first:
|
||||
// the cookie and the navigation need the main thread, which may wait for this lock meanwhile.
|
||||
//
|
||||
// Handing over the cookie waits for the main thread. This function serves a request of the
|
||||
// runtime API, so the waiting happens outside the async runtime, see runtime/AGENTS.md.
|
||||
//
|
||||
let main_window = main_window_spawn_clone.lock().unwrap().clone().unwrap();
|
||||
let navigation = tokio::task::spawn_blocking(move || {
|
||||
hand_over_session_cookie(&main_window, &url);
|
||||
match main_window.navigate(url.clone()) {
|
||||
Ok(_) => info!("The app location was changed to {url}."),
|
||||
Err(e) => error!("Failed to change the app location to {url}: {e}."),
|
||||
}
|
||||
});
|
||||
|
||||
if let Err(error) = navigation.await {
|
||||
error!("Failed to open the app in the main window: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+76
-23
@@ -9,6 +9,7 @@ use tauri::{Manager, Url};
|
||||
use tauri_plugin_shell::process::{CommandChild, CommandEvent, TerminatedPayload};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
use crate::runtime_api_token::API_TOKEN;
|
||||
use crate::app_access::{APP_HOST, DOTNET_ENV_UI_TOKEN, UI_ACCESS_TOKEN};
|
||||
use crate::app_window::change_location_to;
|
||||
use crate::runtime_certificate::CERTIFICATE_FINGERPRINT;
|
||||
use crate::encryption::ENCRYPTION;
|
||||
@@ -177,46 +178,43 @@ fn dotnet_bundle_extraction_directory<R: tauri::Runtime>(app_handle: &tauri::App
|
||||
/// but also remove that code from any production build.
|
||||
#[cfg(debug_assertions)]
|
||||
pub fn create_startup_env_file() {
|
||||
|
||||
// Get the secret password & salt and convert it to a base64 string:
|
||||
let secret_password = BASE64_STANDARD.encode(ENCRYPTION.secret_password);
|
||||
let secret_key_salt = BASE64_STANDARD.encode(ENCRYPTION.secret_key_salt);
|
||||
let api_port = *API_SERVER_PORT;
|
||||
|
||||
warn!(Source = "Bootloader .NET"; "Development environment detected; create the startup env file at '../startup.env'.");
|
||||
let env_file_path = std::path::PathBuf::from("..").join("startup.env");
|
||||
let mut env_file = std::fs::File::create(env_file_path).unwrap();
|
||||
let mut env_file_lines = vec![
|
||||
format!("AI_STUDIO_SECRET_PASSWORD={secret_password}"),
|
||||
format!("AI_STUDIO_SECRET_KEY_SALT={secret_key_salt}"),
|
||||
format!("AI_STUDIO_CERTIFICATE_FINGERPRINT={}", CERTIFICATE_FINGERPRINT.get().unwrap()),
|
||||
format!("AI_STUDIO_API_PORT={api_port}"),
|
||||
format!("AI_STUDIO_API_TOKEN={}", API_TOKEN.to_hex_text()),
|
||||
];
|
||||
for (key, value) in external_http_custom_root_certificate_policy_environment() {
|
||||
env_file_lines.push(format!("{key}={value}"));
|
||||
}
|
||||
let env_file_lines: Vec<String> = ipc_environment()
|
||||
.into_iter()
|
||||
.map(|(key, value)| format!("{key}={value}"))
|
||||
.collect();
|
||||
|
||||
std::io::Write::write_all(&mut env_file, env_file_lines.join("\n").as_bytes()).unwrap();
|
||||
info!(Source = "Bootloader .NET"; "The startup env file was created successfully.");
|
||||
}
|
||||
|
||||
/// Starts the .NET server in a separate process.
|
||||
pub fn start_dotnet_server<R: tauri::Runtime>(app_handle: tauri::AppHandle<R>) {
|
||||
/// Returns the environment which the .NET server needs to talk to this runtime and to let only
|
||||
/// the app window in. The sidecar gets it, and so does the startup env file in the development
|
||||
/// environment, so neither of them can miss a variable.
|
||||
fn ipc_environment() -> Vec<(String, String)> {
|
||||
|
||||
// Get the secret password & salt and convert it to a base64 string:
|
||||
let secret_password = BASE64_STANDARD.encode(ENCRYPTION.secret_password);
|
||||
let secret_key_salt = BASE64_STANDARD.encode(ENCRYPTION.secret_key_salt);
|
||||
let api_port = *API_SERVER_PORT;
|
||||
|
||||
let mut dotnet_server_environment: HashMap<String, String> = HashMap::from_iter([
|
||||
let mut environment = vec![
|
||||
(String::from("AI_STUDIO_SECRET_PASSWORD"), secret_password),
|
||||
(String::from("AI_STUDIO_SECRET_KEY_SALT"), secret_key_salt),
|
||||
(String::from("AI_STUDIO_CERTIFICATE_FINGERPRINT"), CERTIFICATE_FINGERPRINT.get().unwrap().to_string()),
|
||||
(String::from("AI_STUDIO_API_PORT"), format!("{api_port}")),
|
||||
(String::from("AI_STUDIO_API_TOKEN"), API_TOKEN.to_hex_text().to_string()),
|
||||
]);
|
||||
dotnet_server_environment.extend(external_http_custom_root_certificate_policy_environment());
|
||||
(String::from(DOTNET_ENV_UI_TOKEN), UI_ACCESS_TOKEN.to_hex_text().to_string()),
|
||||
];
|
||||
environment.extend(external_http_custom_root_certificate_policy_environment());
|
||||
environment
|
||||
}
|
||||
|
||||
/// Starts the .NET server in a separate process.
|
||||
pub fn start_dotnet_server<R: tauri::Runtime>(app_handle: tauri::AppHandle<R>) {
|
||||
let mut dotnet_server_environment: HashMap<String, String> = HashMap::from_iter(ipc_environment());
|
||||
if let Some(extraction_directory) = dotnet_bundle_extraction_directory(&app_handle) {
|
||||
dotnet_server_environment.insert(String::from(DOTNET_ENV_BUNDLE_EXTRACT_BASE_DIR), extraction_directory);
|
||||
}
|
||||
@@ -303,7 +301,12 @@ pub async fn dotnet_ready() {
|
||||
}
|
||||
|
||||
let dotnet_server_port = *DOTNET_SERVER_PORT;
|
||||
let url = match Url::parse(format!("http://localhost:{dotnet_server_port}").as_str())
|
||||
|
||||
// A development build lets other browsers in, too, such as the one of an agent:
|
||||
#[cfg(debug_assertions)]
|
||||
tokio::task::spawn_blocking(move || crate::app_access::write_dev_boot_link(dotnet_server_port));
|
||||
|
||||
let url = match Url::parse(app_address(dotnet_server_port).as_str())
|
||||
{
|
||||
Ok(url) => url,
|
||||
Err(msg) => {
|
||||
@@ -312,7 +315,14 @@ pub async fn dotnet_ready() {
|
||||
}
|
||||
};
|
||||
|
||||
change_location_to(url.as_str()).await;
|
||||
change_location_to(url).await;
|
||||
}
|
||||
|
||||
/// Returns the address of the app on the .NET server. The server listens on the IPv4 loopback
|
||||
/// address only, so the window goes there directly instead of resolving `localhost`, which may
|
||||
/// answer with the IPv6 loopback address first.
|
||||
fn app_address(port: u16) -> String {
|
||||
format!("http://{APP_HOST}:{port}/")
|
||||
}
|
||||
|
||||
/// Stops the .NET server process.
|
||||
@@ -336,4 +346,47 @@ pub fn cleanup_dotnet_server() {
|
||||
if let Err(e) = kill_stale_process(pid_path, SIDECAR_TYPE) {
|
||||
warn!(Source = ".NET"; "Error during the cleanup of .NET: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::app_window::is_local_http_url;
|
||||
|
||||
#[test]
|
||||
fn the_app_address_is_the_ipv4_loopback_address() {
|
||||
let url = Url::parse(app_address(61153).as_str()).unwrap();
|
||||
|
||||
assert_eq!(url.host_str(), Some("127.0.0.1"));
|
||||
assert_eq!(url.port(), Some(61153));
|
||||
assert_eq!(url.path(), "/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_app_address_counts_as_local() {
|
||||
let url = Url::parse(app_address(61153).as_str()).unwrap();
|
||||
assert!(is_local_http_url(&url));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_ipc_environment_carries_every_variable_the_dotnet_server_needs() {
|
||||
// Only the start of the app generates the certificate this fingerprint belongs to:
|
||||
let _ = CERTIFICATE_FINGERPRINT.set(String::from("test-fingerprint"));
|
||||
|
||||
let environment = ipc_environment();
|
||||
let keys: Vec<&str> = environment.iter().map(|(key, _)| key.as_str()).collect();
|
||||
for expected in [
|
||||
"AI_STUDIO_SECRET_PASSWORD",
|
||||
"AI_STUDIO_SECRET_KEY_SALT",
|
||||
"AI_STUDIO_CERTIFICATE_FINGERPRINT",
|
||||
"AI_STUDIO_API_PORT",
|
||||
"AI_STUDIO_API_TOKEN",
|
||||
DOTNET_ENV_UI_TOKEN,
|
||||
] {
|
||||
assert!(keys.contains(&expected), "The environment lacks {expected}.");
|
||||
}
|
||||
|
||||
let ui_token = environment.iter().find(|(key, _)| key == DOTNET_ENV_UI_TOKEN).map(|(_, value)| value.as_str());
|
||||
assert_eq!(ui_token, Some(UI_ACCESS_TOKEN.to_hex_text()));
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ pub mod environment;
|
||||
pub mod dotnet;
|
||||
pub mod network;
|
||||
pub mod api_token;
|
||||
pub mod app_access;
|
||||
pub mod app_window;
|
||||
pub mod secret;
|
||||
pub mod clipboard;
|
||||
|
||||
Reference in new issue
Block a user