boffire commited on
Commit
ccb5dad
Β·
verified Β·
1 Parent(s): c760ee6

Update proxy.py

Browse files
Files changed (1) hide show
  1. proxy.py +73 -29
proxy.py CHANGED
@@ -2,9 +2,10 @@ import os
2
  import base64
3
  import secrets
4
  import httpx
 
5
  from fastapi import FastAPI, Request, HTTPException
6
  from fastapi.responses import RedirectResponse, Response
7
- from urllib.parse import quote
8
 
9
  app = FastAPI()
10
  LT_INTERNAL = "http://127.0.0.1:5000"
@@ -14,12 +15,13 @@ CLIENT_SECRET = os.environ.get("OAUTH_CLIENT_SECRET")
14
  SPACE_HOST = os.environ.get("SPACE_HOST", "localhost")
15
  REDIRECT_URI = f"https://{SPACE_HOST}/auth/callback"
16
 
17
- # In-memory session store (fine for a single-container Space)
18
  sessions: dict[str, dict] = {}
19
 
20
- # ── OAuth login ─────────────────────────────────────────────
21
  @app.get("/auth/login")
22
  async def auth_login():
 
 
23
  state = secrets.token_urlsafe(32)
24
  scope = quote("openid profile")
25
  url = (
@@ -31,13 +33,16 @@ async def auth_login():
31
  f"&state={state}"
32
  )
33
  resp = RedirectResponse(url)
34
- resp.set_cookie("oauth_state", state, max_age=600, httponly=True, samesite="lax")
35
  return resp
36
 
37
- # ── OAuth callback ──────────────────────────────────────────
38
  @app.get("/auth/callback")
39
- async def auth_callback(code: str, state: str):
40
- # NOTE: In production, verify that `state` matches the cookie.
 
 
 
41
  basic = base64.b64encode(f"{CLIENT_ID}:{CLIENT_SECRET}".encode()).decode()
42
  async with httpx.AsyncClient() as client:
43
  r = await client.post(
@@ -50,44 +55,58 @@ async def auth_callback(code: str, state: str):
50
  headers={"Authorization": f"Basic {basic}"},
51
  timeout=30.0,
52
  )
 
53
  if r.status_code != 200:
54
- raise HTTPException(status_code=400, detail="HuggingFace OAuth failed")
55
 
56
  token_data = r.json()
57
  session_id = secrets.token_urlsafe(32)
58
  sessions[session_id] = token_data
59
 
60
  resp = RedirectResponse("/")
61
- resp.set_cookie("lt_session", session_id, httponly=True, samesite="lax")
 
62
  return resp
63
 
64
- # ── Proxy everything ────────────────────────────────────────
65
- @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH"])
66
- async def proxy(request: Request, path: str):
67
- # Guard /suggest behind HF OAuth
68
- if path == "suggest" and request.method == "POST":
69
- session = request.cookies.get("lt_session")
70
- if not session or session not in sessions:
71
- raise HTTPException(
72
- status_code=401,
73
- detail="You must be logged in with a HuggingFace account to submit suggestions. "
74
- "Visit /auth/login first."
75
- )
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
- # Forward to LibreTranslate
 
78
  async with httpx.AsyncClient() as client:
79
  url = f"{LT_INTERNAL}/{path}"
80
  if request.query_params:
81
  url = f"{url}?{request.query_params}"
82
 
83
- body = await request.body()
84
  headers = {
85
  k: v for k, v in request.headers.items()
86
  if k.lower() not in ("host", "content-length")
87
  }
88
 
89
  try:
