From 9034485408f166592e8a3a69277a6d1996feddf8 Mon Sep 17 00:00:00 2001 From: Alexey Polyakov Date: Wed, 11 Mar 2026 15:21:49 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B0=D0=B7=D0=BB=D0=B8=D1=87=D0=BD?= =?UTF-8?q?=D1=8B=D0=B5=20=D1=84=D0=B8=D0=BA=D1=81=D1=8B=20(#14)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Исключаем только ошибку валидации * Небольшие правки вебсокета тамтама * Теперь номера брутить чутка сложнее * Авторизация теперь проверяется для некоторых команд * Теперь проверяется orign у вебсокета тамтама * Дополнил пример env * Починил немного сокет тамтама --- .env.example | 3 +- readme.md | 1 - src/common/config.py | 5 ++- src/common/tools.py | 38 ++++++++++++++++++++- src/main.py | 3 +- src/oneme_tcp/processors.py | 66 +++++++++++++++++++++--------------- src/oneme_tcp/server.py | 47 +++++++++++++++++++------ src/tamtam_tcp/processors.py | 43 ++++++++--------------- src/tamtam_ws/processors.py | 8 +++-- src/tamtam_ws/proto.py | 10 +++++- src/tamtam_ws/server.py | 24 +++++++------ 11 files changed, 164 insertions(+), 84 deletions(-) diff --git a/.env.example b/.env.example index ec2c8ec..051cdd4 100644 --- a/.env.example +++ b/.env.example @@ -23,4 +23,5 @@ keyfile = "key.pem" avatar_base_url = "http://127.0.0.1/avatar/" telegram_bot_token = "123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ" telegram_bot_enabled = "1" -telegram_whitelist_ids = "1,2,3" \ No newline at end of file +telegram_whitelist_ids = "1,2,3" +origins="http://127.0.0.1,https://web.openmax.su" \ No newline at end of file diff --git a/readme.md b/readme.md index 5165f10..d0e3d30 100644 --- a/readme.md +++ b/readme.md @@ -4,7 +4,6 @@ > > Использование в профессиональных средах не рекомендовано. - # OpenMAX Эмулятор сервера MAX и ТамТам diff --git a/src/common/config.py b/src/common/config.py index dc4c7ed..a4f7680 100644 --- a/src/common/config.py +++ b/src/common/config.py @@ -43,4 +43,7 @@ class ServerConfig: ### Telegram bot telegram_bot_token = os.getenv("telegram_bot_token") or "123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ" telegram_bot_enabled = bool(os.getenv("telegram_bot_enabled")) or True - telegram_whitelist_ids = [x.strip() for x in os.getenv("telegram_whitelist_ids", "").split(",") if x.strip()] \ No newline at end of file + telegram_whitelist_ids = [x.strip() for x in os.getenv("telegram_whitelist_ids", "").split(",") if x.strip()] + + ### origins + origins = [x.strip() for x in os.getenv("origins", "").split(",") if x.strip()] if os.getenv("origins") else None \ No newline at end of file diff --git a/src/common/tools.py b/src/common/tools.py index 70843c4..dcb3af3 100644 --- a/src/common/tools.py +++ b/src/common/tools.py @@ -46,6 +46,38 @@ class Tools: } else: return contact + + def generate_profile_tt( + self, id=1, phone=70000000000, avatarUrl=None, + photoId=None, updateTime=0, + firstName="Test", lastName="Account", options=[], + description=None, username=None + ): + contact = { + "id": id, + "updateTime": updateTime, + "phone": phone, + "names": [ + { + "name": f"{firstName} {lastName}", + "type": "TT" + } + ], + "options": options + } + + if avatarUrl: + contact["photoId"] = photoId + contact["baseUrl"] = avatarUrl + contact["baseRawUrl"] = avatarUrl + + if description: + contact["description"] = description + + if username: + contact["link"] = "https://tamtam.chat/" + username + + return contact def generate_chat(self, id, owner, type, participants, lastMessage, lastEventTime): """Генерация чата""" @@ -182,4 +214,8 @@ class Tools: } # Возвращаем - return message, int(row.get("time")) \ No newline at end of file + return message, int(row.get("time")) + + async def auth_required(self, userPhone, coro, *args): + if userPhone: + await coro(*args) diff --git a/src/main.py b/src/main.py index e9214e1..ec65824 100644 --- a/src/main.py +++ b/src/main.py @@ -68,7 +68,8 @@ async def main(): "db": db, "ssl": ssl_context, "clients": clients, - "event": api_event + "event": api_event, + "origins": server_config.origins } controllers = { diff --git a/src/oneme_tcp/processors.py b/src/oneme_tcp/processors.py index 199c3ea..05bf8de 100644 --- a/src/oneme_tcp/processors.py +++ b/src/oneme_tcp/processors.py @@ -48,8 +48,9 @@ class Processors: # Валидируем данные пакета try: HelloPayloadModel.model_validate(payload) - except Exception as e: - await self._send_error(seq, self.proto.HELLO, self.error_types.INVALID_PAYLOAD, writer) + except pydantic.ValidationError as error: + self.logger.error(f"Возникли ошибки при валидации пакета: {error}") + await self._send_error(seq, self.proto.SESSION_INIT, self.error_types.INVALID_PAYLOAD, writer) return None, None # Получаем данные из пакета @@ -86,7 +87,8 @@ class Processors: # Валидируем данные пакета try: PingPayloadModel.model_validate(payload) - except Exception as e: + except pydantic.ValidationError as error: + self.logger.error(f"Возникли ошибки при валидации пакета: {error}") await self._send_error(seq, self.proto.PING, self.error_types.INVALID_PAYLOAD, writer) return @@ -115,7 +117,8 @@ class Processors: # Валидируем данные пакета try: RequestCodePayloadModel.model_validate(payload) - except Exception as e: + except pydantic.ValidationError as error: + self.logger.error(f"Возникли ошибки при валидации пакета: {error}") await self._send_error(seq, self.proto.AUTH_REQUEST, self.error_types.INVALID_PAYLOAD, writer) return @@ -139,18 +142,15 @@ class Processors: await cursor.execute("SELECT * FROM users WHERE phone = %s", (phone,)) user = await cursor.fetchone() - # Если пользователя нет - отдаем ошибку - if user is None: - await self._send_error(seq, self.proto.AUTH_REQUEST, self.error_types.USER_NOT_FOUND, writer) - return + # Если пользователя найден - сохраняем токен и отправляем код + if user: + # Сохраняем токен + await cursor.execute("INSERT INTO auth_tokens (phone, token_hash, code_hash, expires) VALUES (%s, %s, %s, %s)", (phone, token_hash, code_hash, expires,)) - # Сохраняем токен - await cursor.execute("INSERT INTO auth_tokens (phone, token_hash, code_hash, expires) VALUES (%s, %s, %s, %s)", (phone, token_hash, code_hash, expires,)) - - # Если тг бот включен, и тг привязан к аккаунту - отправляем туда сообщение - if self.telegram_bot and user.get("telegram_id"): - await self.telegram_bot.send_code(chat_id=int(user.get("telegram_id")), phone=phone, code=code) - + # Если тг бот включен, и тг привязан к аккаунту - отправляем туда сообщение + if self.telegram_bot and user.get("telegram_id"): + await self.telegram_bot.send_code(chat_id=int(user.get("telegram_id")), phone=phone, code=code) + # Данные пакета payload = { "requestMaxDuration": 60000, @@ -174,7 +174,8 @@ class Processors: # Валидируем данные пакета try: VerifyCodePayloadModel.model_validate(payload) - except Exception as e: + except pydantic.ValidationError as error: + self.logger.error(f"Возникли ошибки при валидации пакета: {error}") await self._send_error(seq, self.proto.AUTH, self.error_types.INVALID_PAYLOAD, writer) return @@ -263,7 +264,8 @@ class Processors: # Валидируем данные пакета try: LoginPayloadModel.model_validate(payload) - except Exception as e: + except pydantic.ValidationError as error: + self.logger.error(f"Возникли ошибки при валидации пакета: {error}") await self._send_error(seq, self.proto.LOGIN, self.error_types.INVALID_PAYLOAD, writer) return @@ -365,7 +367,8 @@ class Processors: # Валидируем данные пакета try: AssetsPayloadModel.model_validate(payload) - except Exception as e: + except pydantic.ValidationError as error: + self.logger.error(f"Возникли ошибки при валидации пакета: {error}") await self._send_error(seq, self.proto.ASSETS_UPDATE, self.error_types.INVALID_PAYLOAD, writer) return @@ -390,7 +393,8 @@ class Processors: # Валидируем данные пакета try: GetCallHistoryPayloadModel.model_validate(payload) - except Exception as e: + except pydantic.ValidationError as error: + self.logger.error(f"Возникли ошибки при валидации пакета: {error}") await self._send_error(seq, self.proto.VIDEO_CHAT_HISTORY, self.error_types.INVALID_PAYLOAD, writer) return @@ -417,7 +421,8 @@ class Processors: # Валидируем данные пакета try: SendMessagePayloadModel.model_validate(payload) - except Exception as e: + except pydantic.ValidationError as error: + self.logger.error(f"Возникли ошибки при валидации пакета: {error}") await self._send_error(seq, self.proto.MSG_SEND, self.error_types.INVALID_PAYLOAD, writer) return @@ -529,7 +534,8 @@ class Processors: # Валидируем данные пакета try: SyncFoldersPayloadModel.model_validate(payload) - except Exception as e: + except pydantic.ValidationError as error: + self.logger.error(f"Возникли ошибки при валидации пакета: {error}") await self._send_error(seq, self.proto.FOLDERS_GET, self.error_types.INVALID_PAYLOAD, writer) return @@ -597,7 +603,8 @@ class Processors: # Валидируем данные пакета try: SearchUsersPayloadModel.model_validate(payload) - except Exception as e: + except pydantic.ValidationError as error: + self.logger.error(f"Возникли ошибки при валидации пакета: {error}") await self._send_error(seq, self.proto.CONTACT_INFO, self.error_types.INVALID_PAYLOAD, writer) return @@ -658,7 +665,8 @@ class Processors: # Валидируем данные пакета try: SearchChatsPayloadModel.model_validate(payload) - except Exception as e: + except pydantic.ValidationError as error: + self.logger.error(f"Возникли ошибки при валидации пакета: {error}") await self._send_error(seq, self.proto.CHAT_INFO, self.error_types.INVALID_PAYLOAD, writer) return @@ -731,7 +739,8 @@ class Processors: # Валидируем данные пакета try: SearchByPhonePayloadModel.model_validate(payload) - except Exception as e: + except pydantic.ValidationError as error: + self.logger.error(f"Возникли ошибки при валидации пакета: {error}") await self._send_error(seq, self.proto.CONTACT_INFO_BY_PHONE, self.error_types.INVALID_PAYLOAD, writer) return @@ -800,7 +809,8 @@ class Processors: # Валидируем данные пакета try: GetCallTokenPayloadModel.model_validate(payload) - except Exception as e: + except pydantic.ValidationError as error: + self.logger.error(f"Возникли ошибки при валидации пакета: {error}") await self._send_error(seq, self.proto.OK_TOKEN, self.error_types.INVALID_PAYLOAD, writer) return @@ -813,7 +823,8 @@ class Processors: # Валидируем данные пакета try: TypingPayloadModel.model_validate(payload) - except Exception as e: + except pydantic.ValidationError as error: + self.logger.error(f"Возникли ошибки при валидации пакета: {error}") await self._send_error(seq, self.proto.MSG_TYPING, self.error_types.INVALID_PAYLOAD, writer) return @@ -867,7 +878,8 @@ class Processors: # Валидируем данные пакета try: ComplainReasonsGetPayloadModel.model_validate(payload) - except Exception as e: + except pydantic.ValidationError as error: + self.logger.error(f"Возникли ошибки при валидации пакета: {error}") await self._send_error(seq, self.proto.COMPLAIN_REASONS_GET, self.error_types.INVALID_PAYLOAD, writer) return diff --git a/src/oneme_tcp/server.py b/src/oneme_tcp/server.py index 524e3b7..c49ab8d 100644 --- a/src/oneme_tcp/server.py +++ b/src/oneme_tcp/server.py @@ -1,6 +1,7 @@ import asyncio, logging, traceback from oneme_tcp.proto import Proto from oneme_tcp.processors import Processors +from common.tools import Tools class OnemeMobileServer: def __init__(self, host="0.0.0.0", port=443, ssl_context=None, db_pool=None, clients={}, send_event=None, telegram_bot=None): @@ -13,6 +14,7 @@ class OnemeMobileServer: self.clients = clients self.proto = Proto() + self.auth_required = Tools().auth_required self.processors = Processors(db_pool=db_pool, clients=clients, send_event=send_event, telegram_bot=telegram_bot) async def handle_client(self, reader, writer): @@ -54,6 +56,7 @@ class OnemeMobileServer: case self.proto.LOGIN: userPhone, userId, hashedToken = await self.processors.process_login(payload, seq, writer) + # Если авторизация на сервере успешная - можем завершить авторизацию if userPhone: await self._finish_auth(writer, address, userPhone, userId) case self.proto.LOGOUT: @@ -64,27 +67,49 @@ class OnemeMobileServer: case self.proto.LOG: await self.processors.process_telemetry(payload, seq, writer) case self.proto.ASSETS_UPDATE: - await self.processors.process_get_assets(payload, seq, writer) + await self.auth_required( + userPhone, self.processors.process_get_assets, payload, seq, writer + ) case self.proto.VIDEO_CHAT_HISTORY: - await self.processors.process_get_call_history(payload, seq, writer) + await self.auth_required( + userPhone, self.processors.process_get_call_history, payload, seq, writer + ) case self.proto.MSG_SEND: - await self.processors.process_send_message(payload, seq, writer, senderId=userId, db_pool=self.db_pool) + await self.auth_required( + userPhone, self.processors.process_send_message, payload, seq, writer, senderId=userId, db_pool=self.db_pool + ) case self.proto.FOLDERS_GET: - await self.processors.process_get_folders(payload, seq, writer, senderPhone=userPhone) + await self.auth_required( + userPhone, self.processors.process_get_folders, payload, seq, writer, senderPhone=userPhone + ) case self.proto.SESSIONS_INFO: - await self.processors.process_get_sessions(payload, seq, writer, senderPhone=userPhone, hashedToken=hashedToken) + await self.auth_required( + userPhone, self.processors.process_get_sessions, payload, seq, writer, senderPhone=userPhone, hashedToken=hashedToken + ) case self.proto.CHAT_INFO: - await self.processors.process_search_chats(payload, seq, writer, senderId=userId) + await self.auth_required( + userPhone, self.processors.process_search_chats, payload, seq, writer, senderId=userId + ) case self.proto.CONTACT_INFO_BY_PHONE: - await self.processors.process_search_by_phone(payload, seq, writer, senderId=userId) + await self.auth_required( + userPhone, self.processors.process_search_by_phone, payload, seq, writer, senderId=userId + ) case self.proto.OK_TOKEN: - await self.processors.process_get_call_token(payload, seq, writer) + await self.auth_required( + userPhone, self.processors.process_get_call_token, payload, seq, writer + ) case self.proto.MSG_TYPING: - await self.processors.process_typing(payload, seq, writer, senderId=userId) + await self.auth_required( + userPhone, self.processors.process_typing, payload, seq, writer, senderId=userId + ) case self.proto.CONTACT_INFO: - await self.processors.process_search_users(payload, seq, writer) + await self.auth_required( + userPhone, self.processors.process_search_users, payload, seq, writer + ) case self.proto.COMPLAIN_REASONS_GET: - await self.processors.process_complain_reasons_get(payload, seq, writer) + await self.auth_required( + userPhone, self.processors.process_complain_reasons_get, payload, seq, writer + ) case _: self.logger.warning(f"Неизвестный опкод {opcode}") except Exception as e: diff --git a/src/tamtam_tcp/processors.py b/src/tamtam_tcp/processors.py index 5faa5e7..849ac1d 100644 --- a/src/tamtam_tcp/processors.py +++ b/src/tamtam_tcp/processors.py @@ -95,12 +95,10 @@ class Processors: await cursor.execute("SELECT * FROM users WHERE phone = %s", (phone,)) user = await cursor.fetchone() - if user is None: - await self._send_error(seq, self.proto.REQUEST_CODE, self.error_types.USER_NOT_FOUND, writer) - return - - # Сохраняем токен - await cursor.execute("INSERT INTO auth_tokens (phone, token_hash, code_hash, expires, state) VALUES (%s, %s, %s, %s, %s)", (phone, token_hash, code_hash, expires, "started",)) + # Если пользователь существует, сохраняем токен + if user: + # Сохраняем токен + await cursor.execute("INSERT INTO auth_tokens (phone, token_hash, code_hash, expires, state) VALUES (%s, %s, %s, %s, %s)", (phone, token_hash, code_hash, expires, "started",)) # Данные пакета payload = { @@ -162,12 +160,6 @@ class Processors: # Обновляем состояние токена await cursor.execute("UPDATE auth_tokens set state = %s WHERE token_hash = %s", ("verified", hashed_token,)) - # # Создаем сессию - # await cursor.execute( - # "INSERT INTO tokens (phone, token_hash, device_type, device_name, location, time) VALUES (%s, %s, %s, %s, %s, %s)", - # (stored_token.get("phone"), hashed_login, deviceType, deviceName, "Epstein Island", int(time.time()),) - # ) - # Генерируем профиль # Аватарка с биографией photoId = None if not account.get("avatar_id") else int(account.get("avatar_id")) @@ -176,7 +168,7 @@ class Processors: # Собираем данные пакета payload = { - "profile": self.tools.generate_profile( + "profile": self.tools.generate_profile_tt( id=account.get("id"), phone=int(account.get("phone")), avatarUrl=avatar_url, @@ -186,12 +178,8 @@ class Processors: lastName=account.get("lastname"), options=json.loads(account.get("options")), description=description, - accountStatus=int(account.get("accountstatus")), - profileOptions=json.loads(account.get("profileoptions")), - includeProfileOptions=False, - username=account.get("username"), - type="TT" - ).get("contact"), + username=account.get("username") + ), "tokenAttrs": { "AUTH": { "token": token @@ -241,6 +229,7 @@ class Processors: await self._send_error(seq, self.proto.VERIFY_CODE, self.error_types.INVALID_TOKEN, writer) return + # Если авторизация только началась - отдаем ошибку if stored_token.get("state") == "started": await self._send_error(seq, self.proto.VERIFY_CODE, self.error_types.INVALID_TOKEN, writer) return @@ -249,7 +238,7 @@ class Processors: await cursor.execute("SELECT * FROM users WHERE phone = %s", (stored_token.get("phone"),)) account = await cursor.fetchone() - # Обновляем состояние токена + # Удаляем токен await cursor.execute("DELETE FROM auth_tokens WHERE token_hash = %s", (hashed_token,)) # Создаем сессию @@ -265,8 +254,8 @@ class Processors: # Собираем данные пакета payload = { - "userToken": "0", - "profile": self.tools.generate_profile( + "userToken": "0", # Пока как заглушка + "profile": self.tools.generate_profile_tt( id=account.get("id"), phone=int(account.get("phone")), avatarUrl=avatar_url, @@ -276,18 +265,16 @@ class Processors: lastName=account.get("lastname"), options=json.loads(account.get("options")), description=description, - accountStatus=int(account.get("accountstatus")), - profileOptions=json.loads(account.get("profileoptions")), - includeProfileOptions=False, - username=account.get("username"), - type="TT" - ).get("contact"), + username=account.get("username") + ), "tokenType": "LOGIN", "token": login } + # Создаем пакет packet = self.proto.pack_packet( cmd=self.proto.CMD_OK, seq=seq, opcode=self.proto.FINAL_AUTH, payload=payload ) + # Отправялем await self._send(writer, packet) \ No newline at end of file diff --git a/src/tamtam_ws/processors.py b/src/tamtam_ws/processors.py index 94bf2e4..66cba41 100644 --- a/src/tamtam_ws/processors.py +++ b/src/tamtam_ws/processors.py @@ -15,7 +15,10 @@ class Processors: async def _send(self, writer, packet): """Отправка пакета""" - await writer.send(packet) + try: + await writer.send(packet) + except Exception as error: + self.logger.error(f"Ошибка при отправке пакета - {error}") async def _send_error(self, seq, opcode, type, writer): payload = self.static.ERROR_TYPES.get(type, { @@ -36,7 +39,8 @@ class Processors: # Валидируем данные пакета try: HelloPayloadModel.model_validate(payload) - except Exception as e: + except pydantic.ValidationError as error: + self.logger.error(f"Возникли ошибки при валидации пакета: {error}") await self._send_error(seq, self.proto.SESSION_INIT, self.error_types.INVALID_PAYLOAD, writer) return None, None diff --git a/src/tamtam_ws/proto.py b/src/tamtam_ws/proto.py index 6bf093f..e9b88b0 100644 --- a/src/tamtam_ws/proto.py +++ b/src/tamtam_ws/proto.py @@ -18,7 +18,7 @@ class Proto: try: parsed_packet = json.loads(packet) except: - return {} + return None return parsed_packet # мне кажется долго вручную всё писать @@ -36,6 +36,14 @@ class Proto: # мб найдем че. она без обфускации # а ты ее видишь? # пошли + + ### Констаты протокола + CMD_OK = 1 + CMD_NOF = 2 + CMD_ERR = 3 + PROTO_VER = 10 + + ### Команды PING = 1 LOG = 5 SESSION_INIT = 6 diff --git a/src/tamtam_ws/server.py b/src/tamtam_ws/server.py index 6fd0dce..650b60c 100644 --- a/src/tamtam_ws/server.py +++ b/src/tamtam_ws/server.py @@ -6,12 +6,13 @@ from tamtam_ws.proto import Proto from tamtam_ws.processors import Processors class TTWSServer: - def __init__(self, host, port, db_pool=None, clients={}, send_event=None): + def __init__(self, host, port, db_pool=None, clients={}, send_event=None, origins=None): self.host = host self.port = port self.proto = Proto() self.processors = Processors(db_pool=db_pool, clients=clients, send_event=send_event) self.logger = logging.getLogger(__name__) + self.origins = origins async def handle_client(self, websocket): deviceType = None @@ -21,11 +22,17 @@ class TTWSServer: # Распаковываем пакет packet = self.proto.unpack_packet(message) + # Если ничего не извлекли + if packet is None: + self.logger.error(f"Не удалось распаковать пакет - {message}") + return + # Валидируем структуру пакета try: MessageModel.model_validate(packet) - except ValidationError as e: - self.logger.error(e) + except ValidationError as error: + self.logger.error(f"Произошла ошибка при валидации структуры пакета: {error}") + return # Извлекаем данные из пакета seq = packet['seq'] @@ -44,12 +51,6 @@ class TTWSServer: # УДАЛЯЕМ MYTRACKER ИЗ TAMTAM ТАМ ВИРУС # майтрекер отправляет все ваши сообщения на сервер барака обамы. немедленно удаляем!!! await self.processors.process_telemetry(payload, seq, websocket) - # case self.proto.AUTH_REQUEST: - # await self.processors.process_auth_request(payload, seq, websocket) - # case self.proto.VERIFY_CODE: - # await self.processors.process_verify_code(payload, seq, websocket) - # case self.proto.FINAL_AUTH: - # await self.processors.process_final_auth(payload, seq, websocket, deviceType, deviceName) # лан я пойду. пока # а ок @@ -57,5 +58,8 @@ class TTWSServer: async def start(self): self.logger.info(f"Вебсокет запущен на порту {self.port}") - async with serve(self.handle_client, self.host, self.port): + async with serve(handler=self.handle_client, + host=self.host, + port=self.port, + origins=self.origins): await asyncio.Future() \ No newline at end of file