Security + minor fixes (#16)

* implement ip rate limiting

* fix: secure генерация кода для входа

* fix: possible slowloris and dos attacks

* fix: убрать лишний импорт, не давать сообщения из чата незнакомцам, географически верные названия в дб...

* fix device name не использовался

* refactor: убрал лишние импорты

* refactor: вернул dotenv

* убрал импорт после c642434
This commit is contained in:
whymequestion
2026-03-12 21:22:58 +05:00
committed by GitHub
parent 582c0f571c
commit e5c7a7baac
11 changed files with 222 additions and 38 deletions
+25 -3
View File
@@ -4,8 +4,19 @@ class Proto:
def __init__(self) -> None:
self.logger = logging.getLogger(__name__)
# TODO узнать какие должны быть лимиты и поменять,
# сейчас это больше заглушка
MAX_PAYLOAD_SIZE = 1048576 # 1 MB
MAX_DECOMPRESSED_SIZE = 1048576 # 1 MB
HEADER_SIZE = 10 # 1+2+1+2+4
### Работа с протоколом
def unpack_packet(self, data: bytes) -> dict | None:
# Проверяем минимальный размер пакета
if len(data) < self.HEADER_SIZE:
self.logger.warning(f"Пакет слишком маленький: {len(data)} байт")
return None
# Распаковываем заголовок
ver = int.from_bytes(data[0:1], "big")
cmd = int.from_bytes(data[1:3], "big")
@@ -18,6 +29,17 @@ class Proto:
# Парсим данные пакета
payload_length = packed_len & 0xFFFFFF
# Проверяем размер payload
if payload_length > self.MAX_PAYLOAD_SIZE:
self.logger.warning(f"Payload слишком большой: {payload_length} B (лимит {self.MAX_PAYLOAD_SIZE})")
return None
# Проверяем длину пакета
if len(data) < self.HEADER_SIZE + payload_length:
self.logger.warning(f"Пакет неполный: требуется {self.HEADER_SIZE + payload_length} B, получено {len(data)}")
return None
payload_bytes = data[10 : 10 + payload_length]
payload = None
@@ -27,14 +49,14 @@ class Proto:
if comp_flag != 0:
compressed_data = payload_bytes
try:
payload_bytes = lz4.block.decompress(
compressed_data,
uncompressed_size=99999,
uncompressed_size=self.MAX_DECOMPRESSED_SIZE,
)
except lz4.block.LZ4BlockError:
self.logger.warning("Ошибка декомпрессии LZ4")
return None
# Распаковываем msgpack
payload = msgpack.unpackb(payload_bytes, raw=False, strict_map_key=False)
+38 -5
View File
@@ -1,6 +1,7 @@
import asyncio, logging, traceback
from tamtam_tcp.proto import Proto
from tamtam_tcp.processors import Processors
from common.rate_limiter import RateLimiter
class TTMobileServer:
def __init__(self, host="0.0.0.0", port=443, ssl_context=None, db_pool=None, clients={}, send_event=None):
@@ -15,6 +16,12 @@ class TTMobileServer:
self.proto = Proto()
self.processors = Processors(db_pool=db_pool, clients=clients, send_event=send_event)
# rate limiter
self.auth_rate_limiter = RateLimiter(max_attempts=5, window_seconds=60)
self.read_timeout = 300 # Таймаут чтения из сокета (секунды)
self.max_read_size = 65536 # Максимальный размер данных из сокета
async def handle_client(self, reader, writer):
"""Функция для обработки подключений"""
# IP-адрес клиента
@@ -30,16 +37,33 @@ class TTMobileServer:
try:
while True:
# Читаем новые данные из сокета
data = await reader.read(4098)
# Читаем новые данные из сокета (с таймаутом!)
try:
data = await asyncio.wait_for(
reader.read(self.max_read_size),
timeout=self.read_timeout
)
except asyncio.TimeoutError:
self.logger.info(f"Таймаут соединения для {address[0]}:{address[1]}")
break
# Если сокет закрыт - выходим из цикла
if not data:
break
# Проверяем размер данных
if len(data) > self.max_read_size:
self.logger.warning(f"Пакет от {address[0]}:{address[1]} превышает лимит ({len(data)} байт)")
break
# Распаковываем данные
packet = self.proto.unpack_packet(data)
# Если пакет невалидный — пропускаем
if packet is None:
self.logger.warning(f"Невалидный пакет от {address[0]}:{address[1]}")
continue
opcode = packet.get("opcode")
seq = packet.get("seq")
payload = packet.get("payload")
@@ -48,11 +72,20 @@ class TTMobileServer:
case self.proto.HELLO:
deviceType, deviceName = await self.processors.process_hello(payload, seq, writer)
case self.proto.REQUEST_CODE:
await self.processors.process_request_code(payload, seq, writer)
if not self.auth_rate_limiter.is_allowed(address[0]):
await self.processors._send_error(seq, self.proto.REQUEST_CODE, self.processors.error_types.RATE_LIMITED, writer)
else:
await self.processors.process_request_code(payload, seq, writer)
case self.proto.VERIFY_CODE:
await self.processors.process_verify_code(payload, seq, writer)
if not self.auth_rate_limiter.is_allowed(address[0]):
await self.processors._send_error(seq, self.proto.VERIFY_CODE, self.processors.error_types.RATE_LIMITED, writer)
else:
await self.processors.process_verify_code(payload, seq, writer)
case self.proto.FINAL_AUTH:
await self.processors.process_final_auth(payload, seq, writer, deviceType, deviceName)
if not self.auth_rate_limiter.is_allowed(address[0]):
await self.processors._send_error(seq, self.proto.FINAL_AUTH, self.processors.error_types.RATE_LIMITED, writer)
else:
await self.processors.process_final_auth(payload, seq, writer, deviceType, deviceName)
case _:
self.logger.warning(f"Неизвестный опкод {opcode}")
except Exception as e: