Initial commit
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user