From 4df64a404a51d2521e13e7240bd7b01fdf91ba1b Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 17 Apr 2026 17:15:21 +0000 Subject: [PATCH 1/2] feat(client): auto-load local API keys for aw-client-rust --- Cargo.lock | 93 ++++++++++++++++++++++++++++++++++++ aw-client-rust/Cargo.toml | 5 +- aw-client-rust/src/lib.rs | 85 ++++++++++++++++++++++++++++++-- aw-client-rust/tests/test.rs | 82 +++++++++++++++++++++++++++++-- 4 files changed, 256 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5d12c475..580b4f3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -209,6 +209,7 @@ dependencies = [ "thiserror 1.0.69", "tokio", "tokio-test", + "toml", ] [[package]] @@ -1167,6 +1168,20 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper", + "rustls", + "tokio", + "tokio-rustls", +] + [[package]] name = "hyper-tls" version = "0.5.0" @@ -2140,6 +2155,7 @@ dependencies = [ "http 0.2.12", "http-body", "hyper", + "hyper-rustls", "hyper-tls", "ipnet", "js-sys", @@ -2149,6 +2165,8 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "rustls", + "rustls-native-certs", "rustls-pemfile", "serde", "serde_json", @@ -2157,6 +2175,7 @@ dependencies = [ "system-configuration", "tokio", "tokio-native-tls", + "tokio-rustls", "tower-service", "url", "wasm-bindgen", @@ -2165,6 +2184,20 @@ dependencies = [ "winreg", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rocket" version = "0.5.1" @@ -2334,6 +2367,30 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pemfile" version = "1.0.4" @@ -2343,6 +2400,16 @@ dependencies = [ "base64", ] +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.21" @@ -2410,6 +2477,16 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "sd-notify" version = "0.4.5" @@ -2832,6 +2909,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.17" @@ -3036,6 +3123,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.4" diff --git a/aw-client-rust/Cargo.toml b/aw-client-rust/Cargo.toml index 96e1ff04..216c4747 100644 --- a/aw-client-rust/Cargo.toml +++ b/aw-client-rust/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" authors = ["Johan Bjäreholt "] [dependencies] -reqwest = { version = "0.11", features = ["json", "blocking"] } +reqwest = { version = "0.11", default-features = false, features = ["json", "blocking", "rustls-tls-native-roots"] } gethostname = "0.4" serde = { version = "1.0", features = ["derive"] } phf = { version = "0.11", features = ["macros"] } @@ -16,9 +16,10 @@ tokio = { version = "1.28.2", features = ["rt"] } rand = "0.9" log = "0.4" libc = "0.2" -thiserror = "1.0" +thiserror = "1.0" dirs = "6.0" fs4 = { version = "0.13", features = ["sync"] } +toml = "0.8" [dev-dependencies] aw-datastore = { path = "../aw-datastore" } diff --git a/aw-client-rust/src/lib.rs b/aw-client-rust/src/lib.rs index 42368604..2714f540 100644 --- a/aw-client-rust/src/lib.rs +++ b/aw-client-rust/src/lib.rs @@ -10,9 +10,10 @@ pub mod classes; pub mod queries; pub mod single_instance; -use std::{collections::HashMap, error::Error}; +use std::{collections::HashMap, error::Error, fs, path::PathBuf}; use chrono::{DateTime, Utc}; +use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION}; use serde_json::{json, Map}; use single_instance::SingleInstance; use std::net::TcpStream; @@ -20,6 +21,26 @@ use std::time::Duration; pub use aw_models::{Bucket, BucketMetadata, Event}; +#[derive(serde::Deserialize, Default)] +struct LocalAuthConfig { + #[serde(default)] + api_key: Option, +} + +#[derive(serde::Deserialize, Default)] +struct LocalServerConfig { + #[serde(default)] + port: Option, + #[serde(default)] + auth: LocalAuthConfig, +} + +#[derive(Clone, Copy)] +struct ConfigCandidate { + filename: &'static str, + default_port: u16, +} + pub struct AwClient { client: reqwest::Client, #[allow(dead_code)] @@ -39,13 +60,69 @@ fn get_hostname() -> String { gethostname::gethostname().to_string_lossy().to_string() } +fn get_server_config_dir() -> Option { + Some( + dirs::config_dir()? + .join("activitywatch") + .join("aw-server-rust"), + ) +} + +fn load_local_api_key(host: &str, port: u16) -> Option { + if host != "127.0.0.1" && host != "localhost" { + return None; + } + + let config_dir = get_server_config_dir()?; + let candidates = [ + ConfigCandidate { + filename: "config.toml", + default_port: 5600, + }, + ConfigCandidate { + filename: "config-testing.toml", + default_port: 5666, + }, + ]; + + for candidate in candidates { + let path = config_dir.join(candidate.filename); + let content = match fs::read_to_string(path) { + Ok(content) => content, + Err(_) => continue, + }; + let config: LocalServerConfig = match toml::from_str(&content) { + Ok(config) => config, + Err(_) => continue, + }; + let configured_port = config.port.unwrap_or(candidate.default_port); + if configured_port == port { + return config.auth.api_key.filter(|api_key| !api_key.is_empty()); + } + } + + None +} + +fn build_client(api_key: Option) -> Result> { + let mut headers = HeaderMap::new(); + if let Some(api_key) = api_key { + let mut header_value = HeaderValue::from_str(&format!("Bearer {api_key}"))?; + header_value.set_sensitive(true); + headers.insert(AUTHORIZATION, header_value); + } + + Ok(reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(120)) + .default_headers(headers) + .build()?) +} + impl AwClient { pub fn new(host: &str, port: u16, name: &str) -> Result> { let baseurl = reqwest::Url::parse(&format!("http://{}:{}", host, port))?; let hostname = get_hostname(); - let client = reqwest::Client::builder() - .timeout(std::time::Duration::from_secs(120)) - .build()?; + let client = build_client(load_local_api_key(host, port))?; //TODO: change localhost string to 127.0.0.1 for feature parity let single_instance_name = format!("{}-at-{}-on-{}", name, host, port); let single_instance = single_instance::SingleInstance::new(single_instance_name.as_str())?; diff --git a/aw-client-rust/tests/test.rs b/aw-client-rust/tests/test.rs index d518aa0a..9a32293c 100644 --- a/aw-client-rust/tests/test.rs +++ b/aw-client-rust/tests/test.rs @@ -12,13 +12,18 @@ mod test { use aw_client_rust::Event; use chrono::{DateTime, Duration, Utc}; use serde_json::Map; + use std::fs; + use std::net::TcpListener; + use std::path::{Path, PathBuf}; use std::sync::Mutex; use std::thread; + use std::time::{SystemTime, UNIX_EPOCH}; use tokio_test::block_on; // A random port, but still not guaranteed to not be bound // FIXME: Bind to a port that is free for certain and use that for the client instead static PORT: u16 = 41293; + static ENV_LOCK: Mutex<()> = Mutex::new(()); fn wait_for_server(timeout_s: u32, client: &AwClient) { for i in 0.. { @@ -36,7 +41,7 @@ mod test { } } - fn setup_testserver() -> rocket::Shutdown { + fn setup_testserver(port: u16, api_key: Option<&str>) -> rocket::Shutdown { use aw_server::endpoints::AssetResolver; use aw_server::endpoints::ServerState; @@ -46,7 +51,8 @@ mod test { device_id: "test_id".to_string(), }; let mut aw_config = aw_server::config::AWConfig::default(); - aw_config.port = PORT; + aw_config.port = port; + aw_config.auth.api_key = api_key.map(str::to_owned); let server = aw_server::endpoints::build_rocket(state, aw_config); let server = block_on(server.ignite()).unwrap(); let shutdown_handler = server.shutdown(); @@ -58,6 +64,51 @@ mod test { shutdown_handler } + fn reserve_port() -> u16 { + TcpListener::bind("127.0.0.1:0") + .unwrap() + .local_addr() + .unwrap() + .port() + } + + fn write_server_config(port: u16, api_key: Option<&str>) -> PathBuf { + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let config_home = std::env::temp_dir().join(format!( + "aw-client-rust-config-{}-{}", + std::process::id(), + unique + )); + let config_dir = config_home.join("activitywatch").join("aw-server-rust"); + fs::create_dir_all(&config_dir).unwrap(); + + let mut content = format!("port = {port}\n"); + if let Some(api_key) = api_key { + content.push_str("\n[auth]\n"); + content.push_str(&format!("api_key = \"{api_key}\"\n")); + } + fs::write(config_dir.join("config.toml"), content).unwrap(); + + config_home + } + + fn with_config_home(config_home: &Path, f: impl FnOnce() -> T) -> T { + let _guard = ENV_LOCK.lock().unwrap(); + let old_value = std::env::var_os("XDG_CONFIG_HOME"); + std::env::set_var("XDG_CONFIG_HOME", config_home); + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)); + if let Some(old_value) = old_value { + std::env::set_var("XDG_CONFIG_HOME", old_value); + } else { + std::env::remove_var("XDG_CONFIG_HOME"); + } + let _ = fs::remove_dir_all(config_home); + result.unwrap() + } + #[test] fn test_full() { let clientname = "aw-client-rust-test"; @@ -65,7 +116,7 @@ mod test { let client: AwClient = AwClient::new("127.0.0.1", PORT, clientname).expect("Client creation failed"); - let shutdown_handler = setup_testserver(); + let shutdown_handler = setup_testserver(PORT, None); wait_for_server(20, &client); @@ -137,4 +188,29 @@ RETURN = events;", shutdown_handler.notify(); } + + #[test] + fn test_reads_api_key_from_matching_server_config() { + let clientname = "aw-client-rust-auth-test"; + let port = reserve_port(); + let config_home = write_server_config(port, Some("secret123")); + + with_config_home(&config_home, || { + let client: AwClient = + AwClient::new("127.0.0.1", port, clientname).expect("Client creation failed"); + let shutdown_handler = setup_testserver(port, Some("secret123")); + + wait_for_server(20, &client); + + let bucketname = format!("aw-client-rust-auth-test_{}", client.hostname); + client + .create_bucket_simple(&bucketname, "test-type") + .unwrap(); + + let bucket = client.get_bucket(&bucketname).unwrap(); + assert_eq!(bucket.id, bucketname); + + shutdown_handler.notify(); + }); + } } From 9611874ce10a8f68ff795f03de587d52052722bd Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 17 Apr 2026 17:34:35 +0000 Subject: [PATCH 2/2] chore(aw-client-rust): remove unused rand dep Greptile flagged rand as unused - confirmed no rand:: imports exist in src/ or tests/. --- aw-client-rust/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/aw-client-rust/Cargo.toml b/aw-client-rust/Cargo.toml index 216c4747..b191e592 100644 --- a/aw-client-rust/Cargo.toml +++ b/aw-client-rust/Cargo.toml @@ -13,7 +13,6 @@ serde_json = "1.0" chrono = { version = "0.4", features = ["serde"] } aw-models = { path = "../aw-models" } tokio = { version = "1.28.2", features = ["rt"] } -rand = "0.9" log = "0.4" libc = "0.2" thiserror = "1.0"