- ipc.py: _recvall() Loop für fragmentierte Socket-Reads, 64 KiB Payload-Limit gegen OOM - greeter.py: GREETD_SOCK Validierung (absoluter Pfad + S_ISSOCK), Socket-Leak behoben (_close_greetd_sock), shlex.split() statt str.split() für Exec-Befehle, greetd-Fehlermeldungen auf 200 Zeichen begrenzt, get_style_context() durch get_color() ersetzt (GTK 4.10+ Deprecation), Socket-Timeout (10s) - users.py: ValueError-Absicherung bei int(uid_str), Username- Sanitierung gegen Pfad-Traversal, Symlink-Check bei Avatar-Pfaden
105 lines
2.7 KiB
Python
105 lines
2.7 KiB
Python
# ABOUTME: User detection — parses /etc/passwd for login users, finds avatars and GTK themes.
|
|
# ABOUTME: Provides User dataclass and helper functions for the greeter UI.
|
|
|
|
import configparser
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
NOLOGIN_SHELLS = {"/usr/sbin/nologin", "/sbin/nologin", "/bin/false", "/usr/bin/nologin"}
|
|
MIN_UID = 1000
|
|
MAX_UID = 65533
|
|
|
|
DEFAULT_PASSWD = Path("/etc/passwd")
|
|
DEFAULT_ACCOUNTSSERVICE_DIR = Path("/var/lib/AccountsService/icons")
|
|
|
|
|
|
@dataclass
|
|
class User:
|
|
"""Represents a system user suitable for login."""
|
|
|
|
username: str
|
|
uid: int
|
|
gecos: str
|
|
home: Path
|
|
shell: str
|
|
|
|
@property
|
|
def display_name(self) -> str:
|
|
"""Return gecos if available, otherwise username."""
|
|
return self.gecos if self.gecos else self.username
|
|
|
|
|
|
def get_users(passwd_path: Path = DEFAULT_PASSWD) -> list[User]:
|
|
"""Parse /etc/passwd and return users with UID in the login range."""
|
|
users: list[User] = []
|
|
|
|
if not passwd_path.exists():
|
|
return users
|
|
|
|
for line in passwd_path.read_text().splitlines():
|
|
parts = line.split(":")
|
|
if len(parts) < 7:
|
|
continue
|
|
|
|
username, _, uid_str, _, gecos, home, shell = parts[0], parts[1], parts[2], parts[3], parts[4], parts[5], parts[6]
|
|
|
|
try:
|
|
uid = int(uid_str)
|
|
except ValueError:
|
|
continue
|
|
|
|
if uid < MIN_UID or uid > MAX_UID:
|
|
continue
|
|
if shell in NOLOGIN_SHELLS:
|
|
continue
|
|
if "/" in username or username.startswith("."):
|
|
continue
|
|
|
|
users.append(User(
|
|
username=username,
|
|
uid=uid,
|
|
gecos=gecos,
|
|
home=Path(home),
|
|
shell=shell,
|
|
))
|
|
|
|
return users
|
|
|
|
|
|
def get_avatar_path(
|
|
username: str,
|
|
accountsservice_dir: Path = DEFAULT_ACCOUNTSSERVICE_DIR,
|
|
home_dir: Path | None = None,
|
|
) -> Path | None:
|
|
"""Find avatar for a user: AccountsService icon → ~/.face → None."""
|
|
# AccountsService icon
|
|
icon = accountsservice_dir / username
|
|
if icon.exists() and not icon.is_symlink():
|
|
return icon
|
|
|
|
# ~/.face fallback
|
|
if home_dir is not None:
|
|
face = home_dir / ".face"
|
|
if face.exists() and not face.is_symlink():
|
|
return face
|
|
|
|
return None
|
|
|
|
|
|
def get_user_gtk_theme(config_dir: Path | None = None) -> str | None:
|
|
"""Read the GTK theme name from a user's gtk-4.0/settings.ini."""
|
|
if config_dir is None:
|
|
return None
|
|
|
|
settings_file = config_dir / "settings.ini"
|
|
if not settings_file.exists():
|
|
return None
|
|
|
|
config = configparser.ConfigParser()
|
|
config.read(settings_file)
|
|
|
|
if config.has_option("Settings", "gtk-theme-name"):
|
|
return config.get("Settings", "gtk-theme-name")
|
|
|
|
return None
|