diff --git a/.gitignore b/.gitignore index e68a3e1e..8ee21062 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ NDK *.sqlite* *.db *.db-journal + +.vscode diff --git a/README.md b/README.md index 4766e574..2f86b69d 100644 --- a/README.md +++ b/README.md @@ -60,16 +60,31 @@ Available options: # Additional regex CORS origins to allow (e.g. for sideloaded browser extensions) #cors_regex = ["chrome-extension://yourextensionidhere"] + +# Allow official ActivityWatch Chrome extension? (default: true) +#cors_allow_aw_chrome_extension = true + +# Allow all Firefox extensions? (default: false, DANGEROUS) +#cors_allow_all_mozilla_extension = false ``` +#### Persistence and Settings UI + +The CORS-related settings (`cors`, `cors_regex`, `cors_allow_aw_chrome_extension`, and `cors_allow_all_mozilla_extension`) follow a specific persistence logic: + +- **First Start**: These variables are only taken into account on the first server start, at which point they are added to the database. +- **Management**: Once added, they can be managed and edited via the **Settings UI** in the web interface. +- **Precedence**: Values in the database take precedence over the configuration file on subsequent starts. +- **Warning**: If these keys are deleted from the database, the server will again use the values from the configuration file to re-populate them. This ensures that the fields are always present, either from your manual settings or your initial configuration. + #### Custom CORS Origins By default, the server allows requests from: - The server's own origin (`http://127.0.0.1:`, `http://localhost:`) -- The official Chrome extension (`chrome-extension://nglaklhklhcoonedhgnpgddginnjdadi`) -- All Firefox extensions (`moz-extension://.*`) +- The official Chrome extension (`chrome-extension://nglaklhklhcoonedhgnpgddginnjdadi`) if `cors_allow_aw_chrome_extension` is true (default). +- All Firefox extensions (`moz-extension://.*`) ONLY IF `cors_allow_all_mozilla_extension` is set to true. -To allow additional origins (e.g. a sideloaded Chrome extension), add them to your config: +To allow additional origins (e.g. a sideloaded Chrome extension), add them to your `cors` or `cors_regex` config: ```toml # Allow a specific sideloaded Chrome extension diff --git a/aw-server/src/config.rs b/aw-server/src/config.rs index 35ac435c..c4c8bdea 100644 --- a/aw-server/src/config.rs +++ b/aw-server/src/config.rs @@ -36,6 +36,12 @@ pub struct AWConfig { #[serde(default = "default_cors")] pub cors_regex: Vec, + #[serde(default = "default_true")] + pub cors_allow_aw_chrome_extension: bool, + + #[serde(default = "default_false")] + pub cors_allow_all_mozilla_extension: bool, + // A mapping of watcher names to paths where the // custom visualizations are located. #[serde(default = "default_custom_static")] @@ -50,6 +56,8 @@ impl Default for AWConfig { testing: default_testing(), cors: default_cors(), cors_regex: default_cors(), + cors_allow_aw_chrome_extension: default_true(), + cors_allow_all_mozilla_extension: default_false(), custom_static: default_custom_static(), } } @@ -91,6 +99,14 @@ fn default_testing() -> bool { is_testing() } +fn default_true() -> bool { + true +} + +fn default_false() -> bool { + false +} + fn default_port() -> u16 { if is_testing() { 5666 diff --git a/aw-server/src/endpoints/cors.rs b/aw-server/src/endpoints/cors.rs index 530be147..12e9da8a 100644 --- a/aw-server/src/endpoints/cors.rs +++ b/aw-server/src/endpoints/cors.rs @@ -9,18 +9,23 @@ pub fn cors(config: &AWConfig) -> rocket_cors::Cors { let mut allowed_exact_origins = vec![root_url, root_url_localhost]; allowed_exact_origins.extend(config.cors.clone()); - if config.testing { - allowed_exact_origins.push("http://127.0.0.1:27180".to_string()); - allowed_exact_origins.push("http://localhost:27180".to_string()); + let mut allowed_regex_origins = config.cors_regex.clone(); + + if config.cors_allow_aw_chrome_extension { + allowed_regex_origins.push("chrome-extension://nglaklhklhcoonedhgnpgddginnjdadi".to_string()); } - let mut allowed_regex_origins = vec![ - "chrome-extension://nglaklhklhcoonedhgnpgddginnjdadi".to_string(), + + if config.cors_allow_all_mozilla_extension { // Every version of a mozilla extension has its own ID to avoid fingerprinting, so we // unfortunately have to allow all extensions to have access to aw-server - "moz-extension://.*".to_string(), - ]; - allowed_regex_origins.extend(config.cors_regex.clone()); + allowed_regex_origins.push("moz-extension://.*".to_string()); + } + if config.testing { + allowed_exact_origins.extend(vec![ + "http://127.0.0.1:27180".to_string(), + "http://localhost:27180".to_string(), + ]); allowed_regex_origins.push("chrome-extension://.*".to_string()); } diff --git a/aw-server/src/endpoints/mod.rs b/aw-server/src/endpoints/mod.rs index f6c9271e..f6cbd2b2 100644 --- a/aw-server/src/endpoints/mod.rs +++ b/aw-server/src/endpoints/mod.rs @@ -127,11 +127,55 @@ fn get_file(file: PathBuf, state: &State) -> Option<(ContentType, V Some((content_type, asset)) } -pub fn build_rocket(server_state: ServerState, config: AWConfig) -> rocket::Rocket { +pub fn build_rocket( + server_state: ServerState, + mut config: AWConfig, +) -> rocket::Rocket { info!( "Starting aw-server-rust at {}:{}", config.address, config.port ); + { + let db = server_state.datastore.lock().unwrap(); + let parse_cors_list = |raw: &str| -> Vec { + serde_json::from_str::(raw) + .unwrap_or_else(|_| raw.trim_matches('"').to_string()) + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect() + }; + let parse_bool = |raw: &str| -> bool { + serde_json::from_str::(raw).unwrap_or_else(|_| raw.trim_matches('"') == "true") + }; + // Sync settings between Config file and Database. + // On the first run (when a key is missing in the DB), we seed the DB with the value from the config file. + // On subsequent runs, we always prefer the DB value (which might have been changed via the UI). + let sync = + |key: &str, current_val: &mut String, to_save: String| match db.get_key_value(key) { + Ok(raw) => *current_val = raw, + Err(_) => { + db.set_key_value(key, &to_save).ok(); + *current_val = to_save; + } + }; + + let mut raw_cors = String::new(); + sync("settings.cors", &mut raw_cors, serde_json::to_string(&config.cors.join(",")).unwrap()); + config.cors = parse_cors_list(&raw_cors); + + let mut raw_cors_regex = String::new(); + sync("settings.cors_regex", &mut raw_cors_regex, serde_json::to_string(&config.cors_regex.join(",")).unwrap()); + config.cors_regex = parse_cors_list(&raw_cors_regex); + + let mut raw_chrome = String::new(); + sync("settings.cors_allow_aw_chrome_extension", &mut raw_chrome, serde_json::to_string(&config.cors_allow_aw_chrome_extension).unwrap()); + config.cors_allow_aw_chrome_extension = parse_bool(&raw_chrome); + + let mut raw_mozilla = String::new(); + sync("settings.cors_allow_all_mozilla_extension", &mut raw_mozilla, serde_json::to_string(&config.cors_allow_all_mozilla_extension).unwrap()); + config.cors_allow_all_mozilla_extension = parse_bool(&raw_mozilla); + } let cors = cors::cors(&config); let hostcheck = hostcheck::HostCheck::new(&config); let custom_static = config.custom_static.clone();