mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-11 15:13:47 +00:00
Required the API token for every runtime API route (#1045)
This commit is contained in:
1 parent
4f966eed86
commit
8badadb35f
24 files changed
+231
-79
No files matched your search
@@ -130,6 +130,13 @@ Key modules:
|
||||
- `pandoc.rs` - Integration with Pandoc for document conversion
|
||||
- `log.rs` - Logging infrastructure using `flexi_logger`
|
||||
|
||||
**Every runtime API route requires the API token.** `require_api_token` in `runtime_api.rs` checks it
|
||||
for all routes at once, so handlers take no `APIToken` argument. Register a new route in
|
||||
`create_router`, before that call: `route_layer` protects only the routes registered before it, and
|
||||
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.
|
||||
|
||||
**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:
|
||||
|
||||
@@ -9904,6 +9904,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2124655767"] = "AI Studio shows
|
||||
-- The windows-rs project provides access to Windows APIs from Rust. We use several libraries from this project: windows-registry is used to read the desired configuration in Windows enterprise environments. The windows and windows-collections libraries are used to open the native Windows share dialog, e.g., when you share a plugin with others.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2146481269"] = "The windows-rs project provides access to Windows APIs from Rust. We use several libraries from this project: windows-registry is used to read the desired configuration in Windows enterprise environments. The windows and windows-collections libraries are used to open the native Windows share dialog, e.g., when you share a plugin with others."
|
||||
|
||||
-- The user interface proves itself to the runtime with a secret token on every request. subtle compares this token in constant time, so the duration of a check does not reveal how much of a guessed token was right.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2150849628"] = "The user interface proves itself to the runtime with a secret token on every request. subtle compares this token in constant time, so the duration of a check does not reveal how much of a guessed token was right."
|
||||
|
||||
-- This library is used to create temporary folders in runtime tests and supporting filesystem operations.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2160280545"] = "This library is used to create temporary folders in runtime tests and supporting filesystem operations."
|
||||
|
||||
|
||||
@@ -389,6 +389,7 @@
|
||||
<ThirdPartyComponent Name="base64" Developer="Marshall Pierce, Alice Maz & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/marshallpierce/rust-base64/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/marshallpierce/rust-base64" UseCase="@T("For some data transfers, we need to encode the data in base64. This Rust library is great for this purpose.")"/>
|
||||
<ThirdPartyComponent Name="Rust Crypto" Developer="Artyom Pavlov, Tony Arcieri, Brian Warner, Arthur Gautier, Vlad Filippov, Friedel Ziegelmayer, Nicolas Stalder & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/RustCrypto/traits/blob/master/cipher/LICENSE-MIT" RepositoryUrl="https://github.com/RustCrypto" UseCase="@T("When transferring sensitive data between Rust runtime and .NET app, we encrypt the data. We use some libraries from the Rust Crypto project for this purpose: cipher, aes, cbc, pbkdf2, hmac, and sha2. We are thankful for the great work of the Rust Crypto project.")"/>
|
||||
<ThirdPartyComponent Name="rcgen" Developer="RustTLS developers, est31 & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rustls/rcgen/blob/main/LICENSE" RepositoryUrl="https://github.com/rustls/rcgen" UseCase="@T("For the secure communication between the user interface and the runtime, we need to create certificates. This Rust library is great for this purpose.")"/>
|
||||
<ThirdPartyComponent Name="subtle" Developer="Isis Lovecruft, Henry de Valence, Tony Arcieri, Sean Bowe, Amber Sprenkels, Artyom Pavlov & Open Source Community" LicenseName="BSD-3-Clause" LicenseUrl="https://github.com/dalek-cryptography/subtle/blob/main/LICENSE" RepositoryUrl="https://github.com/dalek-cryptography/subtle" UseCase="@T("The user interface proves itself to the runtime with a secret token on every request. subtle compares this token in constant time, so the duration of a check does not reveal how much of a guessed token was right.")"/>
|
||||
<ThirdPartyComponent Name="windows-rs" Developer="Microsoft, Kenny Kerr, Ryan Levick, Rafael Rivera, sivadeilra, Marijn Suijten & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/microsoft/windows-rs/blob/master/license-mit" RepositoryUrl="https://github.com/microsoft/windows-rs" UseCase="@T("The windows-rs project provides access to Windows APIs from Rust. We use several libraries from this project: windows-registry is used to read the desired configuration in Windows enterprise environments. The windows and windows-collections libraries are used to open the native Windows share dialog, e.g., when you share a plugin with others.")"/>
|
||||
<ThirdPartyComponent Name="objc2" Developer="Steven Sheldon, Mads Marquart, silvanshade, Dzmitry Malyshau, Felix Nemo Kaaman, adamnemecek, Samuel Sleight, Paul Mabileau & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/madsmtm/objc2/blob/main/LICENSE-MIT.txt" RepositoryUrl="https://github.com/madsmtm/objc2" UseCase="@T("The objc2 project provides access to Apple's Objective-C frameworks from Rust. On macOS, we use the libraries objc2, objc2-app-kit, and objc2-foundation to open the native macOS share sheet, e.g., when you share a plugin with others.")"/>
|
||||
<ThirdPartyComponent Name="file-format" Developer="Mickaël Malécot & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/mmalecot/file-format/blob/main/LICENSE-MIT" RepositoryUrl="https://github.com/mmalecot/file-format" UseCase="@T("This library identifies files by their content. It is used for document streaming and as the first safety and media classification step before local audio processing.")"/>
|
||||
|
||||
+3
@@ -9906,6 +9906,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2124655767"] = "AI Studio zeigt
|
||||
-- The windows-rs project provides access to Windows APIs from Rust. We use several libraries from this project: windows-registry is used to read the desired configuration in Windows enterprise environments. The windows and windows-collections libraries are used to open the native Windows share dialog, e.g., when you share a plugin with others.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2146481269"] = "Das Projekt windows-rs ermöglicht den Zugriff auf Windows-APIs aus Rust. Wir verwenden mehrere Bibliotheken aus diesem Projekt: windows-registry wird verwendet, um die gewünschte Konfiguration in Windows-Unternehmensumgebungen auszulesen. Die Bibliotheken windows und windows-collections werden verwendet, um den nativen Windows-Dialog zum Teilen zu öffnen, zum Beispiel wenn Sie ein Plugin mit anderen teilen."
|
||||
|
||||
-- The user interface proves itself to the runtime with a secret token on every request. subtle compares this token in constant time, so the duration of a check does not reveal how much of a guessed token was right.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2150849628"] = "Die Benutzeroberfläche authentifiziert sich bei jeder Anfrage gegenüber der Laufzeitumgebung mit einem geheimen Token. Die subtle-Bibliothek vergleicht dieses Token in konstanter Zeit, sodass die Dauer der Prüfung nicht verrät, wie viel von einem erratenen Token korrekt war."
|
||||
|
||||
-- This library is used to create temporary folders in runtime tests and supporting filesystem operations.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2160280545"] = "Diese Bibliothek wird verwendet, um temporäre Ordner bei Laufzeittests zu erstellen und Dateisystemoperationen zu unterstützen."
|
||||
|
||||
|
||||
+3
@@ -9906,6 +9906,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2124655767"] = "AI Studio shows
|
||||
-- The windows-rs project provides access to Windows APIs from Rust. We use several libraries from this project: windows-registry is used to read the desired configuration in Windows enterprise environments. The windows and windows-collections libraries are used to open the native Windows share dialog, e.g., when you share a plugin with others.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2146481269"] = "The windows-rs project provides access to Windows APIs from Rust. We use several libraries from this project: windows-registry is used to read the desired configuration in Windows enterprise environments. The windows and windows-collections libraries are used to open the native Windows share dialog, e.g., when you share a plugin with others."
|
||||
|
||||
-- The user interface proves itself to the runtime with a secret token on every request. subtle compares this token in constant time, so the duration of a check does not reveal how much of a guessed token was right.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2150849628"] = "The user interface proves itself to the runtime with a secret token on every request. subtle compares this token in constant time, so the duration of a check does not reveal how much of a guessed token was right."
|
||||
|
||||
-- This library is used to create temporary folders in runtime tests and supporting filesystem operations.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2160280545"] = "This library is used to create temporary folders in runtime tests and supporting filesystem operations."
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
- Fixed justified texts being hyphenated by the rules of English even when AI Studio shows another language, such as German. Screen readers now also know which language AI Studio uses.
|
||||
- Fixed the log file of AI Studio showing the wrong time for some entries when AI Studio was busy. Each entry now shows when it actually happened.
|
||||
- Fixed AI Studio pausing for a moment while it opened the local index of a data source, such as a mailbox. Other work, for example applying the configuration of your organization, had to wait until then.
|
||||
- Fixed a security issue in how AI Studio prepares images for the Visual Briefing assistant. AI Studio now protects all parts of its internal connection in one central place, and an automated test checks that no part is left out.
|
||||
- Updated the code contributions on the supporters page, which now thank everyone who has contributed code to AI Studio so far.
|
||||
- Upgraded several libraries to improve security.
|
||||
- Upgraded to Rust v1.99.0
|
||||
Generated
+2
@@ -4299,6 +4299,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"sha2 0.11.0",
|
||||
"strum_macros",
|
||||
"subtle",
|
||||
"symphonia",
|
||||
"sys-locale",
|
||||
"sysinfo",
|
||||
@@ -4316,6 +4317,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"toml 1.1.4+spec-1.1.0",
|
||||
"tower",
|
||||
"webkit2gtk",
|
||||
"webm-iterable",
|
||||
"whoami",
|
||||
|
||||
@@ -33,6 +33,7 @@ axum-server = { version = "0.8.0", features = ["tls-rustls"] }
|
||||
rustls = { version = "0.23.45", default-features = false, features = ["aws_lc_rs"] }
|
||||
rand = "0.10.2"
|
||||
rand_chacha = "0.10.0"
|
||||
subtle = "2.6.1"
|
||||
base64 = "0.22.1"
|
||||
aes = "0.9.1"
|
||||
cbc = "0.2.1"
|
||||
@@ -79,6 +80,10 @@ toml = "1.1.4"
|
||||
image = { version = "0.25.10", default-features = false, features = ["jpeg", "png", "webp"] }
|
||||
tokenizers = "0.23.1"
|
||||
|
||||
[dev-dependencies]
|
||||
# Sends requests through the runtime API router in tests, without starting a server.
|
||||
tower = { version = "0.5.2", features = ["util"] }
|
||||
|
||||
[patch.crates-io]
|
||||
# Issue: This repo was not updated since 2020. The rand crate was outdated. We patched it to use a newer version of rand.
|
||||
# State: There is a PR for a long time, but it was not merged. We use the git version for now. Qdrant Edge still depends
|
||||
|
||||
@@ -2,6 +2,7 @@ use log::error;
|
||||
use rand::rngs::SysRng;
|
||||
use rand::{Rng, SeedableRng};
|
||||
use rand_chacha::ChaChaRng;
|
||||
use subtle::ConstantTimeEq;
|
||||
|
||||
/// The API token data structure used to authenticate requests.
|
||||
pub struct APIToken {
|
||||
@@ -31,8 +32,12 @@ impl APIToken {
|
||||
}
|
||||
|
||||
/// Validates the received token against the valid token.
|
||||
///
|
||||
/// The comparison takes the same time no matter where the two tokens differ. A comparison
|
||||
/// which stops at the first difference would let the time of each refusal tell how much of a
|
||||
/// guess was right.
|
||||
pub fn validate(&self, received_token: &Self) -> bool {
|
||||
received_token.to_hex_text() == self.to_hex_text()
|
||||
received_token.to_hex_text().as_bytes().ct_eq(self.to_hex_text().as_bytes()).into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,4 +55,29 @@ pub fn generate_api_token() -> APIToken {
|
||||
});
|
||||
rng.fill_bytes(&mut token);
|
||||
APIToken::from_bytes(token.to_vec())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn validate_accepts_only_the_identical_token() {
|
||||
let valid_token = APIToken::from_hex_text("0123456789abcdef");
|
||||
|
||||
assert!(valid_token.validate(&APIToken::from_hex_text("0123456789abcdef")));
|
||||
assert!(!valid_token.validate(&APIToken::from_hex_text("0123456789abcdee")));
|
||||
assert!(!valid_token.validate(&APIToken::from_hex_text("1123456789abcdef")));
|
||||
assert!(!valid_token.validate(&APIToken::from_hex_text("0123456789abcde")));
|
||||
assert!(!valid_token.validate(&APIToken::from_hex_text("0123456789abcdef0")));
|
||||
assert!(!valid_token.validate(&APIToken::from_hex_text("")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_generated_token_validates_against_its_own_text() {
|
||||
let token = generate_api_token();
|
||||
|
||||
assert_eq!(token.to_hex_text().len(), 64);
|
||||
assert!(token.validate(&APIToken::from_hex_text(token.to_hex_text())));
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,6 @@ use tauri_plugin_updater::{UpdaterExt, Update};
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::time;
|
||||
use crate::api_token::APIToken;
|
||||
use crate::clipboard::shutdown_clipboard;
|
||||
use crate::dotnet::{cleanup_dotnet_server, start_dotnet_server, stop_dotnet_server};
|
||||
use crate::environment::{
|
||||
@@ -379,7 +378,7 @@ fn should_open_in_system_browser<R: tauri::Runtime>(webview: &tauri::Webview<R>,
|
||||
/// When the client disconnects, the stream is closed. But we try to not lose events in between.
|
||||
/// The client is expected to reconnect automatically when the connection is closed and continue
|
||||
/// listening for events.
|
||||
pub async fn get_event_stream(_token: APIToken) -> Response {
|
||||
pub async fn get_event_stream() -> Response {
|
||||
// Get the lock to the event broadcast sender:
|
||||
let event_broadcast_lock = EVENT_BROADCAST.lock().unwrap();
|
||||
|
||||
@@ -652,7 +651,7 @@ pub async fn change_location_to(url: &str) {
|
||||
}
|
||||
|
||||
/// Checks for updates.
|
||||
pub async fn check_for_update(_token: APIToken) -> Json<CheckUpdateResponse> {
|
||||
pub async fn check_for_update() -> Json<CheckUpdateResponse> {
|
||||
if let Some(reason) = self_update_blocked_reason(is_flatpak(), installation_kind()) {
|
||||
warn!(Source = "Updater"; "Skipping update check because {reason}.");
|
||||
return Json(CheckUpdateResponse {
|
||||
@@ -737,7 +736,7 @@ pub struct CheckUpdateResponse {
|
||||
}
|
||||
|
||||
/// Installs the update.
|
||||
pub async fn install_update(_token: APIToken) {
|
||||
pub async fn install_update() {
|
||||
if let Some(reason) = self_update_blocked_reason(is_flatpak(), installation_kind()) {
|
||||
warn!(Source = "Updater"; "Skipping update installation because {reason}.");
|
||||
return;
|
||||
@@ -819,7 +818,7 @@ pub struct AppExitResponse {
|
||||
}
|
||||
|
||||
/// Requests a controlled shutdown of the entire desktop application.
|
||||
pub async fn exit_app(_token: APIToken) -> Json<AppExitResponse> {
|
||||
pub async fn exit_app() -> Json<AppExitResponse> {
|
||||
let app_handle = {
|
||||
let main_window_lock = MAIN_WINDOW.lock().unwrap();
|
||||
match main_window_lock.as_ref() {
|
||||
@@ -848,7 +847,7 @@ pub async fn exit_app(_token: APIToken) -> Json<AppExitResponse> {
|
||||
|
||||
/// Registers or updates a global shortcut. If the shortcut string is empty,
|
||||
/// the existing shortcut for that name will be unregistered.
|
||||
pub async fn register_shortcut(_token: APIToken, payload: Json<RegisterShortcutRequest>) -> Json<ShortcutResponse> {
|
||||
pub async fn register_shortcut(payload: Json<RegisterShortcutRequest>) -> Json<ShortcutResponse> {
|
||||
let app_handle = MAIN_WINDOW.lock().unwrap().as_ref().map(|window| window.app_handle().clone());
|
||||
let event_sender = EVENT_BROADCAST.lock().unwrap().clone();
|
||||
Json(crate::global_shortcuts::register(app_handle, event_sender, payload.0).await)
|
||||
@@ -873,7 +872,7 @@ pub struct ShortcutValidationResponse {
|
||||
/// Validates a shortcut string without registering it.
|
||||
/// Checks if the shortcut syntax is valid and if it
|
||||
/// conflicts with existing shortcuts.
|
||||
pub async fn validate_shortcut(_token: APIToken, payload: Json<ValidateShortcutRequest>) -> Json<ShortcutValidationResponse> {
|
||||
pub async fn validate_shortcut(payload: Json<ValidateShortcutRequest>) -> Json<ShortcutValidationResponse> {
|
||||
let shortcut = payload.shortcut.clone();
|
||||
|
||||
// Empty shortcuts are always valid (means "disabled"):
|
||||
@@ -924,13 +923,13 @@ pub async fn validate_shortcut(_token: APIToken, payload: Json<ValidateShortcutR
|
||||
/// Tauri shortcuts are temporarily unregistered and restored on resume.
|
||||
/// This is useful when opening a dialog to configure shortcuts, so the user can
|
||||
/// press the current shortcut to re-enter it without triggering the action.
|
||||
pub async fn suspend_shortcuts(_token: APIToken) -> Json<ShortcutResponse> {
|
||||
pub async fn suspend_shortcuts() -> Json<ShortcutResponse> {
|
||||
let app_handle = MAIN_WINDOW.lock().unwrap().as_ref().map(|window| window.app_handle().clone());
|
||||
Json(crate::global_shortcuts::suspend(app_handle).await)
|
||||
}
|
||||
|
||||
/// Resumes shortcut processing by re-registering all shortcuts with the OS.
|
||||
pub async fn resume_shortcuts(_token: APIToken) -> Json<ShortcutResponse> {
|
||||
pub async fn resume_shortcuts() -> Json<ShortcutResponse> {
|
||||
let app_handle = MAIN_WINDOW.lock().unwrap().as_ref().map(|window| window.app_handle().clone());
|
||||
let event_sender = EVENT_BROADCAST.lock().unwrap().clone();
|
||||
Json(crate::global_shortcuts::resume(app_handle, event_sender).await)
|
||||
|
||||
@@ -5,7 +5,6 @@ use axum::Json;
|
||||
use log::{debug, error, warn};
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::Serialize;
|
||||
use crate::api_token::APIToken;
|
||||
use crate::encryption::{EncryptedText, ENCRYPTION};
|
||||
|
||||
/// The process-wide clipboard instance. On Linux, retaining this instance keeps the app's
|
||||
@@ -76,7 +75,7 @@ fn release_clipboard<B>(clipboard: &mut Option<B>) -> bool {
|
||||
}
|
||||
|
||||
/// Sets the clipboard text to the provided encrypted text.
|
||||
pub async fn set_clipboard(_token: APIToken, encrypted_text: String) -> Json<SetClipboardResponse> {
|
||||
pub async fn set_clipboard(encrypted_text: String) -> Json<SetClipboardResponse> {
|
||||
//
|
||||
// The clipboard backend of the operating system may take a moment to answer, a failed write is
|
||||
// retried with a fresh backend, and the lock waits for a write still running. None of this
|
||||
|
||||
@@ -8,7 +8,6 @@ use once_cell::sync::Lazy;
|
||||
use tauri::{Manager, Url};
|
||||
use tauri_plugin_shell::process::{CommandChild, CommandEvent, TerminatedPayload};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
use crate::api_token::APIToken;
|
||||
use crate::runtime_api_token::API_TOKEN;
|
||||
use crate::app_window::change_location_to;
|
||||
use crate::runtime_certificate::CERTIFICATE_FINGERPRINT;
|
||||
@@ -109,7 +108,7 @@ fn describe_termination(payload: &TerminatedPayload) -> String {
|
||||
|
||||
/// 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.
|
||||
pub async fn dotnet_port(_token: APIToken) -> String {
|
||||
pub async fn dotnet_port() -> String {
|
||||
let dotnet_server_port = *DOTNET_SERVER_PORT;
|
||||
format!("{dotnet_server_port}")
|
||||
}
|
||||
@@ -287,7 +286,7 @@ pub fn start_dotnet_server<R: tauri::Runtime>(app_handle: tauri::AppHandle<R>) {
|
||||
}
|
||||
|
||||
/// This endpoint is called by the .NET server to signal that the server is ready.
|
||||
pub async fn dotnet_ready(_token: APIToken) {
|
||||
pub async fn dotnet_ready() {
|
||||
|
||||
// We create a manual scope for the lock to be released as soon as possible.
|
||||
// This is necessary because we cannot await any function while the lock is
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::api_token::APIToken;
|
||||
use axum::http::StatusCode;
|
||||
use axum::Json;
|
||||
use log::{debug, error, info, warn};
|
||||
@@ -58,7 +57,7 @@ static USER_LANGUAGE: OnceLock<String> = OnceLock::new();
|
||||
static INSTALLATION_KIND: OnceLock<InstallationKind> = OnceLock::new();
|
||||
|
||||
/// Returns the config directory.
|
||||
pub async fn get_config_directory(_token: APIToken) -> String {
|
||||
pub async fn get_config_directory() -> String {
|
||||
match CONFIG_DIRECTORY.get() {
|
||||
Some(config_directory) => config_directory.clone(),
|
||||
None => String::from(""),
|
||||
@@ -66,7 +65,7 @@ pub async fn get_config_directory(_token: APIToken) -> String {
|
||||
}
|
||||
|
||||
/// Returns the data directory.
|
||||
pub async fn get_data_directory(_token: APIToken) -> String {
|
||||
pub async fn get_data_directory() -> String {
|
||||
match DATA_DIRECTORY.get() {
|
||||
Some(data_directory) => data_directory.clone(),
|
||||
None => String::from(""),
|
||||
@@ -74,7 +73,7 @@ pub async fn get_data_directory(_token: APIToken) -> String {
|
||||
}
|
||||
|
||||
/// Returns the current user's username.
|
||||
pub async fn read_user_name(_token: APIToken) -> String {
|
||||
pub async fn read_user_name() -> String {
|
||||
whoami::username().unwrap_or_else(|e| {
|
||||
error!("Failed to read the current OS username: {e}.");
|
||||
String::new()
|
||||
@@ -129,7 +128,7 @@ pub struct RuntimeInfo {
|
||||
pub installation_kind: InstallationKind,
|
||||
}
|
||||
|
||||
pub async fn get_runtime_info(_token: APIToken) -> Json<RuntimeInfo> {
|
||||
pub async fn get_runtime_info() -> Json<RuntimeInfo> {
|
||||
Json(RuntimeInfo {
|
||||
working_directory: env::current_dir()
|
||||
.map(|path| path.to_string_lossy().into_owned())
|
||||
@@ -545,7 +544,7 @@ fn detect_user_language() -> (String, LanguageDetectionSource) {
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn read_user_language(_token: APIToken) -> String {
|
||||
pub async fn read_user_language() -> String {
|
||||
USER_LANGUAGE
|
||||
.get_or_init(|| {
|
||||
let (user_language, source) = detect_user_language();
|
||||
@@ -642,13 +641,13 @@ struct EnterpriseSourceData {
|
||||
encryption_secret: String,
|
||||
}
|
||||
|
||||
pub async fn read_enterprise_env_config_encryption_secret(_token: APIToken) -> Result<String, (StatusCode, String)> {
|
||||
pub async fn read_enterprise_env_config_encryption_secret() -> Result<String, (StatusCode, String)> {
|
||||
debug!("Trying to read the effective enterprise configuration encryption secret.");
|
||||
read_enterprise_sources(|| resolve_effective_enterprise_secret_source().encryption_secret).await
|
||||
}
|
||||
|
||||
/// Returns all enterprise configurations from the effective source.
|
||||
pub async fn read_enterprise_configs(_token: APIToken) -> Result<Json<Vec<EnterpriseConfig>>, (StatusCode, String)> {
|
||||
pub async fn read_enterprise_configs() -> Result<Json<Vec<EnterpriseConfig>>, (StatusCode, String)> {
|
||||
info!("Trying to read the effective enterprise configurations.");
|
||||
read_enterprise_sources(|| Json(resolve_effective_enterprise_config_source().configs)).await
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ use file_format::FileFormat;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
use tauri_plugin_dialog::{DialogExt, FileDialogBuilder};
|
||||
use crate::api_token::APIToken;
|
||||
use crate::app_window::MAIN_WINDOW;
|
||||
use crate::file_data::is_executable_content;
|
||||
|
||||
@@ -119,7 +118,6 @@ pub struct PreviousFile {
|
||||
|
||||
/// Let the user select a directory.
|
||||
pub async fn select_directory(
|
||||
_token: APIToken,
|
||||
Query(query): Query<SelectDirectoryQuery>,
|
||||
previous_directory: Option<Json<PreviousDirectory>>,
|
||||
) -> Json<DirectorySelectionResponse> {
|
||||
@@ -174,7 +172,6 @@ pub async fn select_directory(
|
||||
|
||||
/// Let the user select a file.
|
||||
pub async fn select_file(
|
||||
_token: APIToken,
|
||||
payload: Json<SelectFileOptions>,
|
||||
) -> Json<FileSelectionResponse> {
|
||||
// Create a new file dialog builder:
|
||||
@@ -233,7 +230,6 @@ pub async fn select_file(
|
||||
|
||||
/// Let the user select some files.
|
||||
pub async fn select_files(
|
||||
_token: APIToken,
|
||||
payload: Json<SelectFileOptions>,
|
||||
) -> Json<FilesSelectionResponse> {
|
||||
// Create a new file dialog builder:
|
||||
@@ -282,7 +278,7 @@ pub async fn select_files(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn save_file(_token: APIToken, payload: Json<SaveFileOptions>) -> Json<FileSaveResponse> {
|
||||
pub async fn save_file(payload: Json<SaveFileOptions>) -> Json<FileSaveResponse> {
|
||||
// Create a new file dialog builder:
|
||||
let file_dialog = MAIN_WINDOW
|
||||
.lock()
|
||||
@@ -343,7 +339,6 @@ pub async fn save_file(_token: APIToken, payload: Json<SaveFileOptions>) -> Json
|
||||
}
|
||||
|
||||
pub async fn open_path_in_file_manager(
|
||||
_token: APIToken,
|
||||
payload: Json<OpenPathOptions>,
|
||||
) -> Json<OpenPathResponse> {
|
||||
let requested_path = PathBuf::from(payload.path.trim());
|
||||
@@ -417,7 +412,6 @@ async fn open_file_manager_target(requested_path: &Path) -> Result<(), String> {
|
||||
/// The page is best effort and never decides whether this succeeded: a viewer which cannot be told
|
||||
/// a page still shows the document, which is what the user asked for by clicking a source.
|
||||
pub async fn open_document(
|
||||
_token: APIToken,
|
||||
payload: Json<OpenDocumentOptions>,
|
||||
) -> Json<OpenDocumentResponse> {
|
||||
let requested_path = PathBuf::from(payload.path.trim());
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::cmp::min;
|
||||
use std::collections::VecDeque;
|
||||
use std::convert::Infallible;
|
||||
use crate::api_token::APIToken;
|
||||
use crate::pandoc::PandocProcessBuilder;
|
||||
use crate::pdfium::{with_pdfium_access, PdfiumInit};
|
||||
use crate::prompt_injection::{Finding as PromptInjectionFinding, Sanitizer};
|
||||
@@ -504,7 +503,6 @@ fn filter_failed_error() -> ExtractionError {
|
||||
}
|
||||
|
||||
pub async fn extract_data(
|
||||
_token: APIToken,
|
||||
query: std::result::Result<Query<ExtractDataQuery>, QueryRejection>,
|
||||
) -> Sse<impl Stream<Item = std::result::Result<Event, Infallible>>> {
|
||||
let query = match query {
|
||||
|
||||
+2
-3
@@ -13,7 +13,6 @@ use log::{kv, Level};
|
||||
use log::kv::{Key, Value, VisitSource};
|
||||
use axum::Json;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::api_token::APIToken;
|
||||
use crate::environment::{is_dev, is_flatpak};
|
||||
|
||||
const FLATPAK_PERSISTENT_DATA_DIRECTORY: &str = "/var/data";
|
||||
@@ -406,7 +405,7 @@ fn file_logger_format(
|
||||
write!(w, "{}", record.args())
|
||||
}
|
||||
|
||||
pub async fn get_log_paths(_token: APIToken) -> Json<LogPathsResponse> {
|
||||
pub async fn get_log_paths() -> Json<LogPathsResponse> {
|
||||
Json(LogPathsResponse {
|
||||
log_startup_path: LOG_STARTUP_PATH.get().expect("No startup log path was set").clone(),
|
||||
log_app_path: LOG_APP_PATH.get().expect("No app log path was set").clone(),
|
||||
@@ -461,7 +460,7 @@ fn log_with_level(
|
||||
}
|
||||
|
||||
/// Logs an event from the .NET server.
|
||||
pub async fn log_event(_token: APIToken, Json(event): Json<LogEvent>) -> Json<LogEventResponse> {
|
||||
pub async fn log_event(Json(event): Json<LogEvent>) -> Json<LogEventResponse> {
|
||||
let level = parse_dotnet_log_level(&event.level);
|
||||
let message = event.message.as_str();
|
||||
let category = event.category.as_str();
|
||||
|
||||
@@ -36,8 +36,6 @@ use tokio_stream::StreamExt;
|
||||
use webm_iterable::matroska_spec::{Master, MatroskaSpec, SimpleBlock};
|
||||
use webm_iterable::{WebmIterator, WebmWriter, WriteOptions};
|
||||
|
||||
use crate::api_token::APIToken;
|
||||
|
||||
/// Sample rate required by the normalized WebM/Opus output contract.
|
||||
const OUTPUT_SAMPLE_RATE: u32 = 48_000;
|
||||
|
||||
@@ -333,7 +331,6 @@ impl MediaJob {
|
||||
|
||||
/// Registers and immediately schedules a media normalization job.
|
||||
pub async fn create_job(
|
||||
_token: APIToken,
|
||||
Json(request): Json<CreateMediaJobRequest>,
|
||||
) -> Result<Json<CreateMediaJobResponse>, (StatusCode, Json<MediaError>)> {
|
||||
let input_path = PathBuf::from(&request.input_path);
|
||||
@@ -415,7 +412,6 @@ async fn retain_terminal_job(job_id: String) {
|
||||
|
||||
/// Streams the current snapshot followed by live media job events.
|
||||
pub async fn get_job_events(
|
||||
_token: APIToken,
|
||||
Path(job_id): Path<String>,
|
||||
) -> Result<Sse<impl Stream<Item = Result<Event, Infallible>>>, StatusCode> {
|
||||
let job = JOBS.read().unwrap().get(&job_id).cloned().ok_or(StatusCode::NOT_FOUND)?;
|
||||
@@ -431,7 +427,7 @@ pub async fn get_job_events(
|
||||
}
|
||||
|
||||
/// Requests cooperative cancellation of a running media job.
|
||||
pub async fn cancel_job(_token: APIToken, Path(job_id): Path<String>) -> impl IntoResponse {
|
||||
pub async fn cancel_job(Path(job_id): Path<String>) -> impl IntoResponse {
|
||||
match JOBS.read().unwrap().get(&job_id) {
|
||||
Some(job) => {
|
||||
job.cancelled.store(true, Ordering::Relaxed);
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
//! pages, each with its own content, title, description, and authors. Those go through the
|
||||
//! batch endpoint, which filters them in one request instead of one round trip per field.
|
||||
|
||||
use crate::api_token::APIToken;
|
||||
use axum::http::StatusCode;
|
||||
use axum::Json;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -46,7 +45,6 @@ pub struct SanitizeBatchResponse {
|
||||
}
|
||||
|
||||
pub async fn sanitize(
|
||||
_token: APIToken,
|
||||
Json(request): Json<SanitizeRequest>,
|
||||
) -> Result<Json<SanitizeResponse>, (StatusCode, String)> {
|
||||
//
|
||||
@@ -73,7 +71,6 @@ pub async fn sanitize(
|
||||
}
|
||||
|
||||
pub async fn sanitize_batch(
|
||||
_token: APIToken,
|
||||
Json(request): Json<SanitizeBatchRequest>,
|
||||
) -> Result<Json<SanitizeBatchResponse>, (StatusCode, String)> {
|
||||
//
|
||||
|
||||
@@ -18,7 +18,6 @@ use qdrant_edge::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::Manager;
|
||||
|
||||
use crate::api_token::APIToken;
|
||||
use crate::environment::DATA_DIRECTORY;
|
||||
use crate::metadata::META_DATA;
|
||||
|
||||
@@ -472,7 +471,7 @@ fn qdrant_edge_base_path() -> QdrantEdgeResult<PathBuf> {
|
||||
.join("vector_database"))
|
||||
}
|
||||
|
||||
pub async fn qdrant_edge_info(_token: APIToken) -> Json<QdrantEdgeServiceInfo> {
|
||||
pub async fn qdrant_edge_info() -> Json<QdrantEdgeServiceInfo> {
|
||||
let (current_status, mut unavailable_reason) = {
|
||||
let status = QDRANT_EDGE_STATUS.lock().unwrap();
|
||||
(status.status, status.unavailable_reason.clone())
|
||||
@@ -513,37 +512,37 @@ fn read_qdrant_edge_info() -> Option<QdrantEdgeInfo> {
|
||||
.and_then(|database| database.info().ok())
|
||||
}
|
||||
|
||||
pub async fn ensure_qdrant_edge_store(_token: APIToken, Json(request): Json<EnsureQdrantEdgeStoreRequest>) -> Json<QdrantEdgeResponse<QdrantEdgeEnsureStoreResult>> {
|
||||
pub async fn ensure_qdrant_edge_store(Json(request): Json<EnsureQdrantEdgeStoreRequest>) -> Json<QdrantEdgeResponse<QdrantEdgeEnsureStoreResult>> {
|
||||
run_qdrant_edge_request(move |database| {
|
||||
database.ensure_store_exists(&request.store_name, &request.data_source_name, request.vector_size)
|
||||
}).await
|
||||
}
|
||||
|
||||
pub async fn insert_qdrant_edge_embedding(_token: APIToken, Json(request): Json<InsertQdrantEdgeEmbeddingRequest>) -> Json<QdrantEdgeResponse<()>> {
|
||||
pub async fn insert_qdrant_edge_embedding(Json(request): Json<InsertQdrantEdgeEmbeddingRequest>) -> Json<QdrantEdgeResponse<()>> {
|
||||
run_qdrant_edge_request(move |database| {
|
||||
database.insert_embedding(&request.store_name, request.points)
|
||||
}).await
|
||||
}
|
||||
|
||||
pub async fn search_qdrant_edge_embeddings(_token: APIToken, Json(request): Json<SearchQdrantEdgeEmbeddingRequest>) -> Json<QdrantEdgeResponse<Vec<QdrantEdgeSearchResult>>> {
|
||||
pub async fn search_qdrant_edge_embeddings(Json(request): Json<SearchQdrantEdgeEmbeddingRequest>) -> Json<QdrantEdgeResponse<Vec<QdrantEdgeSearchResult>>> {
|
||||
run_qdrant_edge_request(move |database| {
|
||||
database.search_embedding(&request.store_name, request.vector, request.max_matches, request.point_ids)
|
||||
}).await
|
||||
}
|
||||
|
||||
pub async fn delete_qdrant_edge_embedding_by_file(_token: APIToken, Json(request): Json<DeleteQdrantEdgeEmbeddingByFileRequest>) -> Json<QdrantEdgeResponse<()>> {
|
||||
pub async fn delete_qdrant_edge_embedding_by_file(Json(request): Json<DeleteQdrantEdgeEmbeddingByFileRequest>) -> Json<QdrantEdgeResponse<()>> {
|
||||
run_qdrant_edge_request(move |database| {
|
||||
database.delete_embedding_by_file(&request.store_name, &request.file_path)
|
||||
}).await
|
||||
}
|
||||
|
||||
pub async fn optimize_qdrant_edge_store(_token: APIToken, Json(request): Json<OptimizeQdrantEdgeStoreRequest>) -> Json<QdrantEdgeResponse<()>> {
|
||||
pub async fn optimize_qdrant_edge_store(Json(request): Json<OptimizeQdrantEdgeStoreRequest>) -> Json<QdrantEdgeResponse<()>> {
|
||||
run_qdrant_edge_request(move |database| {
|
||||
database.optimize_store(&request.store_name)
|
||||
}).await
|
||||
}
|
||||
|
||||
pub async fn delete_qdrant_edge_store(_token: APIToken, Json(request): Json<DeleteQdrantEdgeStoreRequest>) -> Json<QdrantEdgeResponse<()>> {
|
||||
pub async fn delete_qdrant_edge_store(Json(request): Json<DeleteQdrantEdgeStoreRequest>) -> Json<QdrantEdgeResponse<()>> {
|
||||
run_qdrant_edge_request(move |database| {
|
||||
database.delete_store(&request.store_name)
|
||||
}).await
|
||||
@@ -1302,7 +1301,6 @@ mod tests {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn a_request_waiting_for_the_database_leaves_the_runtime_free() {
|
||||
let request = optimize_qdrant_edge_store(
|
||||
APIToken::from_hex_text("test"),
|
||||
Json(OptimizeQdrantEdgeStoreRequest { store_name: SEARCH_STORE.to_string() }),
|
||||
);
|
||||
|
||||
@@ -1314,7 +1312,7 @@ mod tests {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn the_info_waiting_for_the_database_leaves_the_runtime_free() {
|
||||
let request = qdrant_edge_info(APIToken::from_hex_text("test"));
|
||||
let request = qdrant_edge_info();
|
||||
|
||||
let info = assert_runtime_stays_free(&QDRANT_EDGE_DATABASE, request).await;
|
||||
assert!(!info.is_available);
|
||||
|
||||
+137
-7
@@ -1,11 +1,13 @@
|
||||
use log::info;
|
||||
use once_cell::sync::Lazy;
|
||||
use axum::extract::DefaultBodyLimit;
|
||||
use axum::middleware;
|
||||
use axum::routing::{delete, get, post};
|
||||
use axum::Router;
|
||||
use axum_server::tls_rustls::RustlsConfig;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Once;
|
||||
use crate::api_token::APIToken;
|
||||
use crate::runtime_certificate::{CERTIFICATE, CERTIFICATE_PRIVATE_KEY};
|
||||
use crate::environment::is_dev;
|
||||
use crate::network::get_available_port;
|
||||
@@ -28,13 +30,9 @@ 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}'...");
|
||||
|
||||
let app = Router::new()
|
||||
/// Creates the router with every route of the runtime API, each of which requires the API token.
|
||||
fn create_router() -> Router {
|
||||
let router = Router::new()
|
||||
.route("/system/dotnet/port", get(crate::dotnet::dotnet_port))
|
||||
.route("/system/dotnet/ready", get(crate::dotnet::dotnet_ready))
|
||||
.route("/system/qdrant-edge/info", get(crate::qdrant_edge_database::qdrant_edge_info))
|
||||
@@ -91,6 +89,29 @@ pub fn start_runtime_api() {
|
||||
.route("/shortcuts/suspend", post(crate::app_window::suspend_shortcuts))
|
||||
.route("/shortcuts/resume", post(crate::app_window::resume_shortcuts));
|
||||
|
||||
require_api_token(router)
|
||||
}
|
||||
|
||||
/// Requires a valid API token for every route of the given router.
|
||||
///
|
||||
/// This layer is the only place where the runtime API checks the token, so the handlers take no
|
||||
/// `APIToken` argument. Asking each handler for it left a route open to every process on this
|
||||
/// machine as soon as one handler forgot it.
|
||||
///
|
||||
/// `route_layer` protects only the routes registered before it, so call this function after the
|
||||
/// last route. Unknown paths still answer 404 without checking the token.
|
||||
fn require_api_token(router: Router) -> Router {
|
||||
router.route_layer(middleware::from_extractor::<APIToken>())
|
||||
}
|
||||
|
||||
/// 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}'...");
|
||||
|
||||
let app = create_router();
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
install_rustls_crypto_provider();
|
||||
|
||||
@@ -159,3 +180,112 @@ pub(crate) mod test_support {
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::time::Duration;
|
||||
use axum::body::Body;
|
||||
use axum::http::{Request, StatusCode};
|
||||
use regex::Regex;
|
||||
use tokio::time::timeout;
|
||||
use tower::ServiceExt;
|
||||
use crate::runtime_api_token::API_TOKEN;
|
||||
|
||||
/// How long a route may take to refuse a request without the token. The refusal needs no work,
|
||||
/// so a route which takes longer has passed the request on to its handler. In a test, some
|
||||
/// handlers then never answer, for example one which waits for the main window, and others
|
||||
/// panic because the app around them is missing.
|
||||
const REFUSAL_LIMIT: Duration = Duration::from_secs(1);
|
||||
|
||||
/// One route which `create_router` registers.
|
||||
struct RegisteredRoute {
|
||||
method: String,
|
||||
path: String,
|
||||
}
|
||||
|
||||
/// Reads every route from the source of `create_router`, so that a new route is checked
|
||||
/// without anybody having to add it to a test.
|
||||
fn registered_routes() -> Vec<RegisteredRoute> {
|
||||
let source = include_str!("runtime_api.rs");
|
||||
let start = source.find("fn create_router()").unwrap();
|
||||
let end = start + source[start..].find("\n}").unwrap();
|
||||
let create_router_source = &source[start..end];
|
||||
|
||||
let route_pattern = Regex::new(r#"\.route\("([^"]+)",\s*(get|post|put|patch|delete)\("#).unwrap();
|
||||
let path_parameter_pattern = Regex::new(r"\{[^}]+\}").unwrap();
|
||||
let routes: Vec<RegisteredRoute> = route_pattern
|
||||
.captures_iter(create_router_source)
|
||||
.map(|route| RegisteredRoute {
|
||||
method: route[2].to_uppercase(),
|
||||
path: path_parameter_pattern.replace_all(&route[1], "x").into_owned(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
//
|
||||
// A route which the pattern cannot read would silently drop out of the check, for example
|
||||
// one whose methods are not a plain get, post, put, patch, or delete call. Extend the
|
||||
// pattern then.
|
||||
//
|
||||
assert_eq!(
|
||||
routes.len(),
|
||||
create_router_source.matches(".route(").count(),
|
||||
"The test could not read every route of create_router.",
|
||||
);
|
||||
|
||||
assert!(!routes.is_empty(), "The test found no route in create_router.");
|
||||
routes
|
||||
}
|
||||
|
||||
/// Sends one request through the router, without a server, and returns the status of the response.
|
||||
async fn send(router: Router, request: Request<Body>) -> StatusCode {
|
||||
router.oneshot(request).await.unwrap().status()
|
||||
}
|
||||
|
||||
/// Builds a request with the given token header, or without one.
|
||||
fn request(method: &str, uri: &str, token: Option<&str>, body: &'static str) -> Request<Body> {
|
||||
let mut builder = Request::builder()
|
||||
.method(method)
|
||||
.uri(uri)
|
||||
.header("content-type", "application/json");
|
||||
|
||||
if let Some(token) = token {
|
||||
builder = builder.header("token", token);
|
||||
}
|
||||
|
||||
builder.body(Body::from(body)).unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn require_api_token_lets_only_the_valid_token_through() {
|
||||
let router = require_api_token(Router::new().route("/probe", get(|| async {})));
|
||||
|
||||
assert_eq!(send(router.clone(), request("GET", "/probe", None, "")).await, StatusCode::UNAUTHORIZED);
|
||||
assert_eq!(send(router.clone(), request("GET", "/probe", Some("0123abcd"), "")).await, StatusCode::UNAUTHORIZED);
|
||||
assert_eq!(send(router, request("GET", "/probe", Some(API_TOKEN.to_hex_text()), "")).await, StatusCode::OK);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn every_route_requires_the_api_token() {
|
||||
let mut unprotected_routes = Vec::new();
|
||||
for route in registered_routes() {
|
||||
// A task of its own turns a panicking handler into a finding instead of ending the test.
|
||||
let answer = tokio::spawn(send(create_router(), request(&route.method, &route.path, None, "")));
|
||||
match timeout(REFUSAL_LIMIT, answer).await {
|
||||
Ok(Ok(StatusCode::UNAUTHORIZED)) => {},
|
||||
Ok(Ok(status)) => unprotected_routes.push(format!("{} {} answered {status}", route.method, route.path)),
|
||||
Ok(Err(_)) => unprotected_routes.push(format!("{} {} panicked in its handler", route.method, route.path)),
|
||||
Err(_) => unprotected_routes.push(format!("{} {} did not refuse within {REFUSAL_LIMIT:?}", route.method, route.path)),
|
||||
}
|
||||
}
|
||||
|
||||
assert!(unprotected_routes.is_empty(), "These routes answer without the API token: {unprotected_routes:#?}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_valid_token_gets_through_to_the_handler() {
|
||||
// The handler fails on the incomplete body, before it touches any file.
|
||||
let with_token = request("POST", "/image/prepare", Some(API_TOKEN.to_hex_text()), "{}");
|
||||
assert_eq!(send(create_router(), with_token).await, StatusCode::UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
}
|
||||
@@ -26,11 +26,4 @@ where
|
||||
None => Err(StatusCode::UNAUTHORIZED),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The API token error types.
|
||||
#[derive(Debug)]
|
||||
pub enum APITokenError {
|
||||
Missing,
|
||||
Invalid,
|
||||
}
|
||||
@@ -2,7 +2,6 @@ use axum::Json;
|
||||
use keyring_core::{Entry, Error as KeyringError};
|
||||
use log::{debug, error, info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::api_token::APIToken;
|
||||
use crate::encryption::{EncryptedText, ENCRYPTION};
|
||||
|
||||
/// A structured issue reported by the native credential store.
|
||||
@@ -96,7 +95,7 @@ where
|
||||
}
|
||||
|
||||
/// Stores a secret in the secret store using the operating system's keyring.
|
||||
pub async fn store_secret(_token: APIToken, request: Json<StoreSecret>) -> Json<StoreSecretResponse> {
|
||||
pub async fn store_secret(request: Json<StoreSecret>) -> Json<StoreSecretResponse> {
|
||||
match run_secret_store_request(move || store_secret_sync(request)).await {
|
||||
Ok(response) => response,
|
||||
Err(issue) => Json(StoreSecretResponse {
|
||||
@@ -173,7 +172,7 @@ pub struct StoreSecretResponse {
|
||||
}
|
||||
|
||||
/// Retrieves a secret from the secret store using the operating system's keyring.
|
||||
pub async fn get_secret(_token: APIToken, request: Json<RequestSecret>) -> Json<RequestedSecret> {
|
||||
pub async fn get_secret(request: Json<RequestSecret>) -> Json<RequestedSecret> {
|
||||
match run_secret_store_request(move || get_secret_sync(request)).await {
|
||||
Ok(response) => response,
|
||||
Err(issue) => Json(RequestedSecret {
|
||||
@@ -268,7 +267,7 @@ pub struct RequestedSecret {
|
||||
}
|
||||
|
||||
/// Deletes a secret from the secret store using the operating system's keyring.
|
||||
pub async fn delete_secret(_token: APIToken, request: Json<RequestSecret>) -> Json<DeleteSecretResponse> {
|
||||
pub async fn delete_secret(request: Json<RequestSecret>) -> Json<DeleteSecretResponse> {
|
||||
match run_secret_store_request(move || delete_secret_sync(request)).await {
|
||||
Ok(response) => response,
|
||||
Err(issue) => Json(DeleteSecretResponse {
|
||||
|
||||
@@ -2,7 +2,6 @@ use axum::Json;
|
||||
use log::{error, info};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
use crate::api_token::APIToken;
|
||||
|
||||
/// The directory the app creates its shareable plugin archives in. Keep in sync with
|
||||
/// PluginShareService.TEMPORARY_ARCHIVE_DIRECTORY on the .NET side.
|
||||
@@ -23,7 +22,7 @@ pub struct ShareFileResponse {
|
||||
issue: String,
|
||||
}
|
||||
|
||||
pub async fn share_file(_token: APIToken, Json(request): Json<ShareFileRequest>) -> Json<ShareFileResponse> {
|
||||
pub async fn share_file(Json(request): Json<ShareFileRequest>) -> Json<ShareFileResponse> {
|
||||
let path = PathBuf::from(request.file_path.trim());
|
||||
if path.as_os_str().is_empty() {
|
||||
return failure("The file path is empty.");
|
||||
|
||||
@@ -10,7 +10,6 @@ use tauri::path::BaseDirectory;
|
||||
use tauri::Manager;
|
||||
use tokenizers::tokenizer::Tokenizer;
|
||||
|
||||
use crate::api_token::APIToken;
|
||||
use crate::environment::DATA_DIRECTORY;
|
||||
|
||||
const DEFAULT_TOKENIZER_RESOURCE_PATH: &str = "resources/tokenizers/tokenizer.json";
|
||||
@@ -103,28 +102,28 @@ pub fn set_default_tokenizer_path(app_handle: tauri::AppHandle) {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn token_count(_token: APIToken, req: Json<SetTokenText>) -> Json<TokenizerResponse> {
|
||||
pub async fn token_count(req: Json<SetTokenText>) -> Json<TokenizerResponse> {
|
||||
match run_tokenizer_work(move || get_token_count(&req.tokenizer_path, &req.text)).await {
|
||||
Ok(count) => Json(TokenizerResponse::available(count)),
|
||||
Err(e) => Json(TokenizerResponse::unavailable(e)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn validate_tokenizer(_token: APIToken, payload: Json<TokenizerPath>) -> Json<TokenizerResponse> {
|
||||
pub async fn validate_tokenizer(payload: Json<TokenizerPath>) -> Json<TokenizerResponse> {
|
||||
match run_tokenizer_work(move || handle_tokenizer_validate(&PathBuf::from(payload.file_path.clone()))).await {
|
||||
Ok(count) => Json(TokenizerResponse::available(count)),
|
||||
Err(e) => Json(TokenizerResponse::unavailable(e)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn store_tokenizer(_token: APIToken, payload: Json<TokenizerStorage>) -> Json<TokenizerResponse> {
|
||||
pub async fn store_tokenizer(payload: Json<TokenizerStorage>) -> Json<TokenizerResponse> {
|
||||
match run_tokenizer_work(move || handle_tokenizer_store(&payload).map_err(|e| e.to_string())).await {
|
||||
Ok(dest_path) => Json(TokenizerResponse::stored(dest_path)),
|
||||
Err(e) => Json(TokenizerResponse::unavailable(e)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_tokenizer(_token: APIToken, payload: Json<TokenizerDelete>) -> Json<TokenizerResponse> {
|
||||
pub async fn delete_tokenizer(payload: Json<TokenizerDelete>) -> Json<TokenizerResponse> {
|
||||
match run_tokenizer_work(move || handle_tokenizer_delete(&payload).map_err(|e| e.to_string())).await {
|
||||
Ok(_) => Json(TokenizerResponse::stored(String::new())),
|
||||
Err(e) => Json(TokenizerResponse::unavailable(e)),
|
||||
@@ -344,7 +343,6 @@ mod tests {
|
||||
fs::write(&tokenizer_path, "This is not a tokenizer.").unwrap();
|
||||
|
||||
let request = token_count(
|
||||
APIToken::from_hex_text("test"),
|
||||
Json(SetTokenText {
|
||||
text: "Hello, world!".to_string(),
|
||||
tokenizer_path: tokenizer_path.to_string_lossy().to_string(),
|
||||
|
||||
Reference in new issue
Block a user