80 lines
2.8 KiB
Python
80 lines
2.8 KiB
Python
from fastapi import FastAPI, Request, Form, HTTPException
|
|
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
|
|
import os, hmac, hashlib, time, secrets
|
|
|
|
PASSWORD = os.environ.get("HUB_PASSWORD", "changeme")
|
|
SECRET = os.environ.get("HUB_SECRET", secrets.token_hex(32))
|
|
COOKIE_NAME = "darkelite_session"
|
|
COOKIE_DAYS = 30
|
|
COOKIE_TTL = COOKIE_DAYS * 86400
|
|
|
|
app = FastAPI(redirect_slashes=False)
|
|
|
|
def sign(payload: str) -> str:
|
|
return hmac.new(SECRET.encode(), payload.encode(), hashlib.sha256).hexdigest()
|
|
|
|
def make_token() -> str:
|
|
exp = int(time.time()) + COOKIE_TTL
|
|
payload = f"{exp}"
|
|
sig = sign(payload)
|
|
return f"{payload}.{sig}"
|
|
|
|
def verify_token(token: str) -> bool:
|
|
if not token or "." not in token:
|
|
return False
|
|
try:
|
|
payload, sig = token.rsplit(".", 1)
|
|
if not hmac.compare_digest(sig, sign(payload)):
|
|
return False
|
|
exp = int(payload)
|
|
return exp > int(time.time())
|
|
except Exception:
|
|
return False
|
|
|
|
@app.get("/api/check")
|
|
def check(request: Request):
|
|
"""Caddy forward_auth hits this. 200=allowed, 401/redirect=needs login."""
|
|
# LAN bypass
|
|
# Use the FIRST X-Forwarded-For value (real client). Caddy adds this header.
|
|
# Caddy passes real client IP in X-Real-IP via forward_auth header_up
|
|
ip = request.headers.get("x-real-ip", "").strip()
|
|
if not ip:
|
|
xff = request.headers.get("x-forwarded-for", "")
|
|
ip = xff.split(",")[0].strip()
|
|
# Only treat actual LAN subnets as bypass - NOT docker internal 172.x
|
|
if ip.startswith("10.0.0.") or ip.startswith("192.168.") or ip == "127.0.0.1":
|
|
return JSONResponse({"ok": True, "via": "lan"})
|
|
token = request.cookies.get(COOKIE_NAME)
|
|
if verify_token(token):
|
|
return JSONResponse({"ok": True, "via": "cookie"})
|
|
# If this looks like a browser (accepts HTML), redirect to login
|
|
accept = request.headers.get("accept", "")
|
|
orig_uri = request.headers.get("x-forwarded-uri", "/")
|
|
if "text/html" in accept:
|
|
from fastapi.responses import RedirectResponse
|
|
return RedirectResponse(url=f"/login/?next={orig_uri}", status_code=302)
|
|
raise HTTPException(401, "auth required")
|
|
|
|
@app.post("/api/login")
|
|
def login(password: str = Form(...)):
|
|
if not hmac.compare_digest(password, PASSWORD):
|
|
raise HTTPException(401, "bad password")
|
|
token = make_token()
|
|
resp = JSONResponse({"ok": True})
|
|
resp.set_cookie(
|
|
COOKIE_NAME, token,
|
|
max_age=COOKIE_TTL, httponly=True, samesite="lax", path="/"
|
|
)
|
|
return resp
|
|
|
|
@app.post("/api/logout")
|
|
def logout():
|
|
resp = JSONResponse({"ok": True})
|
|
resp.delete_cookie(COOKIE_NAME, path="/")
|
|
return resp
|
|
|
|
@app.get("/", response_class=HTMLResponse)
|
|
def root():
|
|
with open("/static/login.html") as f:
|
|
return f.read()
|