RemanenetSpy commited on
Commit
0970b7b
·
1 Parent(s): 52a5cbc

fix(billing): robust Turnstile verification with SKIP_CAPTCHA escape hatch and fail-open network fallback

Browse files
Files changed (1) hide show
  1. api/routes/billing.py +42 -40
api/routes/billing.py CHANGED
@@ -177,70 +177,72 @@ async def generate_new_key(
177
  ip = (request.client.host if request.client else None) or "unknown"
178
 
179
  # ── 1. Cloudflare Turnstile verification ─────────────────────────────────
180
- # Strip control chars (same issue that corrupted GROQ_API_KEY can affect this too)
181
  import re as _re
182
  def _clean_env(key: str) -> str:
183
  return _re.sub(r"[\x00-\x1f\x7f]", "", os.getenv(key, ""))
184
 
185
  turnstile_secret = _clean_env("CF_TURNSTILE_SECRET") or "1x0000000000000000000000000000000AA"
186
- is_test_secret = turnstile_secret == "1x0000000000000000000000000000000AA"
187
-
188
- # Also detect the frontend dev test sitekey token — if the frontend is using the
189
- # dev test sitekey (1x00000000000000000000AA) but backend has a real secret,
190
- # the verification will always fail. Surface a clear error instead of "captcha failed".
191
- _CF_DEV_TOKEN_PREFIX = "XXXX." # Cloudflare test tokens have a known prefix
192
- _is_frontend_dev_token = cf_token and (
193
- len(cf_token) < 30 or cf_token.startswith("XXXX")
194
- )
195
 
196
- if not is_test_secret:
197
  if not cf_token:
198
  raise HTTPException(status_code=400, detail="Captcha token required.")
199
 
200
- if _is_frontend_dev_token:
201
- logger.error(
202
- "Turnstile mismatch: frontend is using the CF dev test sitekey "
203
- "(1x00000000000000000000AA) but backend has a real CF_TURNSTILE_SECRET. "
204
- "Set NEXT_PUBLIC_CF_TURNSTILE_SITE_KEY to your real Cloudflare site key."
205
- )
206
- raise HTTPException(
207
- status_code=503,
208
- detail="Captcha configuration error. Please contact support.",
209
- )
210
-
211
  try:
212
  import httpx
213
  async with httpx.AsyncClient(timeout=8.0) as client:
214
  resp = await client.post(
215
  "https://challenges.cloudflare.com/turnstile/v0/siteverify",
216
- data={"secret": turnstile_secret, "response": cf_token, "remoteip": ip},
 
 
 
 
217
  )
218
- # Log the raw body before trying to parse — helps debug empty-error issues
219
  raw_body = resp.text
 
220
  try:
221
  result = resp.json()
222
- except Exception:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  logger.error(
224
- f"Turnstile siteverify returned non-JSON (status={resp.status_code}): {raw_body[:200]}"
 
 
 
 
 
225
  )
226
- raise HTTPException(status_code=503, detail="Could not verify captcha. Try again.")
227
  except HTTPException:
228
  raise
229
- except Exception as e:
230
- logger.warning(f"Turnstile verification network error: {type(e).__name__}: {e}")
231
- raise HTTPException(status_code=503, detail="Could not verify captcha. Try again.")
232
-
233
- if not result.get("success"):
234
- codes = result.get("error-codes", [])
235
  logger.warning(
236
- f"Turnstile rejected token for ip={ip}: codes={codes} "
237
- f"full_response={result}"
238
- )
239
- raise HTTPException(
240
- status_code=400,
241
- detail=f"Captcha verification failed ({', '.join(codes) if codes else 'unknown'}). "
242
- "Please refresh and try again.",
243
  )
 
 
244
 
245
  # ── 2. IP rate limiting — 3 keys per IP per calendar day ────────────────
246
  # Module-level store (resets on restart; good enough for HF Spaces)
 
177
  ip = (request.client.host if request.client else None) or "unknown"
178
 
179
  # ── 1. Cloudflare Turnstile verification ─────────────────────────────────
180
+ # Strip control chars from env vars (guards against newline-in-secret bug)
181
  import re as _re
182
  def _clean_env(key: str) -> str:
183
  return _re.sub(r"[\x00-\x1f\x7f]", "", os.getenv(key, ""))
184
 
185
  turnstile_secret = _clean_env("CF_TURNSTILE_SECRET") or "1x0000000000000000000000000000000AA"
186
+ is_test_secret = turnstile_secret == "1x0000000000000000000000000000000AA"
187
+
188
+ # Emergency escape hatch: set SKIP_CAPTCHA=true in HF Spaces to bypass
189
+ # Turnstile entirely while debugging. Remove once widget is confirmed working.
190
+ skip_captcha = os.getenv("SKIP_CAPTCHA", "").strip().lower() in ("1", "true", "yes")
 
 
 
 
191
 
192
+ if not is_test_secret and not skip_captcha:
193
  if not cf_token:
194
  raise HTTPException(status_code=400, detail="Captcha token required.")
195
 
196
+ cf_verified = False
 
 
 
 
 
 
 
 
 
 
197
  try:
198
  import httpx
199
  async with httpx.AsyncClient(timeout=8.0) as client:
200
  resp = await client.post(
201
  "https://challenges.cloudflare.com/turnstile/v0/siteverify",
202
+ data={
203
+ "secret": turnstile_secret,
204
+ "response": cf_token,
205
+ "remoteip": ip,
206
+ },
207
  )
 
208
  raw_body = resp.text
209
+ logger.info(f"Turnstile siteverify HTTP {resp.status_code}: {raw_body[:300]}")
210
  try:
211
  result = resp.json()
212
+ cf_verified = bool(result.get("success"))
213
+ if not cf_verified:
214
+ codes = result.get("error-codes", [])
215
+ logger.warning(
216
+ f"Turnstile rejected: ip={ip} codes={codes} response={result}"
217
+ )
218
+ raise HTTPException(
219
+ status_code=400,
220
+ detail=(
221
+ f"Captcha verification failed "
222
+ f"({', '.join(codes) if codes else 'check site/secret key pair'}). "
223
+ "Please refresh and try again."
224
+ ),
225
+ )
226
+ except HTTPException:
227
+ raise
228
+ except Exception as json_err:
229
  logger.error(
230
+ f"Turnstile siteverify returned non-JSON "
231
+ f"(HTTP {resp.status_code}): {raw_body[:300]} — {json_err}"
232
+ )
233
+ raise HTTPException(
234
+ status_code=503,
235
+ detail="Captcha service returned an unexpected response. Try again.",
236
  )
 
237
  except HTTPException:
238
  raise
239
+ except Exception as net_err:
 
 
 
 
 
240
  logger.warning(
241
+ f"Turnstile network error ({type(net_err).__name__}): {net_err} "
242
+ f"— allowing request through (fail-open)"
 
 
 
 
 
243
  )
244
+ # Fail-open: if Cloudflare's servers are unreachable, don't block users.
245
+ # IP rate-limiting below still provides abuse protection.
246
 
247
  # ── 2. IP rate limiting — 3 keys per IP per calendar day ────────────────
248
  # Module-level store (resets on restart; good enough for HF Spaces)