Spaces:
Build error
Build error
fix: enhance API key validation and implement stale IP eviction in rate limiting
Browse files- app/routes/commend/router.py +29 -4
app/routes/commend/router.py
CHANGED
|
@@ -117,14 +117,27 @@ class CommendHealthResponse(BaseModel):
|
|
| 117 |
|
| 118 |
_COMMEND_API_KEY = os.getenv("COMMEND_API_KEY")
|
| 119 |
_COMMEND_POSTING_ENABLED = os.getenv("COMMEND_ENABLE_POSTING", "false").lower() == "true"
|
|
|
|
| 120 |
|
| 121 |
# Simple in-memory rate limiter: IP -> list of timestamps
|
| 122 |
_rate_limit_store: dict[str, list[float]] = defaultdict(list)
|
| 123 |
_RATE_LIMIT_WINDOW = 60 # seconds
|
| 124 |
_RATE_LIMIT_MAX = 10 # requests per window
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
|
| 126 |
|
| 127 |
def _check_rate_limit(client_ip: str) -> None:
|
|
|
|
| 128 |
now = time.time()
|
| 129 |
window_start = now - _RATE_LIMIT_WINDOW
|
| 130 |
timestamps = _rate_limit_store[client_ip]
|
|
@@ -142,17 +155,29 @@ async def _require_api_key(
|
|
| 142 |
request: Request,
|
| 143 |
x_api_key: Optional[str] = Header(None, alias="X-API-Key"),
|
| 144 |
) -> None:
|
| 145 |
-
"""Validate API key if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
if not _COMMEND_API_KEY:
|
| 147 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
return
|
| 149 |
if x_api_key != _COMMEND_API_KEY:
|
| 150 |
-
logger.warning(f"Unauthorized commend request from {
|
| 151 |
raise HTTPException(
|
| 152 |
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 153 |
detail={"code": "unauthorized", "message": "Invalid or missing API key."}
|
| 154 |
)
|
| 155 |
-
_check_rate_limit(request.client.host if request.client else "unknown")
|
| 156 |
|
| 157 |
|
| 158 |
# --- API Endpoints ---
|
|
|
|
| 117 |
|
| 118 |
_COMMEND_API_KEY = os.getenv("COMMEND_API_KEY")
|
| 119 |
_COMMEND_POSTING_ENABLED = os.getenv("COMMEND_ENABLE_POSTING", "false").lower() == "true"
|
| 120 |
+
_COMMEND_REQUIRE_AUTH = os.getenv("COMMEND_REQUIRE_AUTH", "true").lower() != "false"
|
| 121 |
|
| 122 |
# Simple in-memory rate limiter: IP -> list of timestamps
|
| 123 |
_rate_limit_store: dict[str, list[float]] = defaultdict(list)
|
| 124 |
_RATE_LIMIT_WINDOW = 60 # seconds
|
| 125 |
_RATE_LIMIT_MAX = 10 # requests per window
|
| 126 |
+
_MAX_TRACKED_IPS = 10_000
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def _evict_stale_ips() -> None:
|
| 130 |
+
"""Remove expired entries when map grows beyond threshold."""
|
| 131 |
+
if len(_rate_limit_store) <= _MAX_TRACKED_IPS:
|
| 132 |
+
return
|
| 133 |
+
cutoff = time.time() - _RATE_LIMIT_WINDOW
|
| 134 |
+
stale_keys = [ip for ip, ts in _rate_limit_store.items() if all(t <= cutoff for t in ts)]
|
| 135 |
+
for key in stale_keys:
|
| 136 |
+
del _rate_limit_store[key]
|
| 137 |
|
| 138 |
|
| 139 |
def _check_rate_limit(client_ip: str) -> None:
|
| 140 |
+
_evict_stale_ips()
|
| 141 |
now = time.time()
|
| 142 |
window_start = now - _RATE_LIMIT_WINDOW
|
| 143 |
timestamps = _rate_limit_store[client_ip]
|
|
|
|
| 155 |
request: Request,
|
| 156 |
x_api_key: Optional[str] = Header(None, alias="X-API-Key"),
|
| 157 |
) -> None:
|
| 158 |
+
"""Validate API key. Fail-closed: if COMMEND_REQUIRE_AUTH is true (default)
|
| 159 |
+
and no COMMEND_API_KEY is configured, all requests are rejected."""
|
| 160 |
+
client_ip = request.client.host if request.client else "unknown"
|
| 161 |
+
|
| 162 |
+
# Always apply rate-limit regardless of auth configuration
|
| 163 |
+
_check_rate_limit(client_ip)
|
| 164 |
+
|
| 165 |
if not _COMMEND_API_KEY:
|
| 166 |
+
if _COMMEND_REQUIRE_AUTH:
|
| 167 |
+
# Fail-closed: auth required but no key configured
|
| 168 |
+
logger.error("COMMEND_API_KEY not set but auth is required. Rejecting request.")
|
| 169 |
+
raise HTTPException(
|
| 170 |
+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
| 171 |
+
detail={"code": "auth_not_configured", "message": "Service authentication is not configured."}
|
| 172 |
+
)
|
| 173 |
+
# Explicitly opted out of auth (COMMEND_REQUIRE_AUTH=false)
|
| 174 |
return
|
| 175 |
if x_api_key != _COMMEND_API_KEY:
|
| 176 |
+
logger.warning(f"Unauthorized commend request from {client_ip}")
|
| 177 |
raise HTTPException(
|
| 178 |
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 179 |
detail={"code": "unauthorized", "message": "Invalid or missing API key."}
|
| 180 |
)
|
|
|
|
| 181 |
|
| 182 |
|
| 183 |
# --- API Endpoints ---
|