fix: Audit-Findings — Realize-Handler, Thread-Safety, Input-Validierung
- _on_realize implementiert (war nur connected, nicht definiert) - do_unrealize für as_file() Context-Manager-Cleanup - threading.Lock für _greetd_sock Race Condition - TOML-Parsing-Fehler abfangen statt Crash - last-user Datei: Längen- und Zeichenvalidierung - detect_locale: non-alpha LANG-Werte abweisen - exec_cmd Plausibility-Check mit shutil.which - Exception-Details ins Log statt in die UI - subprocess.run Timeout für Power-Actions - Sequence[Path] statt tuple[Path, ...] in get_sessions - Mock-Server _recvall für fragmentierte Reads - [behavior]-Config-Sektion entfernt (unimplementiert) - Design Decisions in CLAUDE.md dokumentiert
This commit is contained in:
@@ -33,8 +33,11 @@ def load_config(config_path: Path | None = None) -> Config:
|
||||
if not config_path.exists():
|
||||
return Config()
|
||||
|
||||
with open(config_path, "rb") as f:
|
||||
data = tomllib.load(f)
|
||||
try:
|
||||
with open(config_path, "rb") as f:
|
||||
data = tomllib.load(f)
|
||||
except (tomllib.TOMLDecodeError, OSError):
|
||||
return Config()
|
||||
|
||||
config = Config()
|
||||
appearance = data.get("appearance", {})
|
||||
|
||||
+53
-18
@@ -1,8 +1,11 @@
|
||||
# ABOUTME: Main greeter window — builds the GTK4 UI layout for the Moongreet greeter.
|
||||
# ABOUTME: Handles user selection, session choice, password entry, and power actions.
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import socket
|
||||
import stat
|
||||
import subprocess
|
||||
@@ -22,8 +25,12 @@ from moongreet.users import User, get_users, get_avatar_path, get_user_gtk_theme
|
||||
from moongreet.sessions import Session, get_sessions
|
||||
from moongreet.power import reboot, shutdown
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
LAST_USER_PATH = Path("/var/cache/moongreet/last-user")
|
||||
FAILLOCK_MAX_ATTEMPTS = 3
|
||||
VALID_USERNAME = re.compile(r"^[a-zA-Z0-9_.-]+$")
|
||||
MAX_USERNAME_LENGTH = 256
|
||||
PACKAGE_DATA = files("moongreet") / "data"
|
||||
DEFAULT_AVATAR_PATH = PACKAGE_DATA / "default-avatar.svg"
|
||||
DEFAULT_WALLPAPER_PATH = PACKAGE_DATA / "wallpaper.jpg"
|
||||
@@ -56,13 +63,33 @@ class GreeterWindow(Gtk.ApplicationWindow):
|
||||
self._sessions = get_sessions()
|
||||
self._selected_user: User | None = None
|
||||
self._greetd_sock: socket.socket | None = None
|
||||
self._greetd_sock_lock = threading.Lock()
|
||||
self._default_avatar_pixbuf: GdkPixbuf.Pixbuf | None = None
|
||||
self._avatar_cache: dict[str, GdkPixbuf.Pixbuf] = {}
|
||||
self._failed_attempts: dict[str, int] = {}
|
||||
self._wallpaper_ctx = None
|
||||
|
||||
self._build_ui()
|
||||
self._select_initial_user()
|
||||
self._setup_keyboard_navigation()
|
||||
# Defer initial user selection until the window is realized,
|
||||
# so get_color() returns the actual theme foreground for SVG tinting
|
||||
self.connect("realize", self._on_realize)
|
||||
|
||||
def _on_realize(self, widget: Gtk.Widget) -> None:
|
||||
"""Called when the window is realized — select initial user.
|
||||
|
||||
Deferred from __init__ so get_color() returns actual theme values
|
||||
for SVG tinting. Uses idle_add so the first frame renders before
|
||||
avatar loading blocks the main loop.
|
||||
"""
|
||||
GLib.idle_add(self._select_initial_user)
|
||||
|
||||
def do_unrealize(self) -> None:
|
||||
"""Clean up resources when the window is unrealized."""
|
||||
if self._wallpaper_ctx is not None:
|
||||
self._wallpaper_ctx.__exit__(None, None, None)
|
||||
self._wallpaper_ctx = None
|
||||
super().do_unrealize()
|
||||
|
||||
def _build_ui(self) -> None:
|
||||
"""Build the complete greeter UI layout."""
|
||||
@@ -375,12 +402,13 @@ class GreeterWindow(Gtk.ApplicationWindow):
|
||||
|
||||
def _close_greetd_sock(self) -> None:
|
||||
"""Close the greetd socket and reset the reference."""
|
||||
if self._greetd_sock:
|
||||
try:
|
||||
self._greetd_sock.close()
|
||||
except OSError:
|
||||
pass
|
||||
self._greetd_sock = None
|
||||
with self._greetd_sock_lock:
|
||||
if self._greetd_sock:
|
||||
try:
|
||||
self._greetd_sock.close()
|
||||
except OSError:
|
||||
pass
|
||||
self._greetd_sock = None
|
||||
|
||||
def _set_login_sensitive(self, sensitive: bool) -> None:
|
||||
"""Enable or disable login controls during authentication."""
|
||||
@@ -412,7 +440,8 @@ class GreeterWindow(Gtk.ApplicationWindow):
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
sock.settimeout(10.0)
|
||||
sock.connect(sock_path)
|
||||
self._greetd_sock = sock
|
||||
with self._greetd_sock_lock:
|
||||
self._greetd_sock = sock
|
||||
|
||||
# Step 1: Create session
|
||||
response = create_session(sock, user.username)
|
||||
@@ -440,7 +469,7 @@ class GreeterWindow(Gtk.ApplicationWindow):
|
||||
# Step 3: Start session
|
||||
if response.get("type") == "success":
|
||||
cmd = shlex.split(session.exec_cmd)
|
||||
if not cmd:
|
||||
if not cmd or not shutil.which(cmd[0]):
|
||||
cancel_session(sock)
|
||||
GLib.idle_add(self._on_login_error, None, self._strings.invalid_session_command)
|
||||
return
|
||||
@@ -457,11 +486,13 @@ class GreeterWindow(Gtk.ApplicationWindow):
|
||||
self._close_greetd_sock()
|
||||
|
||||
except ConnectionError as e:
|
||||
logger.error("greetd connection error: %s", e)
|
||||
self._close_greetd_sock()
|
||||
GLib.idle_add(self._on_login_error, None, self._strings.connection_error.format(error=e))
|
||||
GLib.idle_add(self._on_login_error, None, self._strings.connection_error)
|
||||
except (OSError, ValueError) as e:
|
||||
logger.error("greetd socket error: %s", e)
|
||||
self._close_greetd_sock()
|
||||
GLib.idle_add(self._on_login_error, None, self._strings.socket_error.format(error=e))
|
||||
GLib.idle_add(self._on_login_error, None, self._strings.socket_error)
|
||||
|
||||
def _on_login_error(self, response: dict | None, message: str) -> None:
|
||||
"""Handle login error on the GTK main thread."""
|
||||
@@ -483,12 +514,13 @@ class GreeterWindow(Gtk.ApplicationWindow):
|
||||
|
||||
def _cancel_pending_session(self) -> None:
|
||||
"""Cancel any in-progress greetd session."""
|
||||
if self._greetd_sock:
|
||||
try:
|
||||
cancel_session(self._greetd_sock)
|
||||
except (ConnectionError, OSError):
|
||||
pass
|
||||
self._close_greetd_sock()
|
||||
with self._greetd_sock_lock:
|
||||
if self._greetd_sock:
|
||||
try:
|
||||
cancel_session(self._greetd_sock)
|
||||
except (ConnectionError, OSError):
|
||||
pass
|
||||
self._close_greetd_sock()
|
||||
|
||||
def _get_selected_session(self) -> Session | None:
|
||||
"""Get the currently selected session from the dropdown."""
|
||||
@@ -535,9 +567,12 @@ class GreeterWindow(Gtk.ApplicationWindow):
|
||||
"""Load the last logged-in username from cache."""
|
||||
if LAST_USER_PATH.exists():
|
||||
try:
|
||||
return LAST_USER_PATH.read_text().strip()
|
||||
username = LAST_USER_PATH.read_text().strip()
|
||||
except OSError:
|
||||
return None
|
||||
if len(username) > MAX_USERNAME_LENGTH or not VALID_USERNAME.match(username):
|
||||
return None
|
||||
return username
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
|
||||
+10
-6
@@ -31,9 +31,11 @@ class Strings:
|
||||
reboot_failed: str
|
||||
shutdown_failed: str
|
||||
|
||||
# Templates (use .format())
|
||||
# Error messages (continued)
|
||||
connection_error: str
|
||||
socket_error: str
|
||||
|
||||
# Templates (use .format())
|
||||
faillock_attempts_remaining: str
|
||||
faillock_locked: str
|
||||
|
||||
@@ -54,8 +56,8 @@ _STRINGS_DE = Strings(
|
||||
session_start_failed="Session konnte nicht gestartet werden",
|
||||
reboot_failed="Neustart fehlgeschlagen",
|
||||
shutdown_failed="Herunterfahren fehlgeschlagen",
|
||||
connection_error="Verbindungsfehler: {error}",
|
||||
socket_error="Socket-Fehler: {error}",
|
||||
connection_error="Verbindungsfehler",
|
||||
socket_error="Socket-Fehler",
|
||||
faillock_attempts_remaining="Noch {n} Versuch(e) vor Kontosperrung!",
|
||||
faillock_locked="Konto ist möglicherweise gesperrt",
|
||||
)
|
||||
@@ -76,8 +78,8 @@ _STRINGS_EN = Strings(
|
||||
session_start_failed="Failed to start session",
|
||||
reboot_failed="Reboot failed",
|
||||
shutdown_failed="Shutdown failed",
|
||||
connection_error="Connection error: {error}",
|
||||
socket_error="Socket error: {error}",
|
||||
connection_error="Connection error",
|
||||
socket_error="Socket error",
|
||||
faillock_attempts_remaining="{n} attempt(s) remaining before lockout!",
|
||||
faillock_locked="Account may be locked",
|
||||
)
|
||||
@@ -102,7 +104,9 @@ def detect_locale(locale_conf_path: Path = DEFAULT_LOCALE_CONF) -> str:
|
||||
return "en"
|
||||
|
||||
# Extract language prefix: "de_DE.UTF-8" → "de"
|
||||
lang = lang.split("_")[0].split(".")[0]
|
||||
lang = lang.split("_")[0].split(".")[0].lower()
|
||||
if not lang.isalpha():
|
||||
return "en"
|
||||
return lang
|
||||
|
||||
|
||||
|
||||
@@ -4,11 +4,14 @@
|
||||
import subprocess
|
||||
|
||||
|
||||
POWER_TIMEOUT = 30
|
||||
|
||||
|
||||
def reboot() -> None:
|
||||
"""Reboot the system via loginctl."""
|
||||
subprocess.run(["loginctl", "reboot"], check=True)
|
||||
subprocess.run(["loginctl", "reboot"], check=True, timeout=POWER_TIMEOUT)
|
||||
|
||||
|
||||
def shutdown() -> None:
|
||||
"""Shut down the system via loginctl."""
|
||||
subprocess.run(["loginctl", "poweroff"], check=True)
|
||||
subprocess.run(["loginctl", "poweroff"], check=True, timeout=POWER_TIMEOUT)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# ABOUTME: Parses .desktop files from standard session directories.
|
||||
|
||||
import configparser
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
@@ -37,8 +38,8 @@ def _parse_desktop_file(path: Path, session_type: str) -> Session | None:
|
||||
|
||||
|
||||
def get_sessions(
|
||||
wayland_dirs: tuple[Path, ...] = DEFAULT_WAYLAND_DIRS,
|
||||
xsession_dirs: tuple[Path, ...] = DEFAULT_XSESSION_DIRS,
|
||||
wayland_dirs: Sequence[Path] = DEFAULT_WAYLAND_DIRS,
|
||||
xsession_dirs: Sequence[Path] = DEFAULT_XSESSION_DIRS,
|
||||
) -> list[Session]:
|
||||
"""Discover available sessions from .desktop files."""
|
||||
sessions: list[Session] = []
|
||||
|
||||
Reference in New Issue
Block a user