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).
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