Skip to content

Commit c9b590c

Browse files
committed
feat(api): optional X-API-Key auth and naive rate limiting (per-IP)
1 parent ebfecf6 commit c9b590c

File tree

1 file changed

+31
-1
lines changed

1 file changed

+31
-1
lines changed

backend/pdf_server.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from backend.types import BasicAnalysis
1616
from bleach.sanitizer import Cleaner
1717
from backend.errors import register_error_handlers, ValidationError
18+
import time
1819

1920
# Настраиваем логирование
2021
logging.basicConfig(level=logging.DEBUG)
@@ -65,10 +66,39 @@ def _block_external_url_fetcher(url):
6566
r"/api/*": {
6667
"origins": ["http://localhost:3000"],
6768
"methods": ["GET", "POST", "OPTIONS"],
68-
"allow_headers": ["Content-Type"]
69+
"allow_headers": ["Content-Type", "X-API-Key"]
6970
}
7071
})
7172

73+
# Опциональная API-авторизация и наивный rate limiting
74+
API_KEY = os.getenv("API_KEY") # если не задан, проверка отключена
75+
RATE_LIMIT_WINDOW_SEC = int(os.getenv("RATE_LIMIT_WINDOW_SEC", "60"))
76+
RATE_LIMIT_MAX_REQ = int(os.getenv("RATE_LIMIT_MAX_REQ", "60"))
77+
_rate_limit_store: dict[str, list[float]] = {}
78+
79+
def _client_id() -> str:
80+
return request.headers.get("X-Forwarded-For", request.remote_addr or "unknown")
81+
82+
@app.before_request
83+
def _security_and_rate_limit():
84+
if not request.path.startswith("/api/"):
85+
return
86+
if API_KEY:
87+
provided = request.headers.get("X-API-Key")
88+
if provided != API_KEY:
89+
return jsonify({"error": "Unauthorized"}), 401
90+
now = time.time()
91+
cid = _client_id()
92+
bucket = _rate_limit_store.get(cid, [])
93+
cutoff = now - RATE_LIMIT_WINDOW_SEC
94+
bucket = [ts for ts in bucket if ts > cutoff]
95+
if len(bucket) >= RATE_LIMIT_MAX_REQ:
96+
retry_after = int(bucket[0] + RATE_LIMIT_WINDOW_SEC - now) + 1
97+
return jsonify({"error": "Too Many Requests", "retry_after": retry_after}), 429
98+
bucket.append(now)
99+
_rate_limit_store[cid] = bucket
100+
101+
72102
def perform_basic_analysis(df: pd.DataFrame) -> BasicAnalysis:
73103
"""Выполняет базовый анализ данных DataFrame."""
74104
logger.debug(f"Starting basic analysis. DataFrame shape: {df.shape}")

0 commit comments

Comments
 (0)