199 lines
6.8 KiB
Python
199 lines
6.8 KiB
Python
#!/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
|