#!/usr/bin/env python3 """Gemeinsame Helfer für die INWX-Skripte. Kapselt das Laden der Zugangsdaten sowie den Login mit Session-Caching: Nach dem ersten Login wird das Session-Cookie von INWX lokal zwischen- gespeichert. Folgeaufrufe nutzen diese Session wieder und benötigen daher keinen neuen Login, solange die Session bei INWX noch gültig ist. Zugangsdaten werden in dieser Reihenfolge gesucht: 1. Umgebungsvariablen INWX_USER / INWX_PASSWORD (z.B. gesetzt über eine systemd EnvironmentFile=) 2. Credentials-Datei CREDS_FILE_PATH im Format: username=dein_inwx_benutzername password=dein_inwx_passwort Die Datei muss chmod 600 und dem ausführenden Benutzer gehören. """ import json import os import sys import requests # Import-Fix für die Library-Struktur (Linux/Mac) try: from INWX.Domrobot import ApiClient except ImportError: from inwx.Domrobot import ApiClient # --- KONFIGURATION --- API_URL = 'https://api.domrobot.com' BASE_DIR = os.path.dirname(os.path.abspath(__file__)) # HTTP-Proxy für die Verbindung zur INWX-API (optional). # Format: http://proxyhost:port oder mit Auth: http://user:passwort@proxyhost:port # Kann per Umgebungsvariable INWX_PROXY gesetzt werden. PROXY_URL = os.environ.get("INWX_PROXY") # Datei mit den INWX-Zugangsdaten (username=... / password=...). # WICHTIG: chmod 600, nur für den ausführenden Benutzer lesbar! CREDS_FILE_PATH = os.path.join(BASE_DIR, ".inwx_credentials") # Datei mit der zwischengespeicherten Session (enthält nur ein kurzlebiges # Session-Cookie, niemals das Passwort). SESSION_CACHE_PATH = os.path.join(BASE_DIR, ".inwx_session") def _check_perms(path): """Warnt, falls eine Secrets-Datei für Gruppe/Andere lesbar ist.""" try: mode = os.stat(path).st_mode if mode & 0o077: print(f"⚠️ {path} ist für andere Benutzer les-/schreibbar! " f"Bitte 'chmod 600 {path}' ausführen.") except OSError: pass def get_creds(): """Holt Benutzername und Passwort für die INWX-API. Siehe Modul-Docstring für die Reihenfolge der Quellen (ENV, dann Datei). """ env_user = os.environ.get("INWX_USER") env_password = os.environ.get("INWX_PASSWORD") if env_user and env_password: return env_user, env_password if not os.path.exists(CREDS_FILE_PATH): print(f"❌ Keine Zugangsdaten gefunden: weder INWX_USER/INWX_PASSWORD " f"gesetzt, noch {CREDS_FILE_PATH} vorhanden.") return None, None _check_perms(CREDS_FILE_PATH) try: creds = {} with open(CREDS_FILE_PATH, "r") as f: for line in f: line = line.strip() if not line or line.startswith("#") or "=" not in line: continue k, v = line.split("=", 1) creds[k.strip().lower()] = v.strip() user = creds.get("username") or creds.get("user") password = creds.get("password") if not user or not password: print(f"❌ {CREDS_FILE_PATH} enthält keine gültigen username=/password=-Zeilen.") return None, None return user, password except Exception as e: print(f"❌ Fehler beim Lesen von {CREDS_FILE_PATH}: {e}") return None, None def _load_cached_cookies(api): """Lädt gespeicherte Session-Cookies in den ApiClient. True bei Erfolg.""" if not os.path.exists(SESSION_CACHE_PATH): return False try: with open(SESSION_CACHE_PATH, "r") as f: cookies = json.load(f) if not cookies: return False api.api_session.cookies = requests.utils.cookiejar_from_dict(cookies) return True except Exception: return False def _save_cached_cookies(api): """Speichert die Session-Cookies (Datei nur für den Benutzer lesbar, 0600).""" try: cookies = requests.utils.dict_from_cookiejar(api.api_session.cookies) fd = os.open(SESSION_CACHE_PATH, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) with os.fdopen(fd, "w") as f: json.dump(cookies, f) except Exception as e: print(f"⚠️ Konnte Session nicht zwischenspeichern: {e}") def _clear_cached_session(): """Entfernt eine (abgelaufene) zwischengespeicherte Session.""" try: os.remove(SESSION_CACHE_PATH) except FileNotFoundError: pass except Exception: pass def _do_login(api): """Frischer Login mit den konfigurierten Zugangsdaten; speichert die Session. True bei Erfolg.""" user, password = get_creds() if not user or not password: return False print(f"Versuche Login für: {user}") login_res = api.login(user, password) if login_res['code'] != 1000: print(f"❌ Login fehlgeschlagen: {login_res['msg']} (Code: {login_res['code']})") return False _save_cached_cookies(api) print("✅ Login erfolgreich (Session zwischengespeichert).") return True def get_client(): """Liefert einen einsatzbereiten ApiClient. Nutzt eine zwischengespeicherte Session, falls vorhanden. Ob diese noch gültig ist, wird erst beim ersten echten Aufruf via call_api() geprüft – so sparen wir uns eine zusätzliche Anfrage. Ist keine Session vorhanden, wird sofort neu eingeloggt. Ist PROXY_URL gesetzt, laufen alle Requests der Session (Login und API-Aufrufe) über diesen HTTP-Proxy. """ api = ApiClient(api_url=API_URL) if PROXY_URL: api.api_session.proxies = {"http": PROXY_URL, "https": PROXY_URL} if _load_cached_cookies(api): print("ℹ️ Verwende zwischengespeicherte Session.") return api if not _do_login(api): sys.exit(1) return api def _is_auth_error(code): """True, wenn der INWX-Code auf eine fehlende/abgelaufene Session hindeutet. 2200–2299 sind die eigentlichen Authentifizierungs-/Autorisierungsfehler. Zusätzlich liefert INWX bei Aufrufen ohne gültige Session teils Codes aus dem 2000er-Bereich (z.B. 2002 „Command use error"); auch diese behandeln wir als abgelaufene Session und melden uns einmal neu an. """ if code is None: return False if 2200 <= code < 2300: return True return code in (2001, 2002, 2003) def call_api(api, method, params=None): """Wie ApiClient.call_api, meldet sich aber bei abgelaufener Session neu an. Deutet der Rückgabecode auf eine fehlende/abgelaufene Session hin (siehe _is_auth_error), wird die zwischengespeicherte Session verworfen, neu eingeloggt und der Aufruf einmal wiederholt. """ params = params or {} res = api.call_api(method, params) if _is_auth_error(res.get('code')): print("ℹ️ Session abgelaufen, melde neu an...") _clear_cached_session() if not _do_login(api): sys.exit(1) res = api.call_api(method, params) return res