176 lines
5.9 KiB
Python
176 lines
5.9 KiB
Python
from flask import Flask, render_template, request, redirect, url_for, flash
|
|
import json
|
|
import os
|
|
import re
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
APP_TITLE = "RelayDock"
|
|
CONFIG_PATH = Path(os.environ.get("RELAYDOCK_CONFIG", "./config.json"))
|
|
RECONCILE_CMD = os.environ.get("RELAYDOCK_RECONCILE", "./relaydock-reconcile.sh")
|
|
SERVICE_PREFIX = os.environ.get("RELAYDOCK_PREFIX", "relaydock")
|
|
app = Flask(__name__)
|
|
app.secret_key = os.environ.get("FLASK_SECRET_KEY", "change-me")
|
|
|
|
NAME_RE = re.compile(r"^[a-zA-Z0-9._-]+$")
|
|
PROTO_SET = {"udp", "tcp"}
|
|
ROLE_SET = {"oracle", "pi"}
|
|
|
|
|
|
def default_config():
|
|
return {"role": "oracle", "rules": []}
|
|
|
|
|
|
def load_config():
|
|
if not CONFIG_PATH.exists():
|
|
return default_config()
|
|
with CONFIG_PATH.open("r", encoding="utf-8") as f:
|
|
return json.load(f)
|
|
|
|
|
|
def save_config(data):
|
|
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
with CONFIG_PATH.open("w", encoding="utf-8") as f:
|
|
json.dump(data, f, indent=2)
|
|
f.write("\n")
|
|
|
|
|
|
def validate_rule(rule, config):
|
|
errors = []
|
|
name = rule.get("name", "").strip()
|
|
proto = rule.get("proto", "").strip().lower()
|
|
target_host = rule.get("target_host", "").strip()
|
|
try:
|
|
listen_port = int(rule.get("listen_port", 0))
|
|
target_port = int(rule.get("target_port", 0))
|
|
except ValueError:
|
|
errors.append("Ports müssen Zahlen sein.")
|
|
return errors
|
|
|
|
if not NAME_RE.match(name):
|
|
errors.append("Name darf nur Buchstaben, Zahlen, Punkt, Unterstrich und Bindestrich enthalten.")
|
|
if proto not in PROTO_SET:
|
|
errors.append("Proto muss udp oder tcp sein.")
|
|
if not (1 <= listen_port <= 65535 and 1 <= target_port <= 65535):
|
|
errors.append("Ports müssen zwischen 1 und 65535 liegen.")
|
|
if not target_host:
|
|
errors.append("Target Host darf nicht leer sein.")
|
|
|
|
existing = [r["name"] for r in config.get("rules", [])]
|
|
if name in existing:
|
|
errors.append("Name existiert bereits.")
|
|
return errors
|
|
|
|
|
|
def run_cmd(cmd):
|
|
return subprocess.run(cmd, text=True, capture_output=True)
|
|
|
|
|
|
def unit_name(name):
|
|
return f"{SERVICE_PREFIX}@{name}.service"
|
|
|
|
|
|
@app.route("/")
|
|
def index():
|
|
config = load_config()
|
|
rules = []
|
|
for rule in config.get("rules", []):
|
|
svc = unit_name(rule["name"])
|
|
state = run_cmd(["systemctl", "is-active", svc])
|
|
enabled = run_cmd(["systemctl", "is-enabled", svc])
|
|
rules.append({
|
|
**rule,
|
|
"service": svc,
|
|
"active": state.stdout.strip() if state.returncode == 0 else state.stdout.strip() or "inactive",
|
|
"enabled": enabled.stdout.strip() if enabled.returncode == 0 else enabled.stdout.strip() or "disabled",
|
|
})
|
|
return render_template("index.html", title=APP_TITLE, config=config, rules=rules)
|
|
|
|
|
|
@app.post("/role")
|
|
def set_role():
|
|
config = load_config()
|
|
role = request.form.get("role", "").strip().lower()
|
|
if role not in ROLE_SET:
|
|
flash("Ungültige Rolle.", "error")
|
|
return redirect(url_for("index"))
|
|
config["role"] = role
|
|
save_config(config)
|
|
flash("Rolle gespeichert.", "success")
|
|
return redirect(url_for("index"))
|
|
|
|
|
|
@app.post("/rules/add")
|
|
def add_rule():
|
|
config = load_config()
|
|
rule = {
|
|
"name": request.form.get("name", ""),
|
|
"proto": request.form.get("proto", ""),
|
|
"listen_port": request.form.get("listen_port", "0"),
|
|
"target_host": request.form.get("target_host", ""),
|
|
"target_port": request.form.get("target_port", "0"),
|
|
}
|
|
errors = validate_rule(rule, config)
|
|
if errors:
|
|
for err in errors:
|
|
flash(err, "error")
|
|
return redirect(url_for("index"))
|
|
rule["listen_port"] = int(rule["listen_port"])
|
|
rule["target_port"] = int(rule["target_port"])
|
|
config.setdefault("rules", []).append(rule)
|
|
save_config(config)
|
|
flash("Regel hinzugefügt.", "success")
|
|
return redirect(url_for("index"))
|
|
|
|
|
|
@app.post("/rules/<name>/delete")
|
|
def delete_rule(name):
|
|
config = load_config()
|
|
before = len(config.get("rules", []))
|
|
config["rules"] = [r for r in config.get("rules", []) if r["name"] != name]
|
|
save_config(config)
|
|
if len(config["rules"]) < before:
|
|
flash(f"Regel {name} gelöscht.", "success")
|
|
else:
|
|
flash("Regel nicht gefunden.", "error")
|
|
return redirect(url_for("index"))
|
|
|
|
|
|
@app.post("/rules/<name>/toggle")
|
|
def toggle_rule(name):
|
|
svc = unit_name(name)
|
|
state = run_cmd(["systemctl", "is-active", svc])
|
|
if state.returncode == 0 and state.stdout.strip() == "active":
|
|
result = run_cmd(["sudo", "systemctl", "stop", svc])
|
|
flash(result.stderr.strip() or result.stdout.strip() or f"{svc} gestoppt.", "success" if result.returncode == 0 else "error")
|
|
else:
|
|
result = run_cmd(["sudo", "systemctl", "start", svc])
|
|
flash(result.stderr.strip() or result.stdout.strip() or f"{svc} gestartet.", "success" if result.returncode == 0 else "error")
|
|
return redirect(url_for("index"))
|
|
|
|
|
|
@app.post("/rules/<name>/restart")
|
|
def restart_rule(name):
|
|
svc = unit_name(name)
|
|
result = run_cmd(["sudo", "systemctl", "restart", svc])
|
|
flash(result.stderr.strip() or result.stdout.strip() or f"{svc} neugestartet.", "success" if result.returncode == 0 else "error")
|
|
return redirect(url_for("index"))
|
|
|
|
|
|
@app.post("/reconcile")
|
|
def reconcile():
|
|
result = run_cmd(["sudo", RECONCILE_CMD, str(CONFIG_PATH)])
|
|
flash(result.stderr.strip() or result.stdout.strip() or "Synchronisierung ausgeführt.", "success" if result.returncode == 0 else "error")
|
|
return redirect(url_for("index"))
|
|
|
|
|
|
@app.post("/daemon-reload")
|
|
def daemon_reload():
|
|
result = run_cmd(["sudo", "systemctl", "daemon-reload"])
|
|
flash(result.stderr.strip() or result.stdout.strip() or "systemd daemon-reload ausgeführt.", "success" if result.returncode == 0 else "error")
|
|
return redirect(url_for("index"))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app.run(host="0.0.0.0", port=8080, debug=True)
|