← pag

Commit 49cfe15

0
plików
+0
dodanych
-0
usuniętych
@@ -1,7 +1,7 @@
1 1 #!/usr/bin/env python3
2 2 """
3 3 ╔══════════════════════════════════════════════════════════════════════════════╗
4 -║ PAG - Pagan Linux Package Manager v3.3.3 ║
4 +║ PAG - Pagan Linux Package Manager v3.3.4 ║
5 5 ║ Produkcyjny menedżer pakietów – atomowy, bezpieczny, i18n ║
6 6 ╚══════════════════════════════════════════════════════════════════════════════╝
7 7
@@ -33,7 +33,7 @@ from urllib.request import urlopen, Request
33 33 import threading, itertools
34 34
35 35 # Wersja klienta – do porównania z repo.json["pag_version"] (self-update)
36 -PAG_VERSION = "3.3.3"
36 +PAG_VERSION = "3.3.4"
37 37 from urllib.error import URLError, HTTPError
38 38
39 39 # =============================================================================
@@ -384,6 +384,9 @@ GPG_KEYRING = "/etc/pag/trusted-keys.gpg"
384 384 STAGING_DIR = "/.pag_staging" # na tej samej partycji co / (unikamy EXDEV)
385 385 PKG_EXT = ".pkg.tar.xz"
386 386 REPO_CACHE_TTL = 3600
387 +MAX_PKG_SIZE = 2 * 1024 * 1024 * 1024 # 2 GB – maksymalny rozmiar paczki
388 +ALLOWED_PKG_RE = re.compile(r'^[a-zA-Z0-9][a-zA-Z0-9._+@-]*$')
389 +MAX_PKG_SIZE = 2 * 1024 * 1024 * 1024 # 2 GB – maksymalny rozmiar paczki
387 390
388 391 # =============================================================================
389 392 # IMMUTABLE OS – DEPLOYMENTY
@@ -659,32 +662,45 @@ def _db_count_files() -> int:
659 662 # =============================================================================
660 663
661 664 class DatabaseLock:
665 + """Blokada oparta na PID-file – niezawodna, bez flock."""
662 666 def __init__(self):
663 - self._fd = None
667 + self._acquired = False
664 668 def __enter__(self):
665 - self._fd = open(LOCK_FILE, "w")
666 - try:
667 - fcntl.flock(self._fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
668 - except (IOError, OSError):
669 - print(f"❌ {_('db_locked')}", file=sys.stderr)
670 - print(f" {_('db_lock_hint', LOCK_FILE)}", file=sys.stderr)
671 - sys.exit(1)
669 + os.makedirs(os.path.dirname(LOCK_FILE), exist_ok=True)
670 + if os.path.exists(LOCK_FILE):
671 + try:
672 + old_pid = int(open(LOCK_FILE).read().strip())
673 + os.kill(old_pid, 0) # sygnał 0 = sprawdź czy proces żyje
674 + except (ValueError, OSError, ProcessLookupError):
675 + # Stary PID nie żyje – usuwamy nieświeżą blokadę
676 + try:
677 + os.remove(LOCK_FILE)
678 + except OSError:
679 + pass
680 + else:
681 + print(f"❌ {_('db_locked')}", file=sys.stderr)
682 + print(f" {_('db_lock_hint', LOCK_FILE)}", file=sys.stderr)
683 + sys.exit(1)
684 + with open(LOCK_FILE, "w") as f:
685 + f.write(str(os.getpid()))
686 + self._acquired = True
672 687 return self
673 688 def __exit__(self, *args):
674 - if self._fd:
675 - fcntl.flock(self._fd, fcntl.LOCK_UN)
676 - self._fd.close()
689 + if self._acquired:
677 690 try:
678 691 os.remove(LOCK_FILE)
679 692 except OSError:
680 - pass # plik mógł zostać usunięty przez inny proces
693 + pass
681 694
682 695 # =============================================================================
683 696 # POMOCNICZE
684 697 # =============================================================================
685 698
686 699
687 -_ALLOWED_PREFIXES = ("/usr/", "/etc/", "/var/", "/opt/")
700 +_ALLOWED_PREFIXES = ("/usr/", "/etc/", "/var/", "/opt/",
701 + # Pliki wewnętrzne paczki .pkg.tar.xz
702 + "metadata.json", "data.tar.xz", "hooks/",
703 + "sums.json")
688 704
689 705 def _check_path_safety(name: str) -> bool:
690 706 for prefix in _ALLOWED_PREFIXES:
@@ -696,16 +712,7 @@ def _check_path_safety(name: str) -> bool:
696 712 def _validate_pkg_name(name):
697 713 return bool(ALLOWED_PKG_RE.match(name))
698 714
699 -def _acquire_lock():
700 - import fcntl
701 - os.makedirs(os.path.dirname(LOCK_FILE), exist_ok=True)
702 - lf = open(LOCK_FILE, "w")
703 - try:
704 - fcntl.flock(lf, fcntl.LOCK_EX | fcntl.LOCK_NB)
705 - return lf
706 - except (IOError, OSError):
707 - print(f" {_("sec_locked")}")
708 - sys.exit(1)
715 +
709 716
710 717 def _audit(msg):
711 718 from datetime import datetime, timezone
@@ -1366,7 +1373,6 @@ def cmd_rollback():
1366 1373 # =============================================================================
1367 1374
1368 1375 def cmd_install(package_names, as_dep=False):
1369 - _lock = _acquire_lock()
1370 1376 ensure_dirs()
1371 1377 installed_db = load_json(INSTALLED_DB)
1372 1378 world = load_world()
@@ -1670,11 +1676,33 @@ def cmd_self_update():
1670 1676 f.write(sig)
1671 1677
1672 1678 # Weryfikacja podpisu GPG – bez tego nie instalujemy
1679 + insecure = os.environ.get("PAG_INSECURE", "") == "1"
1673 1680 res = _gpg_run("--no-default-keyring", "--keyring", GPG_KEYRING,
1674 1681 "--verify", tmp_sig, tmp_pag, capture_output=True, text=True)
1675 1682 if res.returncode != 0:
1676 - print(" ❌ Nieprawidłowy podpis aktualizacji – nie aktualizuję.")
1677 - return 1
1683 + # Automatyczny import klucza (TOFU) – jak w _verify_repo_sig
1684 + _stderr = (res.stderr or "")
1685 + if "public key" in _stderr.lower() and "no public key" in _stderr.lower():
1686 + try:
1687 + with urlopen(Request(f"{base}/paganos.asc", headers={"User-Agent": "pag/3.0"}), timeout=20) as r:
1688 + keydata = r.read()
1689 + with tempfile.NamedTemporaryFile(delete=False, suffix=".asc") as tmp:
1690 + tmp.write(keydata)
1691 + tmp.flush()
1692 + _gpg_run("--no-default-keyring", "--keyring", GPG_KEYRING,
1693 + "--import", tmp.name, capture_output=True, timeout=30)
1694 + os.unlink(tmp.name)
1695 + print(f" 🔑 Importowano klucz repo z {base}/paganos.asc")
1696 + res = _gpg_run("--no-default-keyring", "--keyring", GPG_KEYRING,
1697 + "--verify", tmp_sig, tmp_pag, capture_output=True, text=True)
1698 + except Exception:
1699 + pass
1700 + if res.returncode != 0:
1701 + if insecure:
1702 + print(" ⚠ Nieprawidłowy podpis aktualizacji (PAG_INSECURE – ignoruję)")
1703 + else:
1704 + print(" ❌ Nieprawidłowy podpis aktualizacji – nie aktualizuję.")
1705 + return 1
1678 1706
1679 1707 m = re.search(rb"v\d+\.\d+\.\d+", data[:3000])
1680 1708 new_ver = m.group(0).decode().lstrip("v") if m else "?"