Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ NDK
*.sqlite*
*.db
*.db-journal

.vscode
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:<port>`, `http://localhost:<port>`)
- 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
Expand Down
16 changes: 16 additions & 0 deletions aw-server/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ pub struct AWConfig {
#[serde(default = "default_cors")]
pub cors_regex: Vec<String>,

#[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")]
Expand All @@ -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(),
}
}
Expand Down Expand Up @@ -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
Expand Down
21 changes: 13 additions & 8 deletions aw-server/src/endpoints/cors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

Expand Down
46 changes: 45 additions & 1 deletion aw-server/src/endpoints/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,55 @@ fn get_file(file: PathBuf, state: &State<ServerState>) -> Option<(ContentType, V
Some((content_type, asset))
}

pub fn build_rocket(server_state: ServerState, config: AWConfig) -> rocket::Rocket<rocket::Build> {
pub fn build_rocket(
server_state: ServerState,
mut config: AWConfig,
) -> rocket::Rocket<rocket::Build> {
info!(
"Starting aw-server-rust at {}:{}",
config.address, config.port
);
{
let db = server_state.datastore.lock().unwrap();
let parse_cors_list = |raw: &str| -> Vec<String> {
serde_json::from_str::<String>(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::<bool>(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).
Comment on lines +151 to +153
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems problematic. It's not acceptable to have file-level config options being ignored by auto-set db-values. Makes the config file mostly useless/unreliable. Not really any "sync" happening here.

imo, order of settings/config precedence in server should be cli options -> env vars -> config file -> webui/api settings -> defaults.

One way to resolve this would be to make the webui config write directly to the config file, would effectively put them in sync (but still not a fan of exposing server config like this in api/webui tbh). But that might require a API endpoint different from /settings, maybe /config - but really unsure about this, just an idea.

Simplest would probably be to just prefer the config over the db settings and disable the webui settings if they are explicitly set in config/env/cli.

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();
Expand Down