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
24 changes: 22 additions & 2 deletions aw_watcher_window/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
exclude_titles = []
poll_time = 1.0
strategy_macos = "swift"
host = ""
port = ""
auth_user = ""
auth_password = ""
Comment on lines +13 to +14
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 security Password written to on-disk config file in plaintext

When aw-watcher-window writes or reads its TOML config, auth_password is stored as a plaintext string in the file. Any process or user with read access to the config directory can extract the nginx credentials. Consider documenting this limitation prominently, and potentially advising users to use the CLI flag with an environment variable (e.g., --auth-password "$AW_AUTH_PASSWORD") instead of persisting the secret in the TOML file.

""".strip()


Expand All @@ -22,12 +26,16 @@ def parse_args():
default_exclude_title = config["exclude_title"]
default_exclude_titles = config["exclude_titles"]
default_strategy_macos = config["strategy_macos"]
default_host = config["host"] or None
default_port = config["port"] or None
default_auth_user = config["auth_user"]
default_auth_password = config["auth_password"]

parser = argparse.ArgumentParser(
description="A cross platform window watcher for Activitywatch.\nSupported on: Linux (X11), macOS and Windows."
)
parser.add_argument("--host", dest="host")
parser.add_argument("--port", dest="port")
parser.add_argument("--host", dest="host", default=default_host)
parser.add_argument("--port", dest="port", default=default_port)
parser.add_argument("--testing", dest="testing", action="store_true")
parser.add_argument(
"--exclude-title",
Expand All @@ -43,6 +51,18 @@ def parse_args():
help="Exclude window titles by regular expression. Can specify multiple times."
)
parser.add_argument("--verbose", dest="verbose", action="store_true")
parser.add_argument(
"--auth-user",
dest="auth_user",
default=default_auth_user,
help="Username for HTTP Basic Auth (for nginx-proxied servers)",
)
parser.add_argument(
"--auth-password",
dest="auth_password",
default=default_auth_password,
help="Password for HTTP Basic Auth (for nginx-proxied servers)",
)
Comment on lines +60 to +65
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 security CLI password visible in process list

Passing --auth-password secret on the command line exposes the credential in ps aux output to any user on the same machine. A common mitigation is to read the value from an environment variable (e.g., AW_AUTH_PASSWORD) or a dedicated secrets file, with the CLI flag as an optional override.

parser.add_argument(
"--poll-time", dest="poll_time", type=float, default=default_poll_time
)
Expand Down
34 changes: 34 additions & 0 deletions aw_watcher_window/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import json
import logging
import os
import re
import requests
import signal
import subprocess
import sys
Expand Down Expand Up @@ -40,6 +42,34 @@ def try_compile_title_regex(title):
exit(1)


def _patch_client_auth(client, user, password):
"""Replace aw-client HTTP methods to inject Basic Auth credentials."""
from aw_client.client import always_raise_for_request_errors

auth = requests.auth.HTTPBasicAuth(user, password)
_url = client._url

@always_raise_for_request_errors
def _get(self_ref, endpoint, params=None):
return requests.get(_url(endpoint), params=params, auth=auth)

@always_raise_for_request_errors
def _post(self_ref, endpoint, data, params=None):
headers = {"Content-type": "application/json", "charset": "utf-8"}
return requests.post(_url(endpoint), data=bytes(json.dumps(data), "utf8"), headers=headers, params=params, auth=auth)

@always_raise_for_request_errors
def _delete(self_ref, endpoint, data=None):
if data is None:
data = {}
headers = {"Content-type": "application/json"}
return requests.delete(_url(endpoint), data=json.dumps(data), headers=headers, auth=auth)

client._get = lambda endpoint, params=None: _get(client, endpoint, params)
client._post = lambda endpoint, data, params=None: _post(client, endpoint, data, params)
client._delete = lambda endpoint, data=None: _delete(client, endpoint, data)
Comment on lines +45 to +70
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Fragile import of private internal from aw-client

always_raise_for_request_errors is imported from aw_client.client, which is an internal implementation detail of the aw-client library, not part of its public API. Any aw-client version that renames, moves, or removes this function will cause an ImportError at runtime whenever auth is configured — leaving users with a completely broken watcher instead of a degraded one. A safer approach is to replicate the minimal raise-for-status logic inline, which is only three lines, rather than depending on a private symbol from a third-party package.



def main():
args = parse_args()

Expand All @@ -63,6 +93,10 @@ def main():
"aw-watcher-window", host=args.host, port=args.port, testing=args.testing
)

if args.auth_user and args.auth_password:
_patch_client_auth(client, args.auth_user, args.auth_password)
logger.info("HTTP Basic Auth enabled for user: %s", args.auth_user)

bucket_id = f"{client.client_name}_{client.client_hostname}"
event_type = "currentwindow"

Expand Down