FastKoko / hf_app.py
remsky
feat: add credential honeypot messages to spark joy
662e3eb unverified
Raw
History Blame Contribute Delete
2.79 kB
# HF Space shim: serve the web player at / instead of a bare 404.
import os
import re
import time
from collections import deque
from fastapi import Request
from fastapi.responses import JSONResponse, PlainTextResponse, RedirectResponse
from api.src.main import app
MAX_BODY_BYTES = int(os.getenv("MAX_BODY_BYTES", 8192))
RATE_LIMIT_PER_MIN = int(os.getenv("RATE_LIMIT_PER_MIN", 20))
WEB_CACHE_SECONDS = int(os.getenv("WEB_CACHE_SECONDS", 600))
_hits: dict[str, deque] = {}
# Credential-scanner bait. Nothing here routes to a real endpoint.
_SCANNER = re.compile(
r"\.env|\.git|\.aws|\.ssh|\.npmrc|\.netrc|\.azure|\.claude|\.gcloud|\.openai|\.anthropic"
r"|credential|secrets?\.|service.?account|\.tfstate|terraform|wp-config|\.php"
r"|actuator|telescope|debugbar|horizon|appsettings|application\.(properties|yml)"
r"|docker-compose|web\.config|WEB-INF|\.(sql|bak|orig|swp)$|~$"
r"|169\.254\.169\.254|metadata\.google\.internal|bash_history",
re.I,
)
_HONEYPOT = """# nice try
AWS_ACCESS_KEY_ID=AKIA00000000GOTEEM
AWS_SECRET_ACCESS_KEY=touch/grass/and/get/a/real/job/friend
OPENAI_API_KEY=sk-proj-you-are-scanning-a-text-to-speech-demo
DATABASE_URL=postgres://nobody:nothing@localhost:5432/there_is_no_database
ADMIN_PASSWORD=hunter2
MOTD=this box only makes robot voices. go bother someone else.
"""
def _client_ip(request: Request) -> str:
forwarded = request.headers.get("x-forwarded-for", "").split(",")[-1].strip()
return forwarded or (request.client.host if request.client else "unknown")
@app.middleware("http")
async def throttle(request: Request, call_next):
target = request.url.path + "?" + request.url.query
if _SCANNER.search(target):
return PlainTextResponse(_HONEYPOT, status_code=200)
if request.method != "POST":
response = await call_next(request)
if request.url.path.startswith("/web/") and response.status_code == 200:
response.headers["Cache-Control"] = f"public, max-age={WEB_CACHE_SECONDS}"
return response
length = request.headers.get("content-length")
if length and int(length) > MAX_BODY_BYTES:
return JSONResponse({"detail": "Request body too large"}, status_code=413)
now = time.monotonic()
for stale in [k for k, v in _hits.items() if not v or v[-1] < now - 60]:
del _hits[stale]
hits = _hits.setdefault(_client_ip(request), deque())
while hits and hits[0] < now - 60:
hits.popleft()
if len(hits) >= RATE_LIMIT_PER_MIN:
return JSONResponse(
{"detail": "Rate limit exceeded"}, status_code=429, headers={"Retry-After": "60"}
)
hits.append(now)
return await call_next(request)
@app.get("/", include_in_schema=False)
async def root():
return RedirectResponse(url="/web/")