Files
talkscore-asr/app/security.py
T
Vladimir BryzgalovandClaude Opus 5 c992b7907d HTTPS через Caddy с автоматическими сертификатами
Токен и расшифровки разговоров ходили по сети открытым текстом. Теперь в
пакете лежит Caddy: он держит сертификаты Let's Encrypt и продлевает их сам,
а сервис уходит на localhost.

Отдельно решён вопрос подмены адреса: за прокси все запросы приходят с
localhost, и список разрешённых адресов пускал бы кого угодно. Заголовку
с настоящим адресом сервис верит только от доверенного прокси - на это
восемь тестов.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 01:41:34 +05:00

72 lines
2.9 KiB
Python

"""Проверка доступа: Bearer-токен и список разрешённых адресов."""
import ipaddress
import secrets
from ipaddress import IPv4Network, IPv6Network
__all__ = ["check_token", "parse_allowlist", "ip_allowed", "client_address"]
Network = IPv4Network | IPv6Network
def check_token(header_value: str | None, expected: str) -> bool:
"""Сверяет заголовок Authorization с настроенным токеном.
Пустой токен в конфиге не открывает доступ, а закрывает его полностью -
иначе забытая настройка молча выставила бы сервис наружу.
Сравнение идёт в постоянное время, чтобы токен нельзя было подобрать по таймингам.
"""
if not expected or not header_value:
return False
scheme, _, value = header_value.partition(" ")
if scheme.lower() != "bearer":
return False
return secrets.compare_digest(value.strip(), expected)
def parse_allowlist(raw: str) -> list[Network]:
"""Разбирает список адресов и подсетей через запятую.
Некорректные записи пропускаются: опечатка в конфиге не должна ронять сервис.
"""
nets: list[Network] = []
for item in (raw or "").split(","):
item = item.strip()
if not item:
continue
try:
nets.append(ipaddress.ip_network(item, strict=False))
except ValueError:
continue
return nets
def ip_allowed(client_ip: str | None, allowlist: list[Network]) -> bool:
"""Проверяет адрес по списку. Пустой список означает «проверка выключена»."""
if not allowlist:
return True
if not client_ip:
return False
try:
addr = ipaddress.ip_address(client_ip)
except ValueError:
return False
return any(addr in net for net in allowlist)
def client_address(peer_ip: str | None, forwarded: str | None,
trusted_proxies: list[Network]) -> str | None:
"""Возвращает адрес клиента с учётом обратного прокси.
Заголовку X-Forwarded-For верим только тогда, когда сам запрос пришёл
от доверенного прокси. Иначе любой желающий подставил бы себе разрешённый
адрес и обошёл ограничение.
"""
if not forwarded or not trusted_proxies or not ip_allowed(peer_ip, trusted_proxies):
return peer_ip
first = forwarded.split(",")[0].strip()
try:
ipaddress.ip_address(first)
except ValueError:
return peer_ip
return first