Files
inwx-scripts/inwx_common.py
T
2026-08-18 13:49:31 +02:00

199 lines
6.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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.
22002299 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