mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-11 08:07:21 +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
+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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user