boffire's picture
Update proxy.py
560961b verified
raw
history blame
5.69 kB
import os
import base64
import secrets
import httpx
import json
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import RedirectResponse, Response
from urllib.parse import quote, parse_qs
app = FastAPI()
LT_INTERNAL = "http://127.0.0.1:5000"
CLIENT_ID = os.environ.get("OAUTH_CLIENT_ID")
CLIENT_SECRET = os.environ.get("OAUTH_CLIENT_SECRET")
SPACE_HOST = os.environ.get("SPACE_HOST", "localhost")
REDIRECT_URI = f"https://{SPACE_HOST}/auth/callback"
sessions: dict[str, dict] = {}
SECURE_COOKIE = not SPACE_HOST.startswith("localhost")
# ── OAuth: login ──────────────────────────────────────────
@app.get("/auth/login")
async def auth_login():
if not CLIENT_ID:
raise HTTPException(status_code=500, detail="OAuth not configured")
state = secrets.token_urlsafe(32)
scope = quote("openid profile")
url = (
f"https://huggingface.co/oauth/authorize"
f"?client_id={CLIENT_ID}"
f"&redirect_uri={quote(REDIRECT_URI)}"
f"&response_type=code"
f"&scope={scope}"
f"&state={state}"
)
resp = RedirectResponse(url)
resp.set_cookie("oauth_state", state, max_age=600, httponly=True, samesite="lax", secure=SECURE_COOKIE)
return resp
# ── OAuth: callback ───────────────────────────────────────
@app.get("/auth/callback")
async def auth_callback(code: str, state: str, request: Request):
cookie_state = request.cookies.get("oauth_state")
if not cookie_state or cookie_state != state:
raise HTTPException(status_code=400, detail="Invalid state parameter")
basic = base64.b64encode(f"{CLIENT_ID}:{CLIENT_SECRET}".encode()).decode()
async with httpx.AsyncClient() as client:
r = await client.post(
"https://huggingface.co/oauth/token",
data={
"grant_type": "authorization_code",
"code": code,
"redirect_uri": REDIRECT_URI,
},
headers={"Authorization": f"Basic {basic}"},
timeout=30.0,
)
if r.status_code != 200:
raise HTTPException(status_code=400, detail=f"HF OAuth failed: {r.text}")
token_data = r.json()
session_id = secrets.token_urlsafe(32)
sessions[session_id] = token_data
resp = RedirectResponse("/")
resp.delete_cookie("oauth_state")
resp.set_cookie("lt_session", session_id, httponly=True, samesite="lax", secure=SECURE_COOKIE)
return resp
# ── OAuth: logout ─────────────────────────────────────────
@app.get("/auth/logout")
async def auth_logout():
resp = RedirectResponse("/")
resp.delete_cookie("lt_session")
return resp
# ── Helper: extract target language from request body ─────
def get_target_lang(body: bytes, content_type: str) -> str:
if not body:
return ""
if "application/json" in content_type:
try:
data = json.loads(body)
return data.get("target", "") or data.get("t", "")
except Exception:
return ""
elif "application/x-www-form-urlencoded" in content_type:
try:
form = parse_qs(body.decode("utf-8"))
return form.get("target", [""])[0] or form.get("t", [""])[0]
except Exception:
return ""
return ""
# ── Proxy everything to LibreTranslate ────────────────────
async def forward(request: Request, path: str, body: bytes):
async with httpx.AsyncClient() as client:
url = f"{LT_INTERNAL}/{path}"
if request.query_params:
url = f"{url}?{request.query_params}"
headers = {
k: v for k, v in request.headers.items()
if k.lower() not in ("host", "content-length")
}
try:
lt = await client.request(
method=request.method,
url=url,
headers=headers,
content=body,
timeout=60.0,
follow_redirects=True,
)
except Exception as e:
raise HTTPException(status_code=502, detail=f"LibreTranslate unreachable: {e}")
excluded = {"content-encoding", "transfer-encoding", "content-length"}
return Response(
content=lt.content,
status_code=lt.status_code,
headers={k: v for k, v in lt.headers.items() if k.lower() not in excluded},
)
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH"])
async def proxy(request: Request, path: str):
body = await request.body()
content_type = request.headers.get("content-type", "")
# ── /suggest gate: auth + kabyle-only ──────────────────
if path == "suggest" and request.method == "POST":
session = request.cookies.get("lt_session")
if not session or session not in sessions:
raise HTTPException(
status_code=401,
detail="You must be logged in with a HuggingFace account to submit suggestions. "
"Visit /auth/login first."
)
target = get_target_lang(body, content_type)
if target and target != "kab":
raise HTTPException(
status_code=403,
detail="Suggestions are only accepted for the Kabyle (kab) language."
)
return await forward(request, path, body)