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
35 changes: 35 additions & 0 deletions aw_watcher_afk/afk.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import json
import logging
import os
import platform
import requests
from datetime import datetime, timedelta, timezone
from time import sleep

Expand Down Expand Up @@ -28,6 +30,34 @@
td1ms = timedelta(milliseconds=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"}

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 The "charset" key is set as a standalone HTTP header rather than as a parameter inside the Content-Type value. Per RFC 7231, charset belongs in the media type parameter: application/json; charset=utf-8. As a separate header named charset, most servers will ignore it entirely.

Suggested change
headers = {"Content-type": "application/json", "charset": "utf-8"}
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 +33 to +58

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 Monkey-patch depends on private aw_client internals

_patch_client_auth imports always_raise_for_request_errors from aw_client.client and replaces the private _get/_post/_delete instance methods, and also closes over client._url. All of these are undocumented private APIs. Any refactor of aw-client can silently break auth with no clear diagnostic.



class Settings:
def __init__(self, config_section, timeout=None, poll_time=None):
# Time without input before we're considering the user as AFK
Expand All @@ -48,6 +78,11 @@ def __init__(self, args, testing=False):
self.client = ActivityWatchClient(
"aw-watcher-afk", host=args.host, port=args.port, testing=testing
)

if args.auth_user and args.auth_password:
_patch_client_auth(self.client, args.auth_user, args.auth_password)
logger.info("HTTP Basic Auth enabled for user: %s", args.auth_user)
Comment on lines +82 to +84

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 Silent auth skip when only one credential is provided

When a user sets auth_user but forgets auth_password (or vice versa), the condition evaluates to False and auth is silently disabled — no warning, no error. This is especially confusing because the config write was intentional, but the watcher will connect without credentials and fail with an HTTP 401 from nginx, with no indication the auth config was ignored.


self.bucketname = "{}_{}".format(
self.client.client_name, self.client.client_hostname
)
Expand Down
24 changes: 22 additions & 2 deletions aw_watcher_afk/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
[aw-watcher-afk]
timeout = 180
poll_time = 5
host = ""
port = ""
auth_user = ""
auth_password = ""

[aw-watcher-afk-testing]
timeout = 20
Expand All @@ -26,12 +30,16 @@ def parse_args():

default_poll_time = config["poll_time"]
default_timeout = config["timeout"]
default_host = config.get("host", "") or None
default_port = config.get("port", "") or None
default_auth_user = config.get("auth_user", "")
default_auth_password = config.get("auth_password", "")

parser = argparse.ArgumentParser(
description="A watcher for keyboard and mouse input to detect AFK state."
)
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", help="run in testing mode"
)
Expand All @@ -47,5 +55,17 @@ def parse_args():
parser.add_argument(
"--poll-time", dest="poll_time", type=float, default=default_poll_time
)
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 +64 to +69

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 --auth-password is visible in process listings

Passing a password via CLI argument makes it readable to any user on the system via ps aux or /proc/<pid>/cmdline. The help text doesn't warn about this. Consider noting in the help string that using the TOML config file is more secure, or supporting an environment variable fallback (e.g. AW_AUTH_PASSWORD).

parsed_args = parser.parse_args()
return parsed_args