From 6fadb732636b5ad110a5c9e2f597b87891a9e1f6 Mon Sep 17 00:00:00 2001 From: Stefan Hacker Date: Thu, 9 Apr 2026 16:06:55 +0200 Subject: [PATCH] Replace Basic Auth with login page, add HTTP redirect and log viewer - Session-based login page instead of HTTP Basic Auth - /ca.crt download works without login for easy device access - HTTP port (default 8080) redirects to HTTPS automatically - Nginx access/error log viewer in WebUI - API still supports Basic Auth for curl/scripts - Logout button and log navigation in header Co-Authored-By: Claude Opus 4.6 (1M context) --- .env.example | 1 + app/app.py | 107 ++++++++++++++++++++------ app/templates/index.html | 2 + app/templates/login.html | 123 ++++++++++++++++++++++++++++++ app/templates/logs.html | 157 +++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 1 + nginx/entrypoint.sh | 5 +- nginx/nginx.conf | 12 ++- 8 files changed, 378 insertions(+), 30 deletions(-) create mode 100644 app/templates/login.html create mode 100644 app/templates/logs.html diff --git a/.env.example b/.env.example index 1b7f4bd..b38a03f 100644 --- a/.env.example +++ b/.env.example @@ -13,5 +13,6 @@ CERT_DAYS=36500 # --- WebUI-Einstellungen --- WEBUI_PORT=8443 +WEBUI_PORT_HTTP=8080 WEBUI_USERNAME=admin WEBUI_PASSWORD=admin123 diff --git a/app/app.py b/app/app.py index 8f7c077..489a92f 100644 --- a/app/app.py +++ b/app/app.py @@ -1,16 +1,21 @@ import json import os +import secrets import subprocess from functools import wraps from pathlib import Path -from flask import Flask, jsonify, redirect, render_template, request, send_file, url_for +from flask import (Flask, jsonify, redirect, render_template, request, + send_file, session, url_for) app = Flask(__name__) +app.secret_key = os.environ.get("SECRET_KEY", secrets.token_hex(32)) CONFIG_FILE = "/data/proxy_config.json" NGINX_CONF_DIR = "/etc/nginx/conf.d" NGINX_UPSTREAM_CONF = f"{NGINX_CONF_DIR}/proxy-targets.conf" +NGINX_ACCESS_LOG = "/var/log/nginx/access.log" +NGINX_ERROR_LOG = "/var/log/nginx/error.log" USERNAME = os.environ.get("WEBUI_USERNAME", "admin") PASSWORD = os.environ.get("WEBUI_PASSWORD", "admin123") @@ -131,29 +136,55 @@ def reload_nginx(): return False, str(e) -def check_auth(f): +def read_log_tail(filepath, lines=100): + """Read the last N lines of a log file.""" + try: + result = subprocess.run( + ["tail", "-n", str(lines), filepath], + capture_output=True, text=True, timeout=5 + ) + return result.stdout + except Exception: + return "" + + +def login_required(f): @wraps(f) def decorated(*args, **kwargs): - auth = request.authorization - if not auth or auth.username != USERNAME or auth.password != PASSWORD: - return ( - "Authentication required", - 401, - {"WWW-Authenticate": 'Basic realm="Proxy Admin"'}, - ) + if not session.get("logged_in"): + # API requests get 401, browser requests get redirected + if request.path.startswith("/api/"): + auth = request.authorization + if auth and auth.username == USERNAME and auth.password == PASSWORD: + return f(*args, **kwargs) + return jsonify({"error": "Authentication required"}), 401 + return redirect(url_for("login")) return f(*args, **kwargs) return decorated -# ==================== WebUI Routes ==================== +# ==================== Auth Routes ==================== -@app.route("/") -@check_auth -def index(): - config = load_config() - return render_template("index.html", config=config) +@app.route("/login", methods=["GET", "POST"]) +def login(): + error = None + if request.method == "POST": + if (request.form.get("username") == USERNAME and + request.form.get("password") == PASSWORD): + session["logged_in"] = True + return redirect(url_for("index")) + error = "Falscher Benutzername oder Passwort" + return render_template("login.html", error=error) +@app.route("/logout") +def logout(): + session.clear() + return redirect(url_for("login")) + + +# ==================== Public Routes ==================== + @app.route("/ca.crt") def download_ca(): """Download CA certificate - no auth required so devices can easily fetch it.""" @@ -164,8 +195,27 @@ def download_ca(): mimetype="application/x-x509-ca-cert") +# ==================== WebUI Routes ==================== + +@app.route("/") +@login_required +def index(): + config = load_config() + return render_template("index.html", config=config) + + +@app.route("/logs") +@login_required +def logs(): + lines = int(request.args.get("lines", 100)) + log_type = request.args.get("type", "access") + log_file = NGINX_ACCESS_LOG if log_type == "access" else NGINX_ERROR_LOG + log_content = read_log_tail(log_file, lines) + return render_template("logs.html", log_content=log_content, log_type=log_type, lines=lines) + + @app.route("/target/add", methods=["POST"]) -@check_auth +@login_required def add_target(): config = load_config() @@ -198,7 +248,7 @@ def add_target(): @app.route("/target//delete", methods=["POST"]) -@check_auth +@login_required def delete_target(idx): config = load_config() if 0 <= idx < len(config["targets"]): @@ -210,7 +260,7 @@ def delete_target(idx): @app.route("/target//toggle", methods=["POST"]) -@check_auth +@login_required def toggle_target(idx): config = load_config() if 0 <= idx < len(config["targets"]): @@ -222,7 +272,7 @@ def toggle_target(idx): @app.route("/target//edit", methods=["POST"]) -@check_auth +@login_required def edit_target(idx): config = load_config() if 0 <= idx < len(config["targets"]): @@ -251,14 +301,14 @@ def edit_target(idx): # ==================== API Routes ==================== @app.route("/api/targets", methods=["GET"]) -@check_auth +@login_required def api_list_targets(): config = load_config() return jsonify(config) @app.route("/api/targets", methods=["POST"]) -@check_auth +@login_required def api_add_target(): config = load_config() target = request.get_json() @@ -282,7 +332,7 @@ def api_add_target(): @app.route("/api/targets/", methods=["PUT"]) -@check_auth +@login_required def api_update_target(idx): config = load_config() if idx < 0 or idx >= len(config["targets"]): @@ -301,7 +351,7 @@ def api_update_target(idx): @app.route("/api/targets/", methods=["DELETE"]) -@check_auth +@login_required def api_delete_target(idx): config = load_config() if idx < 0 or idx >= len(config["targets"]): @@ -316,7 +366,7 @@ def api_delete_target(idx): @app.route("/api/reload", methods=["POST"]) -@check_auth +@login_required def api_reload(): config = load_config() generate_nginx_config(config) @@ -324,5 +374,14 @@ def api_reload(): return jsonify({"status": "ok" if success else "error", "message": msg}) +@app.route("/api/logs", methods=["GET"]) +@login_required +def api_logs(): + lines = int(request.args.get("lines", 100)) + log_type = request.args.get("type", "access") + log_file = NGINX_ACCESS_LOG if log_type == "access" else NGINX_ERROR_LOG + return jsonify({"type": log_type, "content": read_log_tail(log_file, lines)}) + + if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=True) diff --git a/app/templates/index.html b/app/templates/index.html index 0a2baca..4476f89 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -199,6 +199,8 @@