90
- lt_resp = await client.request(
91
  method=request.method,
92
  url=url,
93
  headers=headers,
@@ -98,10 +117,35 @@ async def proxy(request: Request, path: str):
98
  except Exception as e:
99
  raise HTTPException(status_code=502, detail=f"LibreTranslate unreachable: {e}")
100
 
101
- # Strip hop-by-hop headers
102
  excluded = {"content-encoding", "transfer-encoding", "content-length"}
103
  return Response(
104
- content=lt_resp.content,
105
- status_code=lt_resp.status_code,
106
- headers={k: v for k, v in lt_resp.headers.items() if k.lower() not in excluded},
107
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import base64
3
  import secrets
4
  import httpx
5
+ import json
6
  from fastapi import FastAPI, Request, HTTPException
7
  from fastapi.responses import RedirectResponse, Response
8
+ from urllib.parse import quote, parse_qs
9
 
10
  app = FastAPI()
11
  LT_INTERNAL = "http://127.0.0.1:5000"
 
15
  SPACE_HOST = os.environ.get("SPACE_HOST", "localhost")
16
  REDIRECT_URI = f"https://{SPACE_HOST}/auth/callback"
17
 
 
18
  sessions: dict[str, dict] = {}
19
 
20
+ # ── OAuth: login ──────────────────────────────────────────
21
  @app.get("/auth/login")
22
  async def auth_login():
23
+ if not CLIENT_ID:
24
+ raise HTTPException(status_code=500, detail="OAuth not configured")
25
  state = secrets.token_urlsafe(32)
26
  scope = quote("openid profile")
27
  url = (
 
33
  f"&state={state}"
34
  )
35
  resp = RedirectResponse(url)
36
+ resp.set_cookie("oauth_state", state, max_age=600, httponly=True, samesite="lax", secure=True)
37
  return resp
38
 
39
+ # ── OAuth: callback ───────────────────────────────────────
40
  @app.get("/auth/callback")
41
+ async def auth_callback(code: str, state: str, request: Request):
42
+ cookie_state = request.cookies.get("oauth_state")
43
+ if not cookie_state or cookie_state != state:
44
+ raise HTTPException(status_code=400, detail="Invalid state parameter")
45
+
46
  basic = base64.b64encode(f"{CLIENT_ID}:{CLIENT_SECRET}".encode()).decode()
47
  async with httpx.AsyncClient() as client:
48
  r = await client.post(
 
55
  headers={"Authorization": f"Basic {basic}"},
56
  timeout=30.0,
57
  )
58
+
59
  if r.status_code != 200:
60
+ raise HTTPException(status_code=400, detail=f"HF OAuth failed: {r.text}")
61
 
62
  token_data = r.json()
63
  session_id = secrets.token_urlsafe(32)
64
  sessions[session_id] = token_data
65
 
66
  resp = RedirectResponse("/")
67
+ resp.delete_cookie("oauth_state")
68
+ resp.set_cookie("lt_session", session_id, httponly=True, samesite="lax", secure=True)
69
  return resp
70
 
71
+ # ── OAuth: logout ─────────────────────────────────────────
72
+ @app.get("/auth/logout")
73
+ async def auth_logout():
74
+ resp = RedirectResponse("/")
75
+ resp.delete_cookie("lt_session")
76
+ return resp
77
+
78
+ # ── Helper: extract target language from request body ─────
79
+ def get_target_lang(body: bytes, content_type: str) -> str:
80
+ if not body:
81
+ return ""
82
+ if "application/json" in content_type:
83
+ try:
84
+ data = json.loads(body)
85
+ return data.get("target", "") or data.get("t", "")
86
+ except Exception:
87
+ return ""
88
+ elif "application/x-www-form-urlencoded" in content_type:
89
+ try:
90
+ form = parse_qs(body.decode("utf-8"))
91
+ return form.get("target", [""])[0] or form.get("t", [""])[0]
92
+ except Exception:
93
+ return ""
94
+ return ""
95
 
96
+ # ── Proxy everything to LibreTranslate ────────────────────
97
+ async def forward(request: Request, path: str, body: bytes):
98
  async with httpx.AsyncClient() as client:
99
  url = f"{LT_INTERNAL}/{path}"
100
  if request.query_params:
101
  url = f"{url}?{request.query_params}"
102
 
 
103
  headers = {
104
  k: v for k, v in request.headers.items()
105
  if k.lower() not in ("host", "content-length")
106
  }
107
 
108
  try:
109
+ lt = await client.request(
110
  method=request.method,
111
  url=url,
112
  headers=headers,
 
117
  except Exception as e:
118
  raise HTTPException(status_code=502, detail=f"LibreTranslate unreachable: {e}")
119
 
 
120
  excluded = {"content-encoding", "transfer-encoding", "content-length"}
121
  return Response(
122
+ content=lt.content,
123
+ status_code=lt.status_code,
124
+ headers={k: v for k, v in lt.headers.items() if k.lower() not in excluded},
125
+ )
126
+
127
+ @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH"])
128
+ async def proxy(request: Request, path: str):
129
+ body = await request.body()
130
+ content_type = request.headers.get("content-type", "")
131
+
132
+ # ── /suggest gate: auth + kabyle-only ──────────────────
133
+ if path == "suggest" and request.method == "POST":
134
+ # 1. Must be logged in via HF
135
+ session = request.cookies.get("lt_session")
136
+ if not session or session not in sessions:
137
+ raise HTTPException(
138
+ status_code=401,
139
+ detail="You must be logged in with a HuggingFace account to submit suggestions. "
140
+ "Visit /auth/login first."
141
+ )
142
+
143
+ # 2. Must be for Kabyle
144
+ target = get_target_lang(body, content_type)
145
+ if target and target != "kab":
146
+ raise HTTPException(
147
+ status_code=403,
148
+ detail="Suggestions are only accepted for the Kabyle (kab) language."
149
+ )
150
+
151
+ return await forward(request, path, body)