fix(api): reap never-readable conns, bound total header time, harden watchdog
Review round 1 on 0ce144d (FIX-FIRST):
F1: stock gthread never reaps a connection that never sends a byte; it
holds a worker_connections slot forever and at 1000 per worker the run
loop stops polling, wedging the worker permanently (survives the
attacker disconnecting). accept/on_client_socket_readable/
murder_keepalived now track such conns in a FIFO with a deadline
(HAPROXY_MGR_FIRST_BYTE_TIMEOUT, 10s) and close expired ones on every
loop pass, including the at-capacity branch.
F2: slowloris (1 header byte per <20s) never tripped the per-recv
timeout. A total request-line+headers deadline
(HAPROXY_MGR_HEADER_DEADLINE, 10s) is armed in handle() and cleared in
handle_request(); a 1s monitor loop shutdown(SHUT_RDWR)s expired
sockets. Armed per thread, not at accept, so legit requests queued
behind busy threads are never cut for waiting.
F4: monitor loop wrapped in try/except with logging; wedge threshold
1200s -> 3600s (multi-domain cert requests run 180s per domain).
F5: docstring now says the timeout bounds each whole sendall().
F6: warn when gunicorn != 23.0.0 (worker overrides gthread internals).
API key compared with hmac.compare_digest.
Tests: +3 production-shape (2x4) tests: 2050 idle conns reaped and
/health recovers while still held; 8 drip conns cut within the deadline
with /health responsive; slow-but-complete headers still served.
15 tests, in the Docker build gate.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0ce144d2fb
commit
fd06b30d78
5 files changed
+343
-53
No files matched your search
+7
-1
@@ -7,6 +7,7 @@ import jinja2
|
||||
import socket
|
||||
import psutil
|
||||
import functools
|
||||
import hmac
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
@@ -165,7 +166,12 @@ def require_api_key(f):
|
||||
def decorated_function(*args, **kwargs):
|
||||
if API_KEY:
|
||||
auth_header = request.headers.get('Authorization')
|
||||
if not auth_header or auth_header != f'Bearer {API_KEY}':
|
||||
# Constant-time comparison so response timing doesn't leak how
|
||||
# much of a guessed key matched. Compared as bytes because
|
||||
# compare_digest rejects non-ASCII str.
|
||||
if not auth_header or not hmac.compare_digest(
|
||||
auth_header.encode('utf-8', 'surrogateescape'),
|
||||
f'Bearer {API_KEY}'.encode('utf-8', 'surrogateescape')):
|
||||
return jsonify({'error': 'Unauthorized - Invalid or missing API key'}), 401
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
Reference in new issue
Block a user