|
| 1 | +"""Redis URL helpers for the Docker API server. |
| 2 | +
|
| 3 | +The Docker entrypoint protects the in-container Redis with a password. These |
| 4 | +helpers keep every Redis client (the app and optional rate-limit storage) using |
| 5 | +the same authenticated URL shape. |
| 6 | +""" |
| 7 | + |
| 8 | +import os |
| 9 | +from urllib.parse import quote, urlsplit, urlunsplit |
| 10 | + |
| 11 | + |
| 12 | +def redis_auth_from_config(config: dict) -> tuple[str, str]: |
| 13 | + """Return Redis ACL username/password from config and environment.""" |
| 14 | + rc = config.get("redis", {}) |
| 15 | + username = os.environ.get("REDIS_USERNAME", rc.get("username", "default")) or "default" |
| 16 | + password = os.environ.get("REDIS_PASSWORD", rc.get("password", "")) or "" |
| 17 | + return str(username), str(password) |
| 18 | + |
| 19 | + |
| 20 | +def redis_auth_netloc(username: str, password: str) -> str: |
| 21 | + """Build a URL-safe Redis AUTH fragment for redis-py/limits clients. |
| 22 | +
|
| 23 | + Redis 6+ HELLO AUTH requires both username and password when negotiating |
| 24 | + RESP3. Supplying the explicit default ACL username avoids clients issuing an |
| 25 | + unauthenticated HELLO against a password-protected Redis instance. |
| 26 | + """ |
| 27 | + if not password: |
| 28 | + return "" |
| 29 | + return f"{quote(username, safe='')}:{quote(password, safe='')}@" |
| 30 | + |
| 31 | + |
| 32 | +def build_redis_url(config: dict) -> str: |
| 33 | + """Build Redis URL from config fields and environment variables.""" |
| 34 | + rc = config.get("redis", {}) |
| 35 | + host = os.environ.get("REDIS_HOST", rc.get("host", "localhost")) |
| 36 | + port = os.environ.get("REDIS_PORT", rc.get("port", 6379)) |
| 37 | + username, password = redis_auth_from_config(config) |
| 38 | + db = rc.get("db", 0) |
| 39 | + scheme = "rediss" if rc.get("ssl", False) else "redis" |
| 40 | + auth = redis_auth_netloc(username, password) |
| 41 | + return f"{scheme}://{auth}{host}:{port}/{db}" |
| 42 | + |
| 43 | + |
| 44 | +def build_rate_limit_storage_uri(config: dict) -> str: |
| 45 | + """Return a rate-limit storage URI that can auth to protected Redis. |
| 46 | +
|
| 47 | + Older/self-managed Docker configs may set rate_limiting.storage_uri to an |
| 48 | + unauthenticated Redis URL such as redis://localhost:6379. Since the Docker |
| 49 | + entrypoint now always protects the in-container Redis with REDIS_PASSWORD, |
| 50 | + SlowAPI/limits would fail before route handlers with Redis HELLO auth errors. |
| 51 | + If the storage URI is Redis-like and has no credentials, reuse the configured |
| 52 | + Redis ACL credentials. Explicit credentials and non-Redis backends are left |
| 53 | + unchanged. |
| 54 | + """ |
| 55 | + storage_uri = config.get("rate_limiting", {}).get("storage_uri", "memory://") |
| 56 | + if not isinstance(storage_uri, str): |
| 57 | + return "memory://" |
| 58 | + parsed = urlsplit(storage_uri) |
| 59 | + if parsed.scheme not in {"redis", "rediss", "redis+sentinel"}: |
| 60 | + return storage_uri |
| 61 | + if parsed.username or parsed.password: |
| 62 | + return storage_uri |
| 63 | + |
| 64 | + username, password = redis_auth_from_config(config) |
| 65 | + if not password: |
| 66 | + return storage_uri |
| 67 | + |
| 68 | + auth = redis_auth_netloc(username, password) |
| 69 | + return urlunsplit(( |
| 70 | + parsed.scheme, |
| 71 | + f"{auth}{parsed.netloc}", |
| 72 | + parsed.path, |
| 73 | + parsed.query, |
| 74 | + parsed.fragment, |
| 75 | + )) |
0 commit comments