mirror of
https://github.com/wowlikon/LiB.git
synced 2026-02-04 12:31:09 +00:00
104 lines
3.0 KiB
Python
104 lines
3.0 KiB
Python
import asyncio
|
|
import hashlib
|
|
import secrets
|
|
|
|
from fastapi import APIRouter, Request, Depends, HTTPException, status
|
|
from fastapi.responses import JSONResponse
|
|
from library_service.services.captcha import (
|
|
limiter,
|
|
get_ip,
|
|
active_challenges,
|
|
challenges_by_ip,
|
|
MAX_CHALLENGES_PER_IP,
|
|
MAX_TOTAL_CHALLENGES,
|
|
CHALLENGE_TTL,
|
|
REDEEM_TTL,
|
|
prng,
|
|
now_ms,
|
|
redeem_tokens,
|
|
)
|
|
|
|
router = APIRouter(prefix="/cap", tags=["captcha"])
|
|
|
|
|
|
@router.post("/challenge", summary="Задача capjs")
|
|
@limiter.limit("15/minute")
|
|
async def challenge(request: Request, ip: str = Depends(get_ip)):
|
|
"""Возвращает задачу capjs"""
|
|
if challenges_by_ip[ip] >= MAX_CHALLENGES_PER_IP:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail="Too many challenges"
|
|
)
|
|
if len(active_challenges) >= MAX_TOTAL_CHALLENGES:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Server busy"
|
|
)
|
|
|
|
token = secrets.token_hex(25)
|
|
redeem = secrets.token_hex(25)
|
|
expires = now_ms() + CHALLENGE_TTL
|
|
|
|
active_challenges[token] = {
|
|
"c": 50,
|
|
"s": 32,
|
|
"d": 4,
|
|
"expires": expires,
|
|
"redeem_token": redeem,
|
|
"ip": ip,
|
|
}
|
|
challenges_by_ip[ip] += 1
|
|
|
|
return {"challenge": {"c": 50, "s": 32, "d": 4}, "token": token, "expires": expires}
|
|
|
|
|
|
@router.post("/redeem", summary="Проверка задачи")
|
|
@limiter.limit("30/minute")
|
|
async def redeem(request: Request, payload: dict, ip: str = Depends(get_ip)):
|
|
"""Возвращает capjs_token"""
|
|
token = payload.get("token")
|
|
solutions = payload.get("solutions", [])
|
|
|
|
if token not in active_challenges:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail="Invalid challenge"
|
|
)
|
|
|
|
ch = active_challenges.pop(token)
|
|
challenges_by_ip[ch["ip"]] -= 1
|
|
|
|
if now_ms() > ch["expires"]:
|
|
raise HTTPException(status_code=status.HTTP_410_GONE, detail="Expired")
|
|
if len(solutions) < ch["c"]:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST, detail="Bad solutions"
|
|
)
|
|
|
|
def verify(i: int) -> bool:
|
|
salt = prng(f"{token}{i+1}", ch["s"])
|
|
target = prng(f"{token}{i+1}d", ch["d"])
|
|
h = hashlib.sha256((salt + str(solutions[i])).encode()).hexdigest()
|
|
return h.startswith(target)
|
|
|
|
results = await asyncio.gather(
|
|
*(asyncio.to_thread(verify, i) for i in range(ch["c"]))
|
|
)
|
|
if not all(results):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid solution"
|
|
)
|
|
|
|
r_token = ch["redeem_token"]
|
|
redeem_tokens[r_token] = now_ms() + REDEEM_TTL
|
|
|
|
resp = JSONResponse(
|
|
{"success": True, "token": r_token, "expires": redeem_tokens[r_token]}
|
|
)
|
|
resp.set_cookie(
|
|
key="capjs_token",
|
|
value=r_token,
|
|
httponly=True,
|
|
samesite="lax",
|
|
max_age=REDEEM_TTL // 1000,
|
|
)
|
|
return resp
|