HTTPS Reverse Proxy

CA-Zertifikat herunterladen + Logs + Abmelden Self-Signed SSL - 100 Jahre
diff --git a/app/templates/login.html b/app/templates/login.html new file mode 100644 index 0000000..b0e79ea --- /dev/null +++ b/app/templates/login.html @@ -0,0 +1,123 @@ + + + + + + HTTPS Proxy - Login + + + + + + diff --git a/app/templates/logs.html b/app/templates/logs.html new file mode 100644 index 0000000..71d6dab --- /dev/null +++ b/app/templates/logs.html @@ -0,0 +1,157 @@ + + + + + + HTTPS Proxy - Logs + + + +
+

HTTPS Reverse Proxy - Logs

+ +
+ +
+
+

Nginx Logs

+ +
+ Access Log + Error Log + +
+ + + +
+ + Aktualisieren +
+ + {% if log_content %} +
{{ log_content }}
+ {% else %} +
+

Keine Log-Eintraege vorhanden.

+
+ {% endif %} +
+
+ + + + diff --git a/docker-compose.yml b/docker-compose.yml index 10c869b..d4f08f2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,6 +14,7 @@ services: - CERT_DAYS=${CERT_DAYS:-36500} # WebUI settings - WEBUI_PORT=${WEBUI_PORT:-8443} + - WEBUI_PORT_HTTP=${WEBUI_PORT_HTTP:-8080} - WEBUI_USERNAME=${WEBUI_USERNAME:-admin} - WEBUI_PASSWORD=${WEBUI_PASSWORD:-admin123} network_mode: host diff --git a/nginx/entrypoint.sh b/nginx/entrypoint.sh index fd29f3b..6b38d2e 100755 --- a/nginx/entrypoint.sh +++ b/nginx/entrypoint.sh @@ -3,14 +3,15 @@ set -e echo "=== Starting HTTPS Proxy ===" -# Set default for WEBUI_PORT +# Set defaults export WEBUI_PORT="${WEBUI_PORT:-8443}" +export WEBUI_PORT_HTTP="${WEBUI_PORT_HTTP:-8080}" # Generate certificates if needed /certs/generate-certs.sh # Replace env vars in nginx config template -envsubst '${WEBUI_PORT}' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf +envsubst '${WEBUI_PORT} ${WEBUI_PORT_HTTP}' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf # Ensure conf.d directory exists with empty config mkdir -p /etc/nginx/conf.d diff --git a/nginx/nginx.conf b/nginx/nginx.conf index e184e91..e99596f 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -18,7 +18,14 @@ http { sendfile on; keepalive_timeout 65; - # WebUI / Admin interface + # HTTP -> HTTPS redirect + server { + listen ${WEBUI_PORT_HTTP}; + server_name _; + return 301 https://$host:${WEBUI_PORT}$request_uri; + } + + # WebUI / Admin interface (HTTPS) server { listen ${WEBUI_PORT} ssl default_server; server_name _; @@ -27,9 +34,6 @@ http { ssl_certificate_key /certs/server.key; ssl_protocols TLSv1.2 TLSv1.3; - # Redirect plain HTTP requests to HTTPS - error_page 497 https://$host:$server_port$request_uri; - location / { proxy_pass http://127.0.0.1:5000; proxy_set_header Host $host;