| import os, secrets, hmac, hashlib, httpx, re |
| import gradio as gr |
| import spaces |
| from fastapi import Request, Form |
| from fastapi.responses import HTMLResponse, RedirectResponse |
|
|
| ADMIN_TOKEN = os.environ.get("ADMIN_TOKEN", "") |
| HF_TOKEN = os.environ.get("HF_TOKEN", "") |
| PRIVATE_URL = os.environ.get("PRIVATE_URL", "") |
| AUTH_HEADERS = {"Authorization": f"Bearer {HF_TOKEN}"} if HF_TOKEN else {} |
|
|
| @spaces.GPU(duration=30) |
| def _gpu(): |
| return "" |
|
|
| def make_token() -> str: |
| raw = secrets.token_hex(16) |
| key = ADMIN_TOKEN.encode() or b"" |
| return raw + "." + hmac.new(key, raw.encode(), hashlib.sha256).hexdigest() |
|
|
| def check_token(t: str) -> bool: |
| if not t or "." not in t: return False |
| p = t.split(".") |
| k = ADMIN_TOKEN.encode() or b"" |
| return hmac.compare_digest(p[1], hmac.new(k, p[0].encode(), hashlib.sha256).hexdigest()) |
|
|
| def rewrite(html: str) -> str: |
| for q in ['"', "'"]: |
| for a in ["href", "src", "action"]: |
| html = html.replace(f'{a}={q}/', f'{a}={q}/px/') |
| return html |
|
|
| LOGIN = '<!DOCTYPE html><html lang="es" class="dark"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>VESTA</title><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Manrope:wght@700;800&display=swap" rel="stylesheet"><style>:root{--p:#004355;--s:#f8fafb;--sl:#f2f4f5;--sm:#eceeef;--os:#191c1d;--osv:#40484c;--ov:#bfc8cc;--e:#ba1a1a}html.dark{--p:#8dd0ea;--s:#0e1415;--sl:#171d1e;--sm:#1b2122;--os:#dde3e5;--osv:#bfc8cc;--ov:#3f484c;--e:#ffb4ab}*{margin:0;padding:0;box-sizing:border-box}body{background:var(--s);color:var(--os);font-family:Inter,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;padding:1.5rem;font-size:15px}.c{background:var(--sl);border:1px solid var(--ov);border-radius:16px;padding:2rem 1.8rem;max-width:340px;width:100%;text-align:center}.l{font-family:Manrope,sans-serif;font-size:1.5rem;font-weight:800;color:var(--p);margin-bottom:2px}.s{font-size:.82rem;color:var(--osv);margin-bottom:1.5rem}.e{background:rgba(186,26,26,.08);border:1px solid var(--e);color:var(--e);font-size:.8rem;font-weight:600;padding:10px;border-radius:12px;margin-bottom:1rem;display:none}</style></head><body><div class="c"><div class="l">VESTA</div><div class="s">Acceso al ecosistema</div><div class="e" id="e"></div><form method="POST" action="/login"><input name="user" placeholder="Usuario" style="width:100%;background:var(--sm);border:1.5px solid var(--ov);border-radius:12px;color:var(--os);font-size:.9rem;padding:10px 12px;outline:none;margin-bottom:12px" required><input type="password" name="pass" placeholder="Contrasena" style="width:100%;background:var(--sm);border:1.5px solid var(--ov);border-radius:12px;color:var(--os);font-size:.9rem;padding:10px 12px;outline:none;margin-bottom:12px" required><button style="width:100%;background:var(--p);color:#fff;border:none;border-radius:12px;font-weight:700;padding:10px;font-size:.85rem;cursor:pointer">Ingresar</button></form></div><script>if(new URLSearchParams(location.search).get("error")){var e=document.getElementById("e");e.textContent="Usuario o contrasena incorrectos";e.style.display="block"}</script></body></html>' |
|
|
| with gr.Blocks(title="VESTA") as demo: |
| gr.Markdown("") |
|
|
| app = demo.app |
|
|
| def cookie(req: Request) -> str: |
| return req.cookies.get("vesta_session", "") |
|
|
| @app.get("/login") |
| async def login_get(req: Request): |
| t = cookie(req) |
| if check_token(t): |
| return RedirectResponse(url="/px/") |
| return HTMLResponse(LOGIN) |
|
|
| @app.post("/login") |
| async def do_login(user: str = Form(...), passw: str = Form(...)): |
| if user.lower() != "admin" or passw != ADMIN_TOKEN: |
| return RedirectResponse(url="/login?error=1", status_code=303) |
| token = make_token() |
| r = RedirectResponse(url="/px/", status_code=303) |
| r.set_cookie("vesta_session", token, max_age=86400, path="/", httponly=True) |
| return r |
|
|
| @app.get("/logout") |
| async def logout(): |
| r = RedirectResponse(url="/login") |
| r.delete_cookie("vesta_session", path="/") |
| return r |
|
|
| @app.get("/px/{rest:path}") |
| async def proxy(req: Request, rest: str = ""): |
| t = cookie(req) |
| if not check_token(t): |
| return RedirectResponse(url="/login") |
| path = "/" + rest |
| try: |
| async with httpx.AsyncClient(timeout=30) as c: |
| r = await c.get(f"{PRIVATE_URL}{path}", headers=AUTH_HEADERS) |
| content = r.text |
| if "text/html" in r.headers.get("content-type", ""): |
| content = rewrite(content) |
| return HTMLResponse(content) |
| except Exception as e: |
| return HTMLResponse(f"Error: {e}", 502) |
|
|
| @app.post("/px/proxy/predict") |
| async def predict(req: Request): |
| if not check_token(cookie(req)): |
| return HTMLResponse("NO_AUTH", 401) |
| body = await req.json() |
| async with httpx.AsyncClient(timeout=120) as c: |
| r = await c.post(f"{PRIVATE_URL}/proxy/predict", json=body, headers=AUTH_HEADERS) |
| return HTMLResponse(r.text, r.status_code) |
|
|
| demo.launch(server_name="0.0.0.0", server_port=7860, ssr_mode=False, head="<script>location.href='/login'</script>") |
|
|