"""Local management via fixed SSH target or container-private Unix RPC.""" from auth import initial_record import base64 import hashlib import json import os from pathlib import Path import secrets import subprocess import threading import time ROOT = Path(__file__).resolve().parent.parent SSH = ['ssh', '-i', str(Path.home()/'.ssh/athena_key'), '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=5', '-o', 'StrictHostKeyChecking=yes', 'root@192.168.1.212'] class NetworkClient: def __init__(self): self.local = os.environ.get('DECK_NETWORK_SOCKET') == '1' self.job = None self.lock = threading.Lock() self.cached = None self.cached_at = 0 def call(self, action, values=None, ingress='management'): data = dict(values or {}, action=action) if self.local: from network.rpc import request data.update(ingress=ingress, proxy_token=os.environ.get('DECK_PROXY_TOKEN', '')) return request(data) result = subprocess.run(SSH + ['docker exec -i athena-deck-network python3 -m network.rpc'], input=json.dumps(data), text=True, capture_output=True, timeout=35) try: value = json.loads(result.stdout) except ValueError: raise ValueError('Deck-Netzwerkmodul auf Athena nicht erreichbar oder noch nicht installiert.') from None if result.returncode or ('error' in value and 'installed' not in value): raise ValueError(value.get('error', 'Netzwerkaktion fehlgeschlagen.')) self.cached = None return value def status(self, ingress='management'): with self.lock: if self.job and self.job['state'] == 'running': return dict(installed=False, state='installing', job=self.job.copy(), ingress=ingress) if self.cached is not None and time.monotonic()-self.cached_at < 3: return dict(self.cached, ingress=ingress, job=self.job) try: result = self.call('status', ingress=ingress) result['reachable'] = True except (ValueError, OSError, subprocess.SubprocessError): result = dict(installed=False, reachable=False, state='unavailable', error='Netzwerkmodul noch nicht installiert oder Athena nicht erreichbar.') self.cached = result self.cached_at = time.monotonic() return dict(result, ingress=ingress, job=self.job) def install(self, password, api_token=None): if self.local: raise ValueError('Die Server-Instanz ist bereits installiert.') if not isinstance(password, str) or not 16 <= len(password) <= 256: raise ValueError('Bitte ein eigenes Deck-Passwort mit mindestens 16 Zeichen setzen.') auth = initial_record(password,api_token) with self.lock: if self.job and self.job['state'] == 'running': raise ValueError('Installation läuft bereits.') self.job = dict(state='running', message='Eigener Deck-Container wird gebaut und gestartet. Das kann mehrere Minuten dauern.') threading.Thread(target=self._install, args=(auth,), daemon=True).start() return dict(job=self.job.copy()) def _install(self, auth): try: # Import the source allowlist without executing the installer entrypoint. from network.install_remote import FILES payload = dict(auth=auth, files={name:base64.b64encode((ROOT/name).read_bytes()).decode() for name in FILES}) script = (ROOT/'network/install_remote.py').read_text() # Remote command contains only fixed trusted program text, never user input. import shlex command = 'python3 -c ' + shlex.quote(script) result = subprocess.run(SSH+[command], input=json.dumps(payload), text=True, capture_output=True, timeout=900) value = json.loads(result.stdout) if result.returncode or ('error' in value and 'installed' not in value): raise ValueError(value.get('error', 'Installation fehlgeschlagen.')) # Confirm helper readiness; don't report a successful installation from docker run alone. for _ in range(20): try: self.call('status') break except (ValueError, OSError, subprocess.SubprocessError): time.sleep(1) else: raise ValueError('Container angelegt, aber Netzwerk-Helper nicht bereit. Installation prüfen.') self.job = dict(state='complete', message='Netzwerkmodul installiert. Jetzt eine eigene WireGuard-Konfiguration importieren.') except Exception as exc: self.job = dict(state='failed', message=str(exc) if isinstance(exc, ValueError) and not isinstance(exc, json.JSONDecodeError) else 'Installation fehlgeschlagen; SSH und Docker prüfen.') finally: self.cached = None