security+robustness(py/gui): thread-safety, upload hardening, keyring
macro_manager: - All macro reads/writes guarded by a lock; macros.json written atomically (temp + fsync + os.replace). Commands are copied under the lock and executed outside it so wait/keyboard steps don't block other threads. - last_used disk writes debounced (>=5s) instead of a full rewrite per press. - Validate key/hotkey names against pyautogui.KEYBOARD_KEYS and clamp wait ms; invalid input is skipped/logged, not blindly executed. web_server: - WebSocket receive loop always disconnects (fixes dead-socket leak); broadcast prunes failed sockets. - POST /api/execute offloaded to a thread executor so a long macro no longer blocks the event loop. - Image upload hardened: 5MB cap (413), real-image validation via Pillow, server-generated name under macro_images/, returns a relative path only, cleans up on failure. gui: - Execute macros off the Qt main thread (signal back to status bar); debounce resizeEvent and only rebuild when the column count changes. - Relay password stored in the OS keyring when available (plaintext JSON blanked; graceful fallback if no backend); delete dialogs name the macro; honor the minimize-to-tray setting; surface save failures. - Add media/system keys (volume, play/pause, track nav) to the macro editor. - THEME gains shared danger/accent-hover colors; drop dead imports. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+45
-4
@@ -5,6 +5,16 @@ import json
|
||||
import secrets
|
||||
from typing import Any, Optional
|
||||
|
||||
# Optional OS keyring for storing the relay password at rest. If the library or
|
||||
# a working backend is unavailable we fall back to the JSON file (now chmod 0600).
|
||||
try:
|
||||
import keyring as _keyring
|
||||
except Exception: # ImportError or any backend import-time failure
|
||||
_keyring = None
|
||||
|
||||
KEYRING_SERVICE = "MacroPad"
|
||||
KEYRING_USER = "relay"
|
||||
|
||||
|
||||
DEFAULT_SETTINGS = {
|
||||
"relay": {
|
||||
@@ -109,13 +119,44 @@ class SettingsManager:
|
||||
return self.get('relay.session_id')
|
||||
|
||||
def get_relay_password(self) -> str:
|
||||
"""Get the relay password."""
|
||||
"""Get the relay password, preferring the OS keyring when available."""
|
||||
if _keyring is not None:
|
||||
try:
|
||||
value = _keyring.get_password(KEYRING_SERVICE, KEYRING_USER)
|
||||
if value:
|
||||
return value
|
||||
except Exception:
|
||||
# Keyring backend missing/broken: fall back to JSON.
|
||||
pass
|
||||
return self.get('relay.password', '')
|
||||
|
||||
def set_relay_session_id(self, session_id: str):
|
||||
"""Store the relay session ID."""
|
||||
def set_relay_password(self, password: str) -> bool:
|
||||
"""Store the relay password, preferring the OS keyring when available.
|
||||
|
||||
When keyring works, the plaintext password is kept out of the JSON file.
|
||||
Never crashes if the keyring backend is missing; falls back to JSON.
|
||||
Returns True on successful persistence.
|
||||
"""
|
||||
password = password or ""
|
||||
stored_in_keyring = False
|
||||
if _keyring is not None:
|
||||
try:
|
||||
_keyring.set_password(KEYRING_SERVICE, KEYRING_USER, password)
|
||||
stored_in_keyring = True
|
||||
except Exception:
|
||||
stored_in_keyring = False
|
||||
|
||||
if stored_in_keyring:
|
||||
# Don't persist plaintext in the JSON when the keyring holds it.
|
||||
self.set('relay.password', "")
|
||||
else:
|
||||
self.set('relay.password', password)
|
||||
return self.save()
|
||||
|
||||
def set_relay_session_id(self, session_id: str) -> bool:
|
||||
"""Store the relay session ID. Returns True on successful save."""
|
||||
self.set('relay.session_id', session_id)
|
||||
self.save()
|
||||
return self.save()
|
||||
|
||||
def get_web_auth_token(self) -> str:
|
||||
"""Get the web API auth token."""
|
||||
|
||||
Reference in New Issue
Block a user