├── src ├── tests │ ├── __init__.py │ └── redis_test.py ├── backends │ ├── __init__.py │ ├── tests │ │ ├── __init__.py │ │ ├── gamespy │ │ │ ├── __init__.py │ │ │ ├── chat │ │ │ │ ├── __init__.py │ │ │ │ └── lib_tests.py │ │ │ ├── web │ │ │ │ ├── __init__.py │ │ │ │ └── handler_tests.py │ │ │ ├── natneg │ │ │ │ ├── __init__.py │ │ │ │ └── handler_tests.py │ │ │ ├── game_status │ │ │ │ ├── __init__.py │ │ │ │ └── handler_tests.py │ │ │ ├── query_report │ │ │ │ ├── __init__.py │ │ │ │ └── data_fetch_tests.py │ │ │ ├── server_browser │ │ │ │ ├── __init__.py │ │ │ │ ├── data_fetch_tests.py │ │ │ │ ├── handler_tests.py │ │ │ │ └── filter_tests.py │ │ │ ├── precence_search_player │ │ │ │ └── __init__.py │ │ │ └── presence_conection_manager │ │ │ │ └── __init__.py │ │ ├── http_tests │ │ │ ├── chat.http │ │ │ ├── natneg.http │ │ │ ├── game_stats.http │ │ │ ├── query_report.http │ │ │ ├── web_services.http │ │ │ ├── server_browser.http │ │ │ ├── presence_search_player.http │ │ │ └── presence_connection_manager.http │ │ └── utils.py │ ├── protocols │ │ ├── __init__.py │ │ └── gamespy │ │ │ ├── query_report │ │ │ ├── responses.py │ │ │ ├── broker.py │ │ │ └── requests.py │ │ │ ├── server_browser │ │ │ ├── data.py │ │ │ ├── responses.py │ │ │ └── requests.py │ │ │ ├── natneg │ │ │ ├── responses.py │ │ │ └── requests.py │ │ │ ├── game_traffic_relay │ │ │ ├── requests.py │ │ │ ├── handlers.py │ │ │ └── data.py │ │ │ ├── presence_connection_manager │ │ │ └── responses.py │ │ │ ├── game_status │ │ │ ├── response.py │ │ │ └── requests.py │ │ │ ├── presence_search_player │ │ │ ├── responses.py │ │ │ └── requests.py │ │ │ ├── web_services │ │ │ └── responses.py │ │ │ └── chat │ │ │ └── response.py │ ├── library │ │ ├── utils │ │ │ └── misc.py │ │ ├── abstractions │ │ │ └── contracts.py │ │ └── networks │ │ │ └── redis_brocker.py │ ├── urls.py │ ├── routers │ │ └── gamespy │ │ │ ├── game_traffic_relay.py │ │ │ ├── natneg.py │ │ │ └── query_report.py │ └── services │ │ └── register.py ├── frontends │ ├── __init__.py │ ├── tests │ │ ├── __init__.py │ │ ├── gamespy │ │ │ ├── __init__.py │ │ │ ├── chat │ │ │ │ └── __init__.py │ │ │ ├── library │ │ │ │ ├── __init__.py │ │ │ │ ├── encrypt_tests.py │ │ │ │ └── mock_objects.py │ │ │ ├── natneg │ │ │ │ ├── __init__.py │ │ │ │ ├── contract_tests.py │ │ │ │ ├── redis.py │ │ │ │ └── mock_objects.py │ │ │ ├── game_status │ │ │ │ ├── __init__.py │ │ │ │ ├── game_tests.py │ │ │ │ └── mock_objects.py │ │ │ ├── query_report │ │ │ │ ├── __init__.py │ │ │ │ ├── mock_objects.py │ │ │ │ └── game_tests.py │ │ │ ├── web_services │ │ │ │ └── __init__.py │ │ │ ├── game_traffic_relay │ │ │ │ ├── __init__.py │ │ │ │ ├── mock_objects.py │ │ │ │ └── handler_tests.py │ │ │ ├── server_browser │ │ │ │ ├── __init__.py │ │ │ │ └── encrypt_tests.py │ │ │ ├── presence_search_player │ │ │ │ ├── __init__.py │ │ │ │ ├── game_tests.py │ │ │ │ └── mock_objects.py │ │ │ └── presence_connection_manager │ │ │ │ ├── __init__.py │ │ │ │ ├── handler_tests.py │ │ │ │ ├── mock_objects.py │ │ │ │ └── game_tests.py │ │ └── test_runner.py │ ├── gamespy │ │ ├── protocols │ │ │ ├── __init__.py │ │ │ ├── chat │ │ │ │ ├── __init__.py │ │ │ │ ├── applications │ │ │ │ │ ├── broker.py │ │ │ │ │ └── server_launcher.py │ │ │ │ └── aggregates │ │ │ │ │ └── peer_room.py │ │ │ ├── game_status │ │ │ │ ├── __init__.py │ │ │ │ ├── aggregations │ │ │ │ │ ├── exceptions.py │ │ │ │ │ ├── enums.py │ │ │ │ │ └── gscrypt.py │ │ │ │ ├── contracts │ │ │ │ │ ├── results.py │ │ │ │ │ └── responses.py │ │ │ │ ├── abstractions │ │ │ │ │ ├── handlers.py │ │ │ │ │ └── contracts.py │ │ │ │ └── applications │ │ │ │ │ └── server_launcher.py │ │ │ ├── query_report │ │ │ │ ├── __init__.py │ │ │ │ ├── v1 │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── abstractions │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ ├── handlers.py │ │ │ │ │ │ └── contracts.py │ │ │ │ │ ├── aggregates │ │ │ │ │ │ └── enums.py │ │ │ │ │ ├── contracts │ │ │ │ │ │ ├── results.py │ │ │ │ │ │ └── responses.py │ │ │ │ │ └── applications │ │ │ │ │ │ ├── handlers.py │ │ │ │ │ │ └── switcher.py │ │ │ │ ├── aggregates │ │ │ │ │ ├── exceptions.py │ │ │ │ │ ├── enums.py │ │ │ │ │ ├── game_server_info.py │ │ │ │ │ ├── peer_room_info.py │ │ │ │ │ └── natneg_channel.py │ │ │ │ ├── v2 │ │ │ │ │ ├── abstractions │ │ │ │ │ │ ├── handlers.py │ │ │ │ │ │ └── contracts.py │ │ │ │ │ ├── contracts │ │ │ │ │ │ └── results.py │ │ │ │ │ ├── aggregates │ │ │ │ │ │ └── enums.py │ │ │ │ │ └── applications │ │ │ │ │ │ └── switcher.py │ │ │ │ └── applications │ │ │ │ │ └── server_launcher.py │ │ │ ├── server_browser │ │ │ │ ├── __init__.py │ │ │ │ ├── v2 │ │ │ │ │ ├── contracts │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ └── results.py │ │ │ │ │ ├── abstractions │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ └── handlers.py │ │ │ │ │ ├── aggregations │ │ │ │ │ │ ├── string_flags.py │ │ │ │ │ │ ├── exceptions.py │ │ │ │ │ │ └── enums.py │ │ │ │ │ └── applications │ │ │ │ │ │ └── client.py │ │ │ │ ├── aggregates │ │ │ │ │ └── exceptions.py │ │ │ │ └── applications │ │ │ │ │ └── server_launcher.py │ │ │ ├── web_services │ │ │ │ ├── __init__.py │ │ │ │ ├── modules │ │ │ │ │ ├── altas │ │ │ │ │ │ └── ___init__.py │ │ │ │ │ ├── direct2game │ │ │ │ │ │ ├── abstractions │ │ │ │ │ │ │ ├── handler.py │ │ │ │ │ │ │ └── contracts.py │ │ │ │ │ │ ├── contracts │ │ │ │ │ │ │ ├── results.py │ │ │ │ │ │ │ └── responses.py │ │ │ │ │ │ └── applications │ │ │ │ │ │ │ └── handlers.py │ │ │ │ │ ├── auth │ │ │ │ │ │ ├── exceptions │ │ │ │ │ │ │ └── general.py │ │ │ │ │ │ └── contracts │ │ │ │ │ │ │ └── results.py │ │ │ │ │ └── sake │ │ │ │ │ │ ├── exceptions │ │ │ │ │ │ └── general.py │ │ │ │ │ │ ├── contracts │ │ │ │ │ │ ├── results.py │ │ │ │ │ │ └── responses.py │ │ │ │ │ │ ├── applications │ │ │ │ │ │ └── handlers.py │ │ │ │ │ │ └── abstractions │ │ │ │ │ │ └── generals.py │ │ │ │ ├── aggregations │ │ │ │ │ ├── exceptions.py │ │ │ │ │ └── soap_envelop.py │ │ │ │ ├── abstractions │ │ │ │ │ ├── handler.py │ │ │ │ │ └── contracts.py │ │ │ │ └── applications │ │ │ │ │ └── server_launcher.py │ │ │ ├── game_traffic_relay │ │ │ │ ├── __init__.py │ │ │ │ ├── applications │ │ │ │ │ ├── broker.py │ │ │ │ │ ├── client.py │ │ │ │ │ ├── switcher.py │ │ │ │ │ └── server_launcher.py │ │ │ │ ├── aggregates │ │ │ │ │ └── exceptions.py │ │ │ │ └── contracts │ │ │ │ │ └── general.py │ │ │ ├── presence_search_player │ │ │ │ ├── __init__.py │ │ │ │ ├── applications │ │ │ │ │ ├── server_launcher.py │ │ │ │ │ └── client.py │ │ │ │ ├── abstractions │ │ │ │ │ ├── handler.py │ │ │ │ │ └── contracts.py │ │ │ │ └── contracts │ │ │ │ │ └── results.py │ │ │ ├── presence_connection_manager │ │ │ │ ├── __init__.py │ │ │ │ ├── aggregates │ │ │ │ │ ├── user_status.py │ │ │ │ │ ├── login_challenge.py │ │ │ │ │ └── sdk_revision.py │ │ │ │ ├── applications │ │ │ │ │ ├── server_launcher.py │ │ │ │ │ └── client.py │ │ │ │ ├── abstractions │ │ │ │ │ ├── handlers.py │ │ │ │ │ └── contracts.py │ │ │ │ └── contracts │ │ │ │ │ └── results.py │ │ │ └── natneg │ │ │ │ ├── __init__.py │ │ │ │ ├── aggregations │ │ │ │ ├── exceptions.py │ │ │ │ ├── natneg_cookie.py │ │ │ │ └── enums.py │ │ │ │ ├── abstractions │ │ │ │ └── handlers.py │ │ │ │ ├── applications │ │ │ │ ├── server_launcher.py │ │ │ │ └── client.py │ │ │ │ └── contracts │ │ │ │ ├── results.py │ │ │ │ └── responses.py │ │ ├── library │ │ │ ├── log │ │ │ │ └── __init__.py │ │ │ ├── encryption │ │ │ │ ├── __init__.py │ │ │ │ ├── encoding.py │ │ │ │ └── xor_encryption.py │ │ │ ├── exceptions │ │ │ │ ├── __init__.py │ │ │ │ └── general.py │ │ │ ├── extentions │ │ │ │ ├── __init__.py │ │ │ │ ├── bytes_extentions.py │ │ │ │ ├── file_watcher.py │ │ │ │ ├── encoding.py │ │ │ │ ├── redis_orm.py │ │ │ │ ├── gamespy_utils.py │ │ │ │ └── password_encoder.py │ │ │ ├── abstractions │ │ │ │ ├── __init__.py │ │ │ │ ├── brocker.py │ │ │ │ ├── contracts.py │ │ │ │ └── switcher.py │ │ │ └── network │ │ │ │ ├── __init__.py │ │ │ │ ├── udp_handler.py │ │ │ │ ├── websocket_brocker.py │ │ │ │ └── http_handler.py │ │ └── __init__.py │ └── app.py ├── requirements.txt ├── .vscode │ └── settings.json ├── Contribute.md └── .devcontainer │ └── devcontainer.json ├── common └── pg_certs │ ├── root │ ├── root.srl │ └── root.key │ └── server.key ├── Dockerfile └── docker-compose-unispy-env.yml /src/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backends/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontends/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backends/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontends/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backends/protocols/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backends/tests/gamespy/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backends/tests/gamespy/chat/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backends/tests/gamespy/web/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backends/tests/http_tests/chat.http: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backends/tests/http_tests/natneg.http: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontends/tests/gamespy/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backends/tests/gamespy/natneg/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backends/tests/http_tests/game_stats.http: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backends/tests/http_tests/query_report.http: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backends/tests/http_tests/web_services.http: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontends/gamespy/library/log/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/chat/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontends/tests/gamespy/chat/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontends/tests/gamespy/library/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontends/tests/gamespy/natneg/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backends/tests/gamespy/game_status/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backends/tests/gamespy/query_report/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backends/tests/gamespy/server_browser/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backends/tests/http_tests/server_browser.http: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontends/gamespy/library/encryption/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontends/gamespy/library/exceptions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontends/gamespy/library/extentions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontends/tests/gamespy/game_status/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontends/tests/gamespy/query_report/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontends/tests/gamespy/web_services/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backends/protocols/gamespy/query_report/responses.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backends/tests/http_tests/presence_search_player.http: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontends/gamespy/library/abstractions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/game_status/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/query_report/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/server_browser/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/web_services/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontends/tests/gamespy/game_traffic_relay/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontends/tests/gamespy/server_browser/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backends/tests/gamespy/precence_search_player/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backends/tests/http_tests/presence_connection_manager.http: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/game_traffic_relay/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/query_report/v1/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontends/tests/gamespy/presence_search_player/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backends/tests/gamespy/presence_conection_manager/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/presence_search_player/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontends/tests/gamespy/presence_connection_manager/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/presence_connection_manager/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/query_report/v1/abstractions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/server_browser/v2/contracts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/web_services/modules/altas/___init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /common/pg_certs/root/root.srl: -------------------------------------------------------------------------------- 1 | 45BD6FD72E54134CD40A3B242DC3A69B964E17BF 2 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/server_browser/v2/abstractions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/presence_connection_manager/aggregates/user_status.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/web_services/modules/direct2game/abstractions/handler.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/backends/protocols/gamespy/server_browser/data.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | # region V1 4 | 5 | 6 | # region V2 7 | 8 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/natneg/__init__.py: -------------------------------------------------------------------------------- 1 | # from .aggregations.enums import ( 2 | # NatClientIndex, 3 | 4 | # ) -------------------------------------------------------------------------------- /src/frontends/gamespy/__init__.py: -------------------------------------------------------------------------------- 1 | # from .library.exceptions.general import UniSpyException 2 | # from .protocols.natneg import NatClientIndex 3 | -------------------------------------------------------------------------------- /src/frontends/tests/gamespy/presence_connection_manager/handler_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | class HandlerTests(unittest.TestCase): 4 | def test_status(self): 5 | pass -------------------------------------------------------------------------------- /src/frontends/gamespy/library/network/__init__.py: -------------------------------------------------------------------------------- 1 | DATA_SIZE = 2048 2 | import abc 3 | 4 | 5 | class Server(abc.ABC): 6 | 7 | @abc.abstractmethod 8 | def start(self): 9 | pass 10 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/server_browser/v2/aggregations/string_flags.py: -------------------------------------------------------------------------------- 1 | SINGLE_SERVER_END_FLAG = 0 2 | ALL_SERVER_END_FLAG = b"\x00\xFF\xFF\xFF\xFF" 3 | STRING_SPLITER = 0 4 | NTS_STRING_FLAG = 255 5 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/chat/applications/broker.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from frontends.gamespy.library.network.websocket_brocker import WebSocketBrocker 4 | 5 | 6 | # class Brocker(WebSocketBrocker): 7 | 8 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/game_status/aggregations/exceptions.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.library.exceptions.general import UniSpyException 2 | 3 | 4 | class GSException(UniSpyException): 5 | pass 6 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/natneg/aggregations/exceptions.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.library.exceptions.general import UniSpyException 2 | 3 | 4 | class NatNegException(UniSpyException): 5 | pass 6 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/query_report/aggregates/exceptions.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.library.exceptions.general import UniSpyException 2 | 3 | 4 | class QRException(UniSpyException): 5 | pass 6 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/game_traffic_relay/applications/broker.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.library.network.websocket_brocker import WebSocketBrocker 2 | 3 | 4 | class Broker(WebSocketBrocker): 5 | pass 6 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/server_browser/v2/aggregations/exceptions.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.library.exceptions.general import UniSpyException 2 | 3 | 4 | class SBException(UniSpyException): 5 | pass 6 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/web_services/aggregations/exceptions.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.library.exceptions.general import UniSpyException 2 | 3 | 4 | class WebException(UniSpyException): 5 | pass 6 | 7 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/game_traffic_relay/aggregates/exceptions.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.library.exceptions.general import UniSpyException 2 | 3 | 4 | class GameTrafficException(UniSpyException): 5 | pass 6 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/web_services/modules/auth/exceptions/general.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.protocols.web_services.aggregations.exceptions import WebException 2 | 3 | 4 | class AuthException(WebException): 5 | pass 6 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/web_services/modules/sake/exceptions/general.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.protocols.web_services.aggregations.exceptions import WebException 2 | 3 | 4 | class SakeException(WebException): 5 | pass 6 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/server_browser/aggregates/exceptions.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.library.exceptions.general import UniSpyException 2 | 3 | 4 | class ServerBrowserException(UniSpyException): 5 | def __init__(self, message: str) -> None: 6 | super().__init__(message) 7 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/query_report/aggregates/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class GameServerStatus(Enum): 5 | NORMAL = 0 6 | UPDATE = 1 7 | SHUTDOWN = 2 8 | PLAYING = 3 9 | 10 | 11 | class ProtocolVersion(Enum): 12 | V1 = 1 13 | V2 = 2 14 | -------------------------------------------------------------------------------- /src/backends/protocols/gamespy/natneg/responses.py: -------------------------------------------------------------------------------- 1 | # region Response 2 | from backends.library.abstractions.contracts import DataResponse 3 | from frontends.gamespy.protocols.natneg.contracts.results import ConnectResult 4 | 5 | 6 | class ConnectResponse(DataResponse): 7 | result: ConnectResult 8 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/query_report/v1/aggregates/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class RequestType(Enum): 5 | HEARTBEAT = "heartbeat" 6 | HEARTBEAT_ACK = "gamename" 7 | 8 | 9 | class ServerStatus(Enum): 10 | START = 0 11 | CHANGED = 1 12 | SHUTDOWN = 2 13 | -------------------------------------------------------------------------------- /src/backends/tests/gamespy/server_browser/data_fetch_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | 5 | class DataFetchTests(unittest.TestCase): 6 | def test_server_main_list(self): 7 | pass 8 | 9 | def test_p2p_group_room_list(self): 10 | pass 11 | 12 | def test_server_info(self): 13 | pass 14 | -------------------------------------------------------------------------------- /src/frontends/tests/test_runner.py: -------------------------------------------------------------------------------- 1 | if __name__ == "__main__": 2 | import unittest 3 | # Create a test suite 4 | loader = unittest.TestLoader() 5 | suite = loader.discover(start_dir='./', pattern='*tests.py') 6 | 7 | # Run the tests 8 | runner = unittest.TextTestRunner() 9 | runner.run(suite) 10 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/natneg/aggregations/natneg_cookie.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class NatNegCookie(BaseModel): 7 | host_ip: str 8 | host_port: int 9 | heartbeat_ip: str 10 | heartbeat_port: int 11 | game_name: str 12 | natneg_message: list 13 | instant_key: int 14 | -------------------------------------------------------------------------------- /src/requirements.txt: -------------------------------------------------------------------------------- 1 | pyfiglet == 1.0.2 2 | prettytable == 3.11.0 3 | psycopg2-binary == 2.9.10 4 | sqlalchemy == 2.0.36 5 | email_validator == 2.1.1 6 | requests == 2.32.3 7 | fastapi[standard] == 0.115.4 8 | xmltodict == 0.14.2 9 | responses == 0.25.3 10 | redis == 5.2.0 11 | websockets == 15.0.1 12 | schedule == 1.2.2 13 | watchdog == 6.0.0 -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/query_report/v1/contracts/results.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.protocols.query_report.v1.abstractions.contracts import ResultBase 2 | from frontends.gamespy.protocols.query_report.v1.aggregates.enums import ServerStatus 3 | 4 | 5 | class HeartbeatPreResult(ResultBase): 6 | status: ServerStatus 7 | game_name: str 8 | -------------------------------------------------------------------------------- /src/frontends/gamespy/library/encryption/encoding.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class Encoding: 4 | @staticmethod 5 | def get_string(data: bytes) -> str: 6 | assert isinstance(data, bytes) 7 | return data.decode("ascii") 8 | 9 | @staticmethod 10 | def get_bytes(data: str) -> bytes: 11 | assert isinstance(data, str) 12 | return data.encode() 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Python image from the Docker Hub 2 | FROM python:3.12-slim 3 | 4 | # Set the working directory in the container 5 | WORKDIR /unispy-server 6 | 7 | # Copy the requirements file into the container 8 | COPY src/requirements.txt . 9 | 10 | # Install the dependencies 11 | RUN pip install --no-cache-dir -r requirements.txt 12 | 13 | RUN apt update 14 | RUN apt install -y curl -------------------------------------------------------------------------------- /src/backends/library/utils/misc.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.library.configs import CONFIG 2 | from frontends.gamespy.library.exceptions.general import UniSpyException 3 | 4 | 5 | def check_public_ip(real_ip: str, report_ip: str): 6 | if CONFIG.backend.is_check_public_ip: 7 | if real_ip != report_ip: 8 | raise UniSpyException( 9 | "client real ip is not equal to its config ip") 10 | -------------------------------------------------------------------------------- /src/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.analysis.typeCheckingMode": "basic", 3 | "workbench.iconTheme": "material-icon-theme", 4 | "python.testing.unittestArgs": [ 5 | "-v", 6 | "-s", 7 | ".", 8 | "-p", 9 | "*test*.py" 10 | ], 11 | "python.testing.pytestEnabled": false, 12 | "python.testing.unittestEnabled": true, 13 | "python.analysis.enablePytestSupport": false, 14 | } -------------------------------------------------------------------------------- /src/backends/urls.py: -------------------------------------------------------------------------------- 1 | PRESENCE_CONNECTION_MANAGER = "/GameSpy/PresenceConnectionManager" 2 | PRESENCE_SEARCH_PLAYER = "/GameSpy/PresenceSearchPlayer" 3 | SERVER_BROWSER_V1 = "/GameSpy/ServerBrowserV1" 4 | SERVER_BROWSER_V2 = "/GameSpy/ServerBrowserV2" 5 | QUERY_REPORT = "/GameSpy/QueryReport" 6 | NATNEG = "/GameSpy/NatNegotiation" 7 | GAMESTATUS = "/GameSpy/GameStatus" 8 | CHAT = "/GameSpy/Chat" 9 | WEB_SERVICES = "/GameSpy/WebServices" 10 | GAME_TRAFFIC_RELAY = "/GameSpy/GameTrafficRelay" 11 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/web_services/modules/direct2game/abstractions/contracts.py: -------------------------------------------------------------------------------- 1 | import frontends.gamespy.protocols.web_services.abstractions.contracts as lib 2 | from frontends.gamespy.protocols.web_services.aggregations.soap_envelop import SoapEnvelop 3 | 4 | NAMESPACE = "http://gamespy.net/commerce/" 5 | 6 | 7 | class RequestBase(lib.RequestBase): 8 | pass 9 | 10 | 11 | class ResultBase(lib.ResultBase): 12 | pass 13 | 14 | 15 | class ResponseBase(lib.ResponseBase): 16 | _content: SoapEnvelop -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/query_report/v1/contracts/responses.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.protocols.query_report.v1.abstractions.contracts import ResponseBase 2 | from frontends.gamespy.protocols.query_report.v1.aggregates.enums import ServerStatus 3 | from frontends.gamespy.protocols.query_report.v1.contracts.results import HeartbeatPreResult 4 | 5 | 6 | class HeartbeatPreResponse(ResponseBase): 7 | _result: HeartbeatPreResult 8 | 9 | def build(self) -> None: 10 | self.sending_buffer = "\\status\\" 11 | -------------------------------------------------------------------------------- /src/backends/tests/utils.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.library.abstractions.contracts import RequestBase 2 | 3 | 4 | def add_headers(request: RequestBase) -> dict: 5 | request.parse() 6 | if isinstance(request.raw_request, bytes): 7 | request.raw_request = request.raw_request.decode( 8 | "ascii", "backslashreplace") 9 | data = request.to_dict() 10 | data["client_ip"] = "192.168.0.1" 11 | data["server_id"] = "950b7638-a90d-469b-ac1f-861e63c8c613" 12 | data["client_port"] = 1234 13 | return data 14 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/query_report/v1/abstractions/handlers.py: -------------------------------------------------------------------------------- 1 | import frontends.gamespy.library.abstractions.handler as lib 2 | from frontends.gamespy.protocols.query_report.v1.abstractions.contracts import RequestBase 3 | from frontends.gamespy.protocols.query_report.applications.client import Client 4 | 5 | 6 | class CmdHandlerBase(lib.CmdHandlerBase): 7 | def __init__(self, client: Client, request: RequestBase) -> None: 8 | assert issubclass(type(request), RequestBase) 9 | assert isinstance(client, Client) 10 | super().__init__(client, request) 11 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/query_report/v2/abstractions/handlers.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.library.abstractions.handler import CmdHandlerBase as CHB 2 | from frontends.gamespy.protocols.query_report.v2.abstractions.contracts import RequestBase 3 | from frontends.gamespy.protocols.query_report.applications.client import Client 4 | 5 | 6 | class CmdHandlerBase(CHB): 7 | def __init__(self, client: Client, request: RequestBase) -> None: 8 | assert issubclass(type(request), RequestBase) 9 | assert isinstance(client, Client) 10 | super().__init__(client, request) 11 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/web_services/modules/auth/contracts/results.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.protocols.web_services.modules.auth.abstractions.general import LoginResultBase 2 | 3 | 4 | class LoginProfileResult(LoginResultBase): 5 | pass 6 | 7 | 8 | class LoginPs3CertResult(LoginResultBase): 9 | auth_token: str 10 | partner_challenge: str 11 | 12 | 13 | class LoginRemoteAuthResult(LoginResultBase): 14 | pass 15 | 16 | 17 | class LoginUniqueNickResult(LoginResultBase): 18 | pass 19 | 20 | 21 | class CreateUserAccountResult(LoginResultBase): 22 | pass 23 | -------------------------------------------------------------------------------- /src/backends/protocols/gamespy/game_traffic_relay/requests.py: -------------------------------------------------------------------------------- 1 | from uuid import UUID 2 | from pydantic import BaseModel 3 | 4 | from backends.library.abstractions.contracts import RequestBase 5 | 6 | """ 7 | There are 2 UpdateGTRServiceRequest class 8 | The other one is in frontends/gamespy/protocols/game_traffic_relay/contracts/general.py 9 | """ 10 | 11 | 12 | class GtrHeartBeatRequest(RequestBase): 13 | server_id: UUID 14 | public_ip_address: str 15 | public_port: int 16 | client_count: int 17 | raw_request: None = None 18 | client_ip: None = None 19 | client_port: None = None 20 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/web_services/abstractions/handler.py: -------------------------------------------------------------------------------- 1 | import frontends.gamespy.library.abstractions.handler as lib 2 | from frontends.gamespy.protocols.web_services.applications.client import Client 3 | from frontends.gamespy.protocols.web_services.abstractions.contracts import RequestBase 4 | 5 | 6 | class CmdHandlerBase(lib.CmdHandlerBase): 7 | _client: Client 8 | 9 | def __init__(self, client: Client, request: RequestBase) -> None: 10 | assert isinstance(client, Client) 11 | assert issubclass(type(request), RequestBase) 12 | super().__init__(client, request) 13 | -------------------------------------------------------------------------------- /src/Contribute.md: -------------------------------------------------------------------------------- 1 | ```python 2 | 3 | class BaseClass: 4 | """ 5 | We use class static member only to type hint the class instance member 6 | Do not initialize the class static member 7 | """ 8 | _property1:type1 9 | _property2:type2 10 | 11 | 12 | def __init__(self): 13 | # if the property do not have default value it must be initialized as None 14 | self._property1 = None 15 | # In the base class we have to check whether the _property has been initialized, if not we init it 16 | if self._property1 is not None: 17 | self._property2 = value2 18 | ``` -------------------------------------------------------------------------------- /src/backends/protocols/gamespy/presence_connection_manager/responses.py: -------------------------------------------------------------------------------- 1 | from backends.library.abstractions.contracts import DataResponse 2 | from frontends.gamespy.protocols.presence_connection_manager.contracts.results import BlockListResult, BuddyListResult, GetProfileResult, LoginResult 3 | 4 | 5 | class LoginResponse(DataResponse): 6 | result: LoginResult 7 | 8 | 9 | class BuddyListResponse(DataResponse): 10 | result: BuddyListResult 11 | 12 | 13 | class BlockListResponse(DataResponse): 14 | result: BlockListResult 15 | 16 | 17 | class GetProfileResponse(DataResponse): 18 | result: GetProfileResult 19 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/web_services/modules/direct2game/contracts/results.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | from frontends.gamespy.protocols.web_services.modules.direct2game.abstractions.contracts import ResultBase 4 | 5 | 6 | class GetPurchaseHistoryResult(ResultBase): 7 | code: int = 0 8 | 9 | 10 | class AvailableCode(IntEnum): 11 | STORE_ONLINE = 10 12 | STORE_OFFLINE_FOR_MAINTAIN = 20 13 | STORE_OFFLINE_RETIRED = 50 14 | STORE_NOT_YET_LAUNCHED = 100 15 | 16 | 17 | class GetStoreAvailabilityResult(ResultBase): 18 | code: int = 0 19 | store_status_id: AvailableCode = AvailableCode.STORE_ONLINE 20 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/web_services/modules/sake/contracts/results.py: -------------------------------------------------------------------------------- 1 | from typing import OrderedDict 2 | 3 | from pydantic import BaseModel 4 | from frontends.gamespy.protocols.web_services.modules.sake.abstractions.generals import ResultBase 5 | 6 | 7 | class CreateRecordResult(ResultBase): 8 | table_id: str 9 | record_id: str 10 | fields: list 11 | 12 | 13 | class GetMyRecordsResult(ResultBase): 14 | class GetMyRecordsInfo(BaseModel): 15 | field_name: str 16 | field_type: str 17 | field_value: str 18 | records: list[GetMyRecordsInfo] 19 | 20 | 21 | class SearchForRecordsResult(ResultBase): 22 | user_data: OrderedDict[str, str] 23 | -------------------------------------------------------------------------------- /src/frontends/gamespy/library/extentions/bytes_extentions.py: -------------------------------------------------------------------------------- 1 | import socket 2 | 3 | 4 | def bytes_to_int(input: bytes) -> int: 5 | assert isinstance(input, bytes) 6 | return int.from_bytes(input, "little") 7 | 8 | 9 | def int_to_bytes(input: int) -> bytes: 10 | assert isinstance(input, int) 11 | return input.to_bytes(4, "little", signed=False) 12 | 13 | 14 | def ip_to_4_bytes(ip: str) -> bytes: 15 | assert isinstance(ip, str) 16 | return socket.inet_aton(ip) 17 | 18 | 19 | def port_to_2_bytes(port: int) -> bytes: 20 | """ 21 | using for qr sb natneg to convert port to bytes 22 | """ 23 | assert isinstance(port, int) 24 | return port.to_bytes(2, "little") 25 | -------------------------------------------------------------------------------- /src/backends/protocols/gamespy/game_status/response.py: -------------------------------------------------------------------------------- 1 | from backends.library.abstractions.contracts import DataResponse 2 | from frontends.gamespy.protocols.game_status.contracts.results import AuthGameResult, AuthPlayerResult, GetPlayerDataResult, GetProfileIdResult, SetPlayerDataResult 3 | 4 | 5 | class AuthGameResponse(DataResponse): 6 | result: AuthGameResult 7 | 8 | 9 | class AuthPlayerResponse(DataResponse): 10 | result: AuthPlayerResult 11 | 12 | 13 | class GetPlayerDataResponse(DataResponse): 14 | result: GetPlayerDataResult 15 | 16 | 17 | class GetProfileIdResponse(DataResponse): 18 | result: GetProfileIdResult 19 | 20 | 21 | class SetPlayerDataResponse(DataResponse): 22 | result: SetPlayerDataResult 23 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/game_status/aggregations/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, IntEnum 2 | 3 | 4 | class AuthMethod(IntEnum): 5 | UNKNOWN = 0 6 | PROFILE_ID_AUTH = 0 7 | PARTNER_ID_AUTH = 1 8 | CDKEY_AUTH = 2 9 | 10 | 11 | class PersistStorageType(IntEnum): 12 | PRIVATE_READ_ONLY = 0 13 | PRIVATE_READ_WRITE = 1 14 | PUBLIC_READ_ONLY = 2 15 | PUBLIC_READ_WRITE = 3 16 | 17 | 18 | class GSErrorCode(IntEnum): 19 | GENERAL = 0 20 | PARSE = 1 21 | DATABASE = 2 22 | NOERROR = 3 23 | 24 | 25 | class RequestType(Enum): 26 | AUTH = "auth" 27 | AUTHP = "authp" 28 | NEWGAME = "newgame" 29 | GETPD = "getpd" 30 | SETPD = "setpd" 31 | UPDGAME = "updgame" 32 | GETPID = "getpid" -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/game_status/contracts/results.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import final 3 | from frontends.gamespy.protocols.game_status.abstractions.contracts import ResultBase 4 | 5 | 6 | @final 7 | class AuthGameResult(ResultBase): 8 | session_key: str 9 | game_name: str 10 | 11 | 12 | @final 13 | class AuthPlayerResult(ResultBase): 14 | profile_id: int 15 | 16 | 17 | @final 18 | class GetPlayerDataResult(ResultBase): 19 | data: str 20 | profile_id: int 21 | modified: datetime 22 | 23 | 24 | @final 25 | class GetProfileIdResult(ResultBase): 26 | profile_id: int 27 | 28 | 29 | @final 30 | class SetPlayerDataResult(ResultBase): 31 | profile_id: int 32 | modified: datetime 33 | -------------------------------------------------------------------------------- /docker-compose-unispy-env.yml: -------------------------------------------------------------------------------- 1 | services: 2 | redis: 3 | image: redis 4 | container_name: unispy_redis 5 | restart: always 6 | ports: 7 | - "6379:6379" 8 | command: redis-server --requirepass 123456 9 | networks: 10 | - unispy 11 | 12 | postgresql: 13 | image: postgres:14 14 | container_name: unispy_postgresql 15 | ports: 16 | - "5432:5432" 17 | restart: always 18 | environment: 19 | POSTGRES_USER: unispy 20 | POSTGRES_PASSWORD: 123456 21 | POSTGRES_DB: unispy 22 | volumes: 23 | - ./common/UniSpy_pg.sql:/docker-entrypoint-initdb.d/init.sql 24 | networks: 25 | - unispy 26 | 27 | networks: 28 | unispy: 29 | external: true 30 | name: unispy 31 | driver: bridge -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/natneg/abstractions/handlers.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.protocols.natneg.applications.client import Client 2 | from frontends.gamespy.protocols.natneg.abstractions.contracts import RequestBase, ResponseBase, ResultBase 3 | import frontends.gamespy.library.abstractions.handler as lib 4 | 5 | 6 | class CmdHandlerBase(lib.CmdHandlerBase): 7 | _request: RequestBase 8 | _result: ResultBase 9 | _response: ResponseBase 10 | 11 | def __init__(self, client: Client, request: RequestBase) -> None: 12 | super().__init__(client, request) 13 | assert isinstance(client, Client) 14 | assert issubclass(type(request), RequestBase) 15 | 16 | 17 | if __name__ == "__main__": 18 | # cmd = CmdHandlerBase(None, None) 19 | pass 20 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/chat/applications/server_launcher.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.library.abstractions.server_launcher import ServiceBase, ServicesFactory 2 | from frontends.gamespy.library.network.tcp_handler import TcpServer 3 | from frontends.gamespy.protocols.chat.applications.client import Client 4 | 5 | 6 | class Service(ServiceBase): 7 | 8 | def __init__(self) -> None: 9 | super().__init__( 10 | config_name="Chat", 11 | client_cls=Client, 12 | network_server_cls=TcpServer, 13 | ) 14 | 15 | 16 | if __name__ == "__main__": 17 | from frontends.gamespy.library.extentions.debug_helper import DebugHelper 18 | 19 | chat = Service() 20 | helper = DebugHelper("./frontends/", ServicesFactory([chat])) 21 | helper.start() 22 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/game_status/abstractions/handlers.py: -------------------------------------------------------------------------------- 1 | 2 | from frontends.gamespy.library.abstractions.contracts import ResponseBase 3 | import frontends.gamespy.library.abstractions.handler as lib 4 | from frontends.gamespy.protocols.game_status.abstractions.contracts import RequestBase, ResultBase 5 | from frontends.gamespy.protocols.game_status.applications.client import Client 6 | 7 | 8 | class CmdHandlerBase(lib.CmdHandlerBase): 9 | _client: Client 10 | _request: RequestBase 11 | _result: ResultBase 12 | _response: ResponseBase | None 13 | 14 | def __init__(self, client: Client, request: RequestBase) -> None: 15 | super().__init__(client, request) 16 | assert isinstance(client, Client) 17 | assert issubclass(type(request), RequestBase) 18 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/web_services/applications/server_launcher.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.library.abstractions.server_launcher import ServicesFactory, ServiceBase 2 | from frontends.gamespy.library.network.http_handler import HttpServer 3 | from frontends.gamespy.protocols.web_services.applications.client import Client 4 | 5 | 6 | class Service(ServiceBase): 7 | def __init__(self) -> None: 8 | super().__init__( 9 | config_name="WebServices", 10 | client_cls=Client, 11 | network_server_cls=HttpServer 12 | ) 13 | 14 | 15 | if __name__ == "__main__": 16 | from frontends.gamespy.library.extentions.debug_helper import DebugHelper 17 | web = Service() 18 | helper = DebugHelper("./frontends/", ServicesFactory([web])) 19 | helper.start() 20 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/server_browser/applications/server_launcher.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.library.abstractions.server_launcher import ServicesFactory, ServiceBase 2 | from frontends.gamespy.library.network.tcp_handler import TcpServer 3 | from frontends.gamespy.protocols.server_browser.v2.applications.client import Client 4 | 5 | 6 | class Service(ServiceBase): 7 | def __init__(self) -> None: 8 | super().__init__( 9 | config_name="ServerBrowserV2", client_cls=Client, network_server_cls=TcpServer 10 | ) 11 | 12 | 13 | if __name__ == "__main__": 14 | from frontends.gamespy.library.extentions.debug_helper import DebugHelper 15 | sb2 = Service() 16 | # todo: add v1 server here 17 | helper = DebugHelper("./frontends/", ServicesFactory([sb2])) 18 | helper.start() 19 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/natneg/applications/server_launcher.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.library.abstractions.server_launcher import ServicesFactory, ServiceBase 2 | from frontends.gamespy.library.network.udp_handler import UdpServer 3 | from frontends.gamespy.protocols.natneg.applications.client import Client 4 | 5 | 6 | class Service(ServiceBase): 7 | server: UdpServer 8 | 9 | def __init__(self) -> None: 10 | super().__init__( 11 | config_name="NatNegotiation", 12 | client_cls=Client, 13 | network_server_cls=UdpServer, 14 | ) 15 | 16 | 17 | if __name__ == "__main__": 18 | from frontends.gamespy.library.extentions.debug_helper import DebugHelper 19 | nn = Service() 20 | helper = DebugHelper("./frontends/", ServicesFactory([nn])) 21 | helper.start() 22 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/game_status/applications/server_launcher.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.protocols.game_status.applications.client import Client 2 | from frontends.gamespy.library.abstractions.server_launcher import ServicesFactory, ServiceBase 3 | from frontends.gamespy.library.network.tcp_handler import TcpServer 4 | 5 | 6 | class Service(ServiceBase): 7 | server: "TcpServer" 8 | 9 | def __init__(self) -> None: 10 | super().__init__( 11 | config_name="GameStatus", 12 | client_cls=Client, 13 | network_server_cls=TcpServer, 14 | ) 15 | 16 | 17 | if __name__ == "__main__": 18 | from frontends.gamespy.library.extentions.debug_helper import DebugHelper 19 | gs = Service() 20 | helper = DebugHelper("./frontends/", ServicesFactory([gs])) 21 | helper.start() 22 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/query_report/applications/server_launcher.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.library.abstractions.server_launcher import ServicesFactory, ServiceBase 2 | from frontends.gamespy.library.network.udp_handler import UdpServer 3 | from frontends.gamespy.protocols.query_report.applications.client import Client 4 | 5 | 6 | class Service(ServiceBase): 7 | natneg_channel: object 8 | 9 | def __init__(self) -> None: 10 | super().__init__( 11 | config_name="QueryReport", 12 | client_cls=Client, 13 | network_server_cls=UdpServer 14 | ) 15 | 16 | 17 | if __name__ == "__main__": 18 | from frontends.gamespy.library.extentions.debug_helper import DebugHelper 19 | qr = Service() 20 | helper = DebugHelper("./frontends/", ServicesFactory([qr])) 21 | helper.start() 22 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/presence_search_player/applications/server_launcher.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.library.abstractions.server_launcher import ServicesFactory, ServiceBase 2 | from frontends.gamespy.library.network.tcp_handler import TcpServer 3 | from frontends.gamespy.protocols.presence_search_player.applications.client import ( 4 | Client, 5 | ) 6 | 7 | 8 | class Service(ServiceBase): 9 | def __init__(self) -> None: 10 | super().__init__( 11 | config_name="PresenceSearchPlayer", 12 | client_cls=Client, 13 | network_server_cls=TcpServer 14 | ) 15 | 16 | 17 | if __name__ == "__main__": 18 | from frontends.gamespy.library.extentions.debug_helper import DebugHelper 19 | psp = Service() 20 | helper = DebugHelper("./frontends/", ServicesFactory([psp])) 21 | helper.start() 22 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/natneg/applications/client.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.library.abstractions.client import ClientBase 2 | from frontends.gamespy.library.log.log_manager import LogWriter 3 | from frontends.gamespy.library.network.udp_handler import UdpConnection 4 | from frontends.gamespy.library.configs import ServerConfig 5 | 6 | 7 | class Client(ClientBase): 8 | client_pool: dict[str, "Client"] = {} 9 | 10 | def __init__(self, connection: UdpConnection, server_config: ServerConfig, logger: LogWriter): 11 | super().__init__(connection, server_config, logger) 12 | self.is_log_raw = True 13 | 14 | def _create_switcher(self, buffer: bytes): 15 | assert isinstance(buffer, bytes) 16 | from frontends.gamespy.protocols.natneg.applications.switcher import Switcher 17 | 18 | return Switcher(self, buffer) 19 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/game_traffic_relay/contracts/general.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, UUID4 2 | 3 | from frontends.gamespy.library.abstractions.contracts import RequestBase 4 | from frontends.gamespy.protocols.natneg.aggregations.enums import NatClientIndex, NatPortType 5 | 6 | 7 | class InitPacketInfo(BaseModel): 8 | server_id: UUID4 9 | cookie: int 10 | version: int 11 | port_type: NatPortType 12 | client_index: NatClientIndex 13 | game_name: str 14 | use_game_port: bool 15 | public_ip: str 16 | public_port: int 17 | private_ip: str 18 | private_port: int 19 | 20 | 21 | class GtrHeartbeat(BaseModel): 22 | server_id: UUID4 23 | public_ip_address: str 24 | public_port: int 25 | client_count: int 26 | 27 | 28 | class MessageRelayRequest(RequestBase): 29 | raw_request: bytes 30 | pass 31 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/presence_connection_manager/applications/server_launcher.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.library.abstractions.server_launcher import ServicesFactory, ServiceBase 2 | from frontends.gamespy.library.network.tcp_handler import TcpServer 3 | from frontends.gamespy.protocols.presence_connection_manager.applications.client import ( 4 | Client, 5 | ) 6 | 7 | 8 | class Service(ServiceBase): 9 | def __init__(self) -> None: 10 | super().__init__( 11 | config_name="PresenceConnectionManager", 12 | client_cls=Client, 13 | network_server_cls=TcpServer, 14 | ) 15 | 16 | 17 | if __name__ == "__main__": 18 | from frontends.gamespy.library.extentions.debug_helper import DebugHelper 19 | pcm = Service() 20 | helper = DebugHelper("./frontends/", ServicesFactory([pcm])) 21 | helper.start() 22 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/presence_search_player/abstractions/handler.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.library.abstractions.handler import CmdHandlerBase as CHB 2 | from frontends.gamespy.protocols.presence_search_player.abstractions.contracts import RequestBase 3 | from frontends.gamespy.protocols.presence_search_player.applications.client import Client 4 | from frontends.gamespy.protocols.presence_search_player.aggregates.exceptions import GPException 5 | 6 | 7 | class CmdHandlerBase(CHB): 8 | def __init__(self, client: Client, request: RequestBase) -> None: 9 | assert issubclass(type(request), RequestBase) 10 | assert isinstance(client, Client) 11 | super().__init__(client, request) 12 | 13 | def _handle_exception(self, ex) -> None: 14 | if ex is GPException: 15 | self._client.send(ex) 16 | super()._handle_exception(ex) 17 | -------------------------------------------------------------------------------- /src/tests/redis_test.py: -------------------------------------------------------------------------------- 1 | 2 | # import redis 3 | # from frontends.gamespy.library.configs import CONFIG 4 | 5 | 6 | # # SESSION = redis.Redis.from_url(CONFIG.redis.url) 7 | 8 | # client = redis.from_url(CONFIG.redis.url) 9 | # pubsub = client.pubsub() 10 | # pubsub.subscribe("test") 11 | 12 | # for message in pubsub.listen(): 13 | # if message['type'] == 'message': 14 | # print(f"Received: {message['data'].decode('utf-8')}") 15 | 16 | # client.set("hello", "hi") 17 | # data = client.get("hello") 18 | # pass 19 | # import socket 20 | 21 | # SERVER_IP = "127.0.0.1" # change to target IP if needed 22 | # SERVER_PORT = 27901 23 | # MESSAGE = b"hello" 24 | 25 | # sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 26 | 27 | # try: 28 | # for _ in range(10): 29 | # sock.sendto(MESSAGE, (SERVER_IP, SERVER_PORT)) 30 | # print(f"Sent {MESSAGE!r} to {SERVER_IP}:{SERVER_PORT}") 31 | # finally: 32 | # sock.close() -------------------------------------------------------------------------------- /src/backends/protocols/gamespy/server_browser/responses.py: -------------------------------------------------------------------------------- 1 | from backends.library.abstractions.contracts import DataResponse 2 | from frontends.gamespy.protocols.server_browser.v2.contracts.results import ServerFullInfoListResult 3 | from frontends.gamespy.protocols.server_browser.v2.contracts.results import ( 4 | P2PGroupRoomListResult, 5 | SendMessageResult, 6 | UpdateServerInfoResult, 7 | ServerMainListResult, 8 | ServerFullInfoListResult, 9 | ) 10 | 11 | 12 | class ServerFullInfoListResponse(DataResponse): 13 | result: ServerFullInfoListResult 14 | 15 | 16 | class P2PGroupRoomListResponse(DataResponse): 17 | result: P2PGroupRoomListResult 18 | 19 | 20 | class SendMessageResponse(DataResponse): 21 | result: SendMessageResult 22 | 23 | 24 | class ServerInfoResponse(DataResponse): 25 | result: UpdateServerInfoResult 26 | 27 | 28 | class ServerMainListResponse(DataResponse): 29 | result: ServerMainListResult 30 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/presence_search_player/applications/client.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.library.abstractions.client import ClientBase 2 | 3 | from frontends.gamespy.library.abstractions.switcher import SwitcherBase 4 | from frontends.gamespy.library.log.log_manager import LogWriter 5 | from frontends.gamespy.library.network.tcp_handler import TcpConnection 6 | from frontends.gamespy.library.configs import ServerConfig 7 | 8 | 9 | class Client(ClientBase): 10 | client_pool: dict[str, "Client"] = {} 11 | 12 | def __init__(self, connection: TcpConnection, server_config: ServerConfig, logger: LogWriter): 13 | super().__init__(connection, server_config, logger) 14 | 15 | def _create_switcher(self, buffer: bytes) -> SwitcherBase: 16 | from frontends.gamespy.protocols.presence_search_player.applications.switcher import Switcher 17 | temp_buffer = buffer.decode() 18 | return Switcher(self, temp_buffer) 19 | -------------------------------------------------------------------------------- /src/frontends/gamespy/library/extentions/file_watcher.py: -------------------------------------------------------------------------------- 1 | import time 2 | from watchdog.observers import Observer 3 | from watchdog.events import FileSystemEventHandler 4 | 5 | class FileWatcher: 6 | pass 7 | 8 | class MyHandler(FileSystemEventHandler): 9 | def on_any_event(self, event): 10 | if event.is_directory: 11 | return None 12 | elif event.event_type == 'created': 13 | print(f"Created: {event.src_path}") 14 | elif event.event_type =='modified': 15 | print(f"Modified: {event.src_path}") 16 | 17 | 18 | if __name__ == "__main__": 19 | event_handler = MyHandler() 20 | observer = Observer() 21 | path = '.' # Monitor the current directory 22 | observer.schedule(event_handler, path, recursive=True) 23 | observer.start() 24 | try: 25 | while True: 26 | time.sleep(1) 27 | except KeyboardInterrupt: 28 | observer.stop() 29 | observer.join() -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/query_report/aggregates/game_server_info.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from uuid import UUID 3 | 4 | from pydantic import BaseModel 5 | 6 | from frontends.gamespy.library.extentions.bytes_extentions import ip_to_4_bytes 7 | from frontends.gamespy.protocols.query_report.aggregates.enums import GameServerStatus 8 | 9 | NESSESARY_KEYS: list[str] = ["gamename", "hostname", "hostport"] 10 | 11 | 12 | class GameServerInfo(BaseModel): 13 | server_id: UUID 14 | host_ip_address: str 15 | instant_key: str 16 | game_name: str 17 | query_report_port: int 18 | 19 | update_time: datetime 20 | status: GameServerStatus 21 | data:dict[str,str] 22 | 23 | @property 24 | def query_report_port_bytes(self) -> bytes: 25 | return self.query_report_port.to_bytes(2) 26 | 27 | @property 28 | def host_ip_address_bytes(self) -> bytes: 29 | return ip_to_4_bytes(self.host_ip_address) 30 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/game_status/aggregations/gscrypt.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from frontends.gamespy.library.abstractions.enctypt_base import EncryptBase 4 | from frontends.gamespy.library.encryption.xor_encryption import XorEncoding, XorType 5 | from frontends.gamespy.protocols.game_status.aggregations.exceptions import GSException 6 | 7 | 8 | class GSCrypt(EncryptBase): 9 | def decrypt(self, data: bytes) -> bytes: 10 | if b"final" not in data: 11 | raise GSException("Ciphertext must contains delimeter \\final\\") 12 | cipher = data[:-7] 13 | plain = XorEncoding.encode(cipher, XorType.TYPE_1) 14 | return plain + b"\\final\\" 15 | 16 | def encrypt(self, data: bytes) -> bytes: 17 | if b"final" not in data: 18 | raise GSException("Ciphertext must contains delimeter \\final\\") 19 | cipher = data[:-7] 20 | plain = XorEncoding.encode(cipher, XorType.TYPE_1) 21 | return plain + b"\\final\\" -------------------------------------------------------------------------------- /src/frontends/gamespy/library/extentions/encoding.py: -------------------------------------------------------------------------------- 1 | import enum 2 | import json 3 | from uuid import UUID 4 | 5 | from pydantic import BaseModel 6 | 7 | 8 | def get_string(data: bytes) -> str: 9 | return data.decode("ascii") 10 | 11 | 12 | def get_bytes(data: str) -> bytes: 13 | return data.encode("ascii") 14 | 15 | 16 | class UniSpyJsonEncoder(json.JSONEncoder): 17 | def default(self, obj): 18 | # Handle bytes 19 | if isinstance(obj, bytes): 20 | return obj.decode("ascii", "backslashreplace") 21 | # Handle enum and IntEnum 22 | elif isinstance(obj, enum.Enum): 23 | return obj.value 24 | elif isinstance(obj, enum.IntEnum): 25 | return obj.value 26 | elif isinstance(obj, UUID): 27 | return str(obj) 28 | elif isinstance(obj,BaseModel): 29 | return obj.model_dump_json() 30 | # Fallback to the default method for other types 31 | return super().default(obj) 32 | -------------------------------------------------------------------------------- /src/frontends/tests/gamespy/library/encrypt_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from frontends.gamespy.library.encryption.gs_encryption import ChatCrypt 3 | from frontends.gamespy.library.encryption.xor_encryption import XorEncoding, XorType 4 | 5 | 6 | class EncryptionTest(unittest.TestCase): 7 | def test_chat_encryption(self): 8 | enc = ChatCrypt("123345") 9 | result = enc.encrypt("hello".encode("ascii")) 10 | self.assertEqual(result, b"\xda\xaek^d") 11 | 12 | def test_chat_decryption(self): 13 | enc = ChatCrypt("123345") 14 | result = enc.decrypt(b"\xda\xaek^d") 15 | self.assertEqual(result, b"hello") 16 | 17 | def test_xor_encoding(self): 18 | raw = b"abcdefghijklmnopqrstuvwxyz" 19 | plaintext = XorEncoding.encode(raw, XorType.TYPE_1) 20 | self.assertEqual( 21 | b"&\x03\x0e\x016\x16\x1e[--\n\x01\x08=\x1f\tB64\x15\x18\x13$\x08\x00I", plaintext) 22 | 23 | 24 | if __name__ == "__main__": 25 | unittest.main() 26 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/query_report/aggregates/peer_room_info.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | 3 | 4 | class PeerRoomInfo(BaseModel): 5 | game_name: str 6 | group_id: int = Field(..., alias='groupid') 7 | room_name: str = Field(alias="hostname") 8 | number_of_waiting: int = Field(default=0, alias="numwaiting") 9 | max_waiting: int = Field(default=200, alias='maxwaiting') 10 | number_of_servers: int = Field(default=0, alias="numservers") 11 | number_of_players: int = Field(default=0, alias="numplayers") 12 | max_players: int = Field(default=200, alias="maxplayers") 13 | password: str = Field(default="", alias="password") 14 | number_of_games: int = Field(default=0, alias="numgames") 15 | number_of_playing: int = Field(default=0, alias="numplaying") 16 | 17 | def get_gamespy_dict(self) -> dict: 18 | """ 19 | return a immutable dict 20 | """ 21 | data = self.model_dump(mode="json") 22 | del data["game_name"] 23 | return data 24 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/query_report/v2/contracts/results.py: -------------------------------------------------------------------------------- 1 | from typing import final 2 | from frontends.gamespy.protocols.query_report.v2.abstractions.contracts import ResultBase 3 | from frontends.gamespy.protocols.query_report.v2.aggregates.enums import PacketType 4 | 5 | 6 | @final 7 | class AvailableResult(ResultBase): 8 | packet_type: PacketType = PacketType.AVALIABLE_CHECK 9 | 10 | 11 | @final 12 | class ChallengeResult(ResultBase): 13 | packet_type: PacketType = PacketType.CHALLENGE 14 | 15 | 16 | @final 17 | class ClientMessageResult(ResultBase): 18 | natneg_message: bytes 19 | message_key: int 20 | packet_type: PacketType = PacketType.CLIENT_MESSAGE 21 | 22 | 23 | @final 24 | class EchoResult(ResultBase): 25 | info: dict 26 | packet_type: PacketType = PacketType.ECHO 27 | 28 | 29 | @final 30 | class HeartbeatResult(ResultBase): 31 | """ 32 | this result is replied in unispy server 33 | """ 34 | packet_type: PacketType = PacketType.HEARTBEAT 35 | remote_ip: str 36 | remote_port: int 37 | -------------------------------------------------------------------------------- /src/frontends/tests/gamespy/natneg/contract_tests.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from frontends.gamespy.protocols.natneg.aggregations.enums import NatClientIndex 4 | from frontends.gamespy.protocols.natneg.applications.switcher import Switcher 5 | from frontends.gamespy.protocols.natneg.contracts.requests import ConnectAckRequest 6 | from frontends.tests.gamespy.natneg.mock_objects import create_client 7 | 8 | 9 | class ContractTests(TestCase): 10 | def test_connect_ack(self): 11 | raws = [ 12 | b'\xfd\xfc\x1efj\xb2\x04\x06\x00\x00\x02\x9a\x00\x01\x00\x00\xd8\xf2@\x00\x00', 13 | b'\xfd\xfc\x1efj\xb2\x04\x06\x00\x00\x02\x9aj\x01\x04\x07\x00\x00\x02\x9a\xac', 14 | b'\xfd\xfc\x1efj\xb2\x04\x06\x00\x00\x02\x9aj\x01\x04\x07\x00\x00\x02\x9a\xac', 15 | b'\xfd\xfc\x1efj\xb2\x04\x06\x00\x00\x02\x9a@\x01\x00\x00\xc0\xf28o\xfd', 16 | ] 17 | for raw in raws: 18 | r = ConnectAckRequest(raw) 19 | r.parse() 20 | self.assertEqual(r.client_index, NatClientIndex.GAME_SERVER) 21 | -------------------------------------------------------------------------------- /src/backends/protocols/gamespy/presence_search_player/responses.py: -------------------------------------------------------------------------------- 1 | from backends.library.abstractions.contracts import DataResponse 2 | from frontends.gamespy.protocols.presence_search_player.contracts.results import CheckResult, NewUserResult, NicksResult, OthersListResult, OthersResult, SearchResult, SearchUniqueResult, UniqueSearchResult, ValidResult 3 | 4 | 5 | class CheckResponse(DataResponse): 6 | result: CheckResult 7 | 8 | 9 | class NewUserResponse(DataResponse): 10 | result: NewUserResult 11 | 12 | 13 | class NicksResponse(DataResponse): 14 | result: NicksResult 15 | 16 | 17 | class OthersResponse(DataResponse): 18 | result: OthersResult 19 | 20 | 21 | class OthersListResponse(DataResponse): 22 | result: OthersListResult 23 | 24 | 25 | class SearchResponse(DataResponse): 26 | result: SearchResult 27 | 28 | 29 | class SearchUniqueResponse(DataResponse): 30 | result: SearchUniqueResult 31 | 32 | 33 | class ValidResponse(DataResponse): 34 | result: ValidResult 35 | 36 | class UniqueSearchResponse(DataResponse): 37 | result:UniqueSearchResult -------------------------------------------------------------------------------- /src/frontends/gamespy/library/extentions/redis_orm.py: -------------------------------------------------------------------------------- 1 | # import redis 2 | 3 | 4 | # class RedisORM: 5 | # def __init__(self, host="localhost", port=6379, db=0): 6 | # # self.redis_conn = redis.StrictRedis(host=host, port=port, db=db) 7 | # pass 8 | 9 | # def query(self, table_class): 10 | # return QueryBuilder(self.redis_conn, table_class) 11 | 12 | 13 | # class QueryBuilder: 14 | # def __init__(self, redis_conn, table_class): 15 | # self.redis_conn = redis_conn 16 | # self.table_class = table_class 17 | 18 | # def filter_by(self, **kwargs): 19 | # self.filter_criteria = kwargs 20 | # return self 21 | 22 | # def first(self): 23 | # key = f"{self.table_class.__name__}:{self.filter_criteria['url']}" 24 | # data = self.redis_conn.hgetall(key) 25 | # return data 26 | 27 | 28 | # # Example usage 29 | # class User: 30 | # pass 31 | 32 | 33 | # redis_orm = RedisORM() 34 | # query = QueryBuilder(None, None) 35 | # result = query.filter_by(url="example.com", name="hello").first() 36 | # print(result) 37 | 38 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/query_report/v1/abstractions/contracts.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | import frontends.gamespy.library.abstractions.contracts as lib 3 | 4 | from frontends.gamespy.library.extentions.gamespy_utils import convert_to_key_value 5 | 6 | 7 | class RequestBase(lib.RequestBase): 8 | raw_request: str 9 | _request_dict: dict[str, str] 10 | 11 | def __init__(self, raw_request: str) -> None: 12 | assert isinstance(raw_request, str) 13 | super().__init__(raw_request) 14 | self._request_dict = {} 15 | 16 | def parse(self) -> None: 17 | self._request_dict = convert_to_key_value(self.raw_request) 18 | if 'final' in self._request_dict: 19 | del self._request_dict['final'] 20 | self.command_name = list(self._request_dict.keys())[0] 21 | 22 | 23 | class ResultBase(lib.ResultBase): 24 | pass 25 | 26 | 27 | class ResponseBase(lib.ResponseBase): 28 | sending_buffer: str 29 | 30 | def __init__(self, result: ResultBase) -> None: 31 | assert issubclass(type(result), ResultBase) 32 | super().__init__(result) 33 | -------------------------------------------------------------------------------- /src/frontends/tests/gamespy/game_traffic_relay/mock_objects.py: -------------------------------------------------------------------------------- 1 | from typing import cast 2 | from frontends.gamespy.library.configs import CONFIG 3 | from frontends.gamespy.protocols.game_traffic_relay.applications.client import Client 4 | from frontends.gamespy.protocols.game_traffic_relay.applications.handlers import ( 5 | PingHandler, 6 | ) 7 | from frontends.tests.gamespy.library.mock_objects import ( 8 | ConnectionMock, 9 | LogMock, 10 | RequestHandlerMock, 11 | create_mock_url, 12 | ) 13 | 14 | 15 | class ClientMock(Client): 16 | pass 17 | 18 | 19 | def create_client(client_address: tuple = ("192.168.0.1", 0)) -> Client: 20 | CONFIG.unittest.is_raise_except = True 21 | handler = RequestHandlerMock(client_address) 22 | logger = LogMock() 23 | conn = ConnectionMock( 24 | handler=handler, 25 | config=CONFIG.servers["GameTrafficRelay"], 26 | t_client=ClientMock, 27 | logger=logger, 28 | ) 29 | 30 | config = CONFIG.servers["GameTrafficRelay"] 31 | create_mock_url(config, PingHandler, {"message": "ok"}) 32 | return cast(Client, conn._client) 33 | -------------------------------------------------------------------------------- /src/backends/library/abstractions/contracts.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | from pydantic import BaseModel, UUID4 3 | 4 | 5 | class RequestBase(BaseModel): 6 | """ 7 | The ultimate request base class of all gamespy requests 8 | """ 9 | 10 | server_id: UUID4 11 | raw_request: str 12 | """ 13 | if the raw_request is bytes, we decode it to decode("ascii","backslashreplace") str 14 | """ 15 | client_ip: str 16 | client_port: int 17 | 18 | 19 | class Response(BaseModel): 20 | message: str 21 | 22 | def to_json_dict(self) -> dict[str, object]: 23 | return self.model_dump(mode="json") 24 | 25 | 26 | class OKResponse(Response): 27 | message: str = "ok" 28 | 29 | 30 | class DataResponse(OKResponse): 31 | result: BaseModel 32 | pass 33 | 34 | 35 | class ErrorResponse(Response): 36 | exception_name: str 37 | pass 38 | 39 | 40 | RESPONSES_DEF: Dict = { 41 | 400: {"model": ErrorResponse}, 42 | 422: {"model": ErrorResponse}, 43 | 500: {"model": ErrorResponse}, 44 | 450: {"model": ErrorResponse} 45 | } 46 | """Dict type is using to compatible with language checking""" 47 | -------------------------------------------------------------------------------- /src/backends/protocols/gamespy/query_report/broker.py: -------------------------------------------------------------------------------- 1 | from contextlib import asynccontextmanager 2 | import logging 3 | 4 | from fastapi import APIRouter 5 | from backends.protocols.gamespy.query_report.handlers import ClientMessageHandler 6 | from backends.protocols.gamespy.query_report.requests import ClientMessageRequest 7 | from backends.library.networks.redis_brocker import RedisBrocker 8 | from frontends.gamespy.library.configs import CONFIG 9 | 10 | from backends.library.networks.ws_manager import WebsocketManager as WsManager 11 | 12 | logger = logging.getLogger("backend") 13 | 14 | 15 | class WebsocketManager(WsManager): 16 | pass 17 | 18 | 19 | def handle_client_message(message: str): 20 | try: 21 | request = ClientMessageRequest.model_validate(message) 22 | handler = ClientMessageHandler(request) 23 | handler.handle() 24 | except Exception as e: 25 | logger.error(str(e)) 26 | 27 | 28 | MANAGER = WebsocketManager() 29 | BROCKER = RedisBrocker("master", CONFIG.redis.url, handle_client_message) 30 | 31 | 32 | @asynccontextmanager 33 | async def launch_brocker(_: APIRouter): 34 | BROCKER.subscribe() 35 | yield 36 | -------------------------------------------------------------------------------- /src/backends/tests/gamespy/game_status/handler_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from backends.protocols.gamespy.game_status.handlers import AuthGameHandler 4 | from backends.protocols.gamespy.game_status.requests import AuthGameRequest 5 | 6 | 7 | class HandlerTests(unittest.TestCase): 8 | def test_auth_game(self): 9 | raw = { 10 | "raw_request": "\\auth\\\\gamename\\gmtest\\response\\b7f8b7f83dcc4427c35864c5d53c5fe5\\port\\2667\\id\\1\\final\\", 11 | "request_dict": { 12 | "auth": "", 13 | "gamename": "gmtest", 14 | "response": "b7f8b7f83dcc4427c35864c5d53c5fe5", 15 | "port": "2667", 16 | "id": "1", 17 | }, 18 | "local_id": 1, 19 | "game_name": "gmtest", 20 | "response": "b7f8b7f83dcc4427c35864c5d53c5fe5", 21 | "port": 2667, 22 | "client_ip": "172.19.0.5", 23 | "server_id": "950b7638-a90d-469b-ac1f-861e63c8c613", 24 | "client_port": 38996, 25 | } 26 | req = AuthGameRequest(**raw) 27 | h = AuthGameHandler(req) 28 | h.handle() 29 | pass 30 | -------------------------------------------------------------------------------- /src/backends/protocols/gamespy/server_browser/requests.py: -------------------------------------------------------------------------------- 1 | 2 | import backends.library.abstractions.contracts as lib 3 | from frontends.gamespy.protocols.server_browser.v2.aggregations.enums import ( 4 | ServerListUpdateOption, 5 | ) 6 | 7 | 8 | class RequestBase(lib.RequestBase): 9 | raw_request: str 10 | 11 | 12 | class ServerListUpdateOptionRequestBase(RequestBase): 13 | request_version: int 14 | protocol_version: int 15 | encoding_version: int 16 | game_version: int 17 | dev_game_name: str 18 | game_name: str 19 | client_challenge: str 20 | update_option: ServerListUpdateOption 21 | keys: list[str] 22 | filter: str | None = None 23 | max_servers: int | None = None 24 | source_ip: str | None = None 25 | query_options: int | None = None 26 | 27 | 28 | class ServerListRequest(ServerListUpdateOptionRequestBase): 29 | pass 30 | 31 | 32 | class AdHocRequestBase(RequestBase): 33 | game_server_public_ip: str 34 | game_server_public_port: int 35 | 36 | 37 | class SendMessageRequest(AdHocRequestBase): 38 | prefix_message: str 39 | client_message: str 40 | 41 | 42 | class ServerInfoRequest(AdHocRequestBase): 43 | pass 44 | -------------------------------------------------------------------------------- /src/backends/routers/gamespy/game_traffic_relay.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Request 2 | 3 | from backends.library.abstractions.contracts import RESPONSES_DEF, Response 4 | from backends.library.utils.misc import check_public_ip 5 | from backends.protocols.gamespy.game_traffic_relay.handlers import ( 6 | GtrHeartBeatHandler, 7 | ) 8 | from backends.protocols.gamespy.game_traffic_relay.requests import ( 9 | GtrHeartBeatRequest, 10 | ) 11 | from backends.urls import GAME_TRAFFIC_RELAY 12 | from frontends.gamespy.library.exceptions.general import UniSpyException 13 | 14 | router = APIRouter() 15 | 16 | 17 | @router.post(f"{GAME_TRAFFIC_RELAY}/Heartbeat", responses=RESPONSES_DEF) 18 | def heartbeat(request: Request, heartbeat: GtrHeartBeatRequest) -> Response: 19 | assert request.client is not None 20 | check_public_ip(request.client.host, heartbeat.public_ip_address) 21 | 22 | handler = GtrHeartBeatHandler(heartbeat) 23 | handler.handle() 24 | return handler.response 25 | 26 | 27 | if __name__ == "__main__": 28 | import uvicorn 29 | from fastapi import FastAPI 30 | 31 | app = FastAPI() 32 | app.include_router(router) 33 | uvicorn.run(app, host="0.0.0.0", port=8080) 34 | -------------------------------------------------------------------------------- /src/backends/tests/gamespy/query_report/data_fetch_tests.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timezone 2 | import unittest 3 | 4 | from pydantic import ValidationError 5 | from backends.library.database.pg_orm import ENGINE, ChatChannelCaches 6 | import backends.protocols.gamespy.query_report.data as data 7 | from sqlalchemy.orm import Session 8 | 9 | 10 | class DataFetchTests(unittest.TestCase): 11 | def test_get_peer_staging_channels(self): 12 | cache = ChatChannelCaches( 13 | channel_name="#GSP!unispy_test_game_name!*", 14 | server_id="b6480a17-5e3d-4da0-aeec-c421620bff71", 15 | game_name="unispy_test_game_name", 16 | room_name="unispy_test_room_name", 17 | group_id=0, 18 | max_num_user=100, 19 | update_time=datetime.now(timezone.utc), 20 | ) 21 | with Session(ENGINE) as session: 22 | session.add(cache) 23 | session.commit() 24 | self.assertRaises( 25 | ValidationError, 26 | data.get_peer_staging_channels, 27 | "unispy_test_game_name", 28 | 0, 29 | session, 30 | ) 31 | session.delete(cache) 32 | session.commit() 33 | -------------------------------------------------------------------------------- /src/backends/services/register.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from datetime import datetime, timedelta 4 | from backends.library.database.pg_orm import ENGINE, FrontendInfo 5 | from frontends.gamespy.library.configs import ServerConfig 6 | from sqlalchemy.orm import Session 7 | 8 | from frontends.gamespy.library.exceptions.general import UniSpyException 9 | 10 | 11 | def register_services(config: ServerConfig, external_ip: str) -> dict: 12 | expire_time = datetime.now() - timedelta(minutes=5) 13 | with Session(ENGINE) as session: 14 | result = session.query( 15 | FrontendInfo.server_id == config.server_id, 16 | FrontendInfo.update_time >= expire_time).first() 17 | if result is not None: 18 | info = FrontendInfo( 19 | server_id=config.server_id, 20 | server_name=config.server_name, 21 | external_ip="", 22 | listening_ip=config.listening_address, 23 | listening_port=config.listening_port, 24 | update_time=datetime.now() 25 | ) 26 | 27 | session.add(info) 28 | session.commit() 29 | return {"status": "online"} 30 | else: 31 | raise UniSpyException("server is already registered") 32 | -------------------------------------------------------------------------------- /src/frontends/gamespy/library/abstractions/brocker.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from threading import Thread 3 | from typing import final, Callable 4 | 5 | 6 | class BrockerBase: 7 | _subscriber: object 8 | is_started: bool = False 9 | _name: str 10 | _call_back_func: Callable | None 11 | """ 12 | brocker subscribe name 13 | """ 14 | 15 | def __init__(self, name: str, url: str, call_back_func: Callable | None) -> None: 16 | assert isinstance(name, str) 17 | 18 | self._name = name 19 | self.url = url 20 | if call_back_func is not None: 21 | assert callable(call_back_func) 22 | self._call_back_func = call_back_func 23 | 24 | @abc.abstractmethod 25 | def subscribe(self): 26 | """ 27 | define the brocker event binding 28 | """ 29 | pass 30 | 31 | @final 32 | def receive_message(self, message: str): 33 | assert isinstance(message, str) 34 | if self._call_back_func is None: 35 | return 36 | self._call_back_func(message) 37 | 38 | @abc.abstractmethod 39 | def publish_message(self, message: str): 40 | assert isinstance(message, str) 41 | 42 | @abc.abstractmethod 43 | def unsubscribe(self): 44 | pass 45 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/web_services/abstractions/contracts.py: -------------------------------------------------------------------------------- 1 | import frontends.gamespy.library.abstractions.contracts as lib 2 | import xml.etree.ElementTree as ET 3 | 4 | from frontends.gamespy.protocols.web_services.aggregations.exceptions import WebException 5 | from frontends.gamespy.protocols.web_services.aggregations.soap_envelop import SoapEnvelop 6 | 7 | 8 | class RequestBase(lib.RequestBase): 9 | raw_request: str 10 | _content_element: ET.Element 11 | 12 | def __init__(self, raw_request: str) -> None: 13 | assert isinstance(raw_request, str) 14 | super().__init__(raw_request) 15 | 16 | def parse(self) -> None: 17 | xelements = ET.fromstring(self.raw_request) 18 | self._content_element = xelements[0][0] 19 | 20 | 21 | class ResultBase(lib.ResultBase): 22 | pass 23 | 24 | 25 | class ResponseBase(lib.ResponseBase): 26 | _content: SoapEnvelop 27 | """ 28 | Soap envelope content, should be initialized in response sub class 29 | """ 30 | sending_buffer: str 31 | 32 | def __init__(self, result: ResultBase) -> None: 33 | assert issubclass(type(result), ResultBase) 34 | super().__init__(result) 35 | 36 | def build(self) -> None: 37 | self.sending_buffer = str(self._content) 38 | -------------------------------------------------------------------------------- /src/frontends/tests/gamespy/natneg/redis.py: -------------------------------------------------------------------------------- 1 | # import datetime 2 | # 3 | 4 | # from pydantic.v1 import EmailStr 5 | 6 | # from redis_om import Field, HashModel, Migrator 7 | 8 | 9 | # class Customer(HashModel): 10 | # first_name: str 11 | # last_name: str = Field(index=True) 12 | # email: EmailStr 13 | # join_date: datetime.date 14 | # age: int = Field(index=True) 15 | # bio: str | None 16 | 17 | 18 | # # Now, if we use this model with a Redis deployment that has the 19 | # # RediSearch module installed, we can run queries like the following. 20 | 21 | # # Before running queries, we need to run migrations to set up the 22 | # # indexes that Redis OM will use. You can also use the `migrate` 23 | # # CLI tool for this! 24 | # Migrator().run() 25 | 26 | # # Find all customers with the last name "Brookins" 27 | # Customer.find(Customer.last_name == "Brookins").all() 28 | 29 | # # Find all customers that do NOT have the last name "Brookins" 30 | # Customer.find(Customer.last_name != "Brookins").all() 31 | 32 | # # Find all customers whose last name is "Brookins" OR whose age is 33 | # # 100 AND whose last name is "Smith" 34 | # Customer.find( 35 | # (Customer.last_name == "Brookins") 36 | # | (Customer.age == 100) & (Customer.last_name == "Smith") 37 | # ).all() 38 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/natneg/contracts/results.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.protocols.natneg.abstractions.contracts import CommonResultBase, ResultBase 2 | from frontends.gamespy.protocols.natneg.aggregations.enums import ( 3 | ConnectPacketStatus, 4 | PreInitState, 5 | ResponseType, 6 | ) 7 | 8 | 9 | class AddressCheckResult(CommonResultBase): 10 | packet_type: ResponseType = ResponseType.ADDRESS_REPLY 11 | 12 | 13 | class ConnectResult(ResultBase): 14 | is_both_client_ready: bool 15 | status: ConnectPacketStatus | None 16 | ip: str | None 17 | port: int | None 18 | packet_type: ResponseType = ResponseType.CONNECT 19 | 20 | 21 | class InitResult(CommonResultBase): 22 | packet_type: ResponseType = ResponseType.INIT_ACK 23 | 24 | 25 | class ErtAckResult(InitResult): 26 | packet_type: ResponseType = ResponseType.ERT_ACK 27 | 28 | 29 | class NatifyResult(CommonResultBase): 30 | packet_type: ResponseType = ResponseType.ERT_TEST 31 | 32 | 33 | class PreInitResult(ResultBase): 34 | client_index: int 35 | state: PreInitState 36 | client_id: int 37 | packet_type: ResponseType = ResponseType.PRE_INIT_ACK 38 | state = PreInitState.READY 39 | 40 | 41 | class ReportResult(ResultBase): 42 | packet_type: ResponseType = ResponseType.REPORT_ACK 43 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/server_browser/v2/contracts/results.py: -------------------------------------------------------------------------------- 1 | from uuid import UUID 2 | 3 | from frontends.gamespy.protocols.query_report.aggregates.game_server_info import ( 4 | GameServerInfo, 5 | ) 6 | from frontends.gamespy.protocols.query_report.aggregates.peer_room_info import ( 7 | PeerRoomInfo, 8 | ) 9 | from frontends.gamespy.protocols.server_browser.v2.abstractions.contracts import ( 10 | AdHocResultBase, 11 | ResultBase, 12 | ServerListUpdateOptionResultBase, 13 | ) 14 | from frontends.gamespy.protocols.server_browser.v2.aggregations.enums import GameServerFlags 15 | 16 | 17 | class UpdateServerInfoResult(AdHocResultBase): 18 | pass 19 | 20 | 21 | class P2PGroupRoomListResult(ServerListUpdateOptionResultBase): 22 | peer_room_info: list[PeerRoomInfo] 23 | 24 | 25 | class ServerMainListResult(ServerListUpdateOptionResultBase): 26 | servers_info: list[GameServerInfo] 27 | flag: GameServerFlags = GameServerFlags.HAS_KEYS_FLAG 28 | 29 | 30 | class ServerFullInfoListResult(ServerListUpdateOptionResultBase): 31 | servers_info: list[GameServerInfo] 32 | flag: GameServerFlags = GameServerFlags.HAS_FULL_RULES_FLAG 33 | 34 | 35 | class SendMessageResult(ResultBase): 36 | sb_sender_id: UUID 37 | natneg_message: str 38 | server_info: GameServerInfo 39 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/presence_connection_manager/aggregates/login_challenge.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | from frontends.gamespy.protocols.presence_connection_manager.aggregates.enums import GPPartnerId, LoginType 4 | 5 | SERVER_CHALLENGE = "0000000000" 6 | 7 | 8 | class LoginChallengeProof: 9 | 10 | def __init__( 11 | self, userData: str, loginType: LoginType, partnerID: int, challenge1: str, challenge2: str, passwordHash: str 12 | ): 13 | self.userData = userData 14 | self.loginType = loginType 15 | self.partnerID = partnerID 16 | self.challenge1 = challenge1 17 | self.challenge2 = challenge2 18 | self.passwordHash = passwordHash 19 | 20 | def generate_proof(self): 21 | tempUserData = self.userData 22 | 23 | if self.partnerID is not None: 24 | if ( 25 | self.partnerID != GPPartnerId.GAMESPY.value 26 | and self.loginType != LoginType.AUTH_TOKEN 27 | ): 28 | tempUserData = f"{self.partnerID}@{self.userData}" 29 | 30 | responseString = f"{self.passwordHash} { 31 | ' ' * 48}{tempUserData}{self.challenge1}{self.challenge2}{self.passwordHash}" 32 | hashString = hashlib.md5(responseString.encode()).hexdigest() 33 | return hashString 34 | -------------------------------------------------------------------------------- /src/frontends/tests/gamespy/query_report/mock_objects.py: -------------------------------------------------------------------------------- 1 | from typing import cast 2 | from frontends.gamespy.library.configs import CONFIG 3 | from frontends.gamespy.protocols.query_report.applications.client import Client 4 | from frontends.gamespy.protocols.query_report.v2.applications.handlers import AvailableHandler, HeartbeatHandler, KeepAliveHandler 5 | from frontends.gamespy.protocols.query_report.v2.contracts.results import HeartbeatResult 6 | from frontends.tests.gamespy.library.mock_objects import ConnectionMock, LogMock, RequestHandlerMock, create_mock_url 7 | 8 | 9 | class ClientMock(Client): 10 | pass 11 | 12 | 13 | def create_client() -> Client: 14 | CONFIG.unittest.is_raise_except = True 15 | handler = RequestHandlerMock() 16 | logger = LogMock() 17 | conn = ConnectionMock( 18 | handler=handler, 19 | config=CONFIG.servers["QueryReport"], t_client=ClientMock, 20 | logger=logger) 21 | config = CONFIG.servers["QueryReport"] 22 | create_mock_url(config, HeartbeatHandler, HeartbeatResult.model_validate( 23 | {"remote_ip": conn.remote_ip, "remote_port": conn.remote_port, "instant_key": "123", "command_name": 3}).model_dump(mode='json')) 24 | create_mock_url(config, AvailableHandler, {"message": "ok"}) 25 | create_mock_url(config, KeepAliveHandler, {"message": "ok"}) 26 | return cast(Client, conn._client) 27 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/presence_connection_manager/abstractions/handlers.py: -------------------------------------------------------------------------------- 1 | 2 | from frontends.gamespy.protocols.presence_connection_manager.applications.client import Client 3 | from frontends.gamespy.protocols.presence_connection_manager.aggregates.enums import LoginStatus 4 | from frontends.gamespy.protocols.presence_search_player.aggregates.exceptions import GPException 5 | 6 | from frontends.gamespy.protocols.presence_connection_manager.abstractions.contracts import ( 7 | RequestBase, 8 | ResultBase, 9 | ) 10 | import frontends.gamespy.library.abstractions.handler as lib 11 | 12 | 13 | class CmdHandlerBase(lib.CmdHandlerBase): 14 | _client: Client 15 | _request: RequestBase 16 | _result: ResultBase 17 | 18 | def __init__(self, client: Client, request: RequestBase) -> None: 19 | assert isinstance(client, Client) 20 | assert issubclass(type(request), RequestBase) 21 | super().__init__(client, request) 22 | 23 | def _handle_exception(self, ex) -> None: 24 | if ex is GPException: 25 | self._client.send(ex) 26 | super()._handle_exception(ex) 27 | 28 | 29 | class LoginedHandlerBase(CmdHandlerBase): 30 | 31 | def _request_check(self) -> None: 32 | if self._client.info.login_status != LoginStatus.COMPLETED: 33 | raise GPException("please login first.") 34 | super()._request_check() 35 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/query_report/v2/aggregates/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class RequestType(Enum): 5 | CHALLENGE = 0x01 6 | HEARTBEAT = 0x03 7 | CLIENT_MESSAGE = 0x06 8 | CLIENT_MESSAGE_ACK = 0x07 9 | ADD_ERROR = 0x04 10 | ECHO = 0x02 11 | KEEP_ALIVE = 0x08 12 | AVALIABLE_CHECK = 0x09 13 | 14 | 15 | class ResponseType(Enum): 16 | QUERY = 0x00 17 | ECHO = 0x02 18 | ADD_ERROR = 0x04 19 | CLIENT_MESSAGE = 0x06 20 | REQUIRE_IP_VERIFY = 0x09 21 | CLIENT_REGISTERED = 0x0A 22 | 23 | 24 | class PacketType(Enum): 25 | QUERY = 0x00 26 | CHALLENGE = 0x01 27 | ECHO = 0x02 28 | ADD_ERROR = 0x04 29 | CLIENT_MESSAGE = 0x06 30 | REQUIRE_IP_VERIFY = 0x09 31 | CLIENT_REGISTERED = 0x0A 32 | HEARTBEAT = 0x03 33 | ECHO_RESPONSE = 0x05 34 | CLIENT_MESSAGE_ACK = 0x07 35 | KEEP_ALIVE = 0x08 36 | AVALIABLE_CHECK = 0x09 37 | 38 | 39 | class QRStateChange(Enum): 40 | NORMAL_HEARTBEAT = 0 41 | GAME_MODE_CHANGE = 1 42 | SERVER_SHUTDOWN = 2 43 | CANNOT_RECIEVE_CHALLENGE = 3 44 | 45 | 46 | class HeartBeatReportType(Enum): 47 | SERVER_TEAM_PLAYER_DATA = 1 48 | SERVER_PLAYER_DATA = 2 49 | SERVER_DATA = 3 50 | 51 | 52 | 53 | 54 | class ServerAvailability(Enum): 55 | AVAILABLE = 0 56 | WAITING = 1 57 | PERMANENT_UNAVAILABLE = 2 58 | TEMPORARILY_UNAVAILABLE = 3 59 | -------------------------------------------------------------------------------- /src/frontends/tests/gamespy/game_traffic_relay/handler_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from frontends.gamespy.protocols.game_traffic_relay.applications.client import ConnectionListener 4 | from frontends.gamespy.protocols.game_traffic_relay.applications.handlers import ( 5 | PingHandler, 6 | ) 7 | from frontends.gamespy.protocols.natneg.contracts.requests import PingRequest 8 | from frontends.tests.gamespy.game_traffic_relay.mock_objects import create_client 9 | 10 | 11 | class HandlerTests(unittest.TestCase): 12 | def test_ping(self): 13 | """ 14 | test whether 2 clients can be binding togather with ping command 15 | """ 16 | ping_raw = ( 17 | b"\xfd\xfc\x1efj\xb2\x03\x07\x00\x00\x02\x9a\xc0\xa8\x01gl\xfd\x00\x00" 18 | ) 19 | client1 = create_client(("127.0.0.1", 1234)) 20 | client1._log_prefix = "[127.0.0.1:1234]" 21 | client2 = create_client(("127.0.0.1", 1235)) 22 | client2._log_prefix = "[127.0.0.1:1235]" 23 | client1.on_received(ping_raw) 24 | client2.on_received(ping_raw) 25 | # cookie length check 26 | self.assertEqual(len(ConnectionListener.cookie_pool), 1) 27 | clients = list(ConnectionListener.cookie_pool.values())[0] 28 | self.assertEqual(len(clients), 2) 29 | client1 = create_client(("127.0.0.1", 1234)) 30 | client1.on_received(ping_raw) 31 | pass 32 | -------------------------------------------------------------------------------- /src/backends/protocols/gamespy/game_traffic_relay/handlers.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | import logging 3 | import socket 4 | from backends.library.abstractions.contracts import OKResponse 5 | from backends.library.abstractions.handler_base import HandlerBase 6 | from backends.library.database.pg_orm import RelayServerCaches 7 | from backends.protocols.gamespy.game_traffic_relay.requests import ( 8 | GtrHeartBeatRequest, 9 | ) 10 | 11 | import backends.protocols.gamespy.game_traffic_relay.data as data 12 | from frontends.gamespy.library.exceptions.general import UniSpyException 13 | 14 | 15 | class GtrHeartBeatHandler(HandlerBase): 16 | _request: GtrHeartBeatRequest 17 | response: OKResponse 18 | 19 | def _data_operate(self) -> None: 20 | data.check_expired_server(self._session) 21 | info = data.search_relay_server( 22 | self._request.server_id, self._request.public_ip_address, self._session 23 | ) 24 | if info is None: 25 | info = RelayServerCaches( 26 | server_id=self._request.server_id, 27 | public_ip=self._request.public_ip_address, 28 | public_port=self._request.public_port, 29 | client_count=self._request.client_count, 30 | ) 31 | data.create_relay_server(info, self._session) 32 | else: 33 | # refresh update time 34 | data.update_relay_server(info, self._session) 35 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/game_traffic_relay/applications/client.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from frontends.gamespy.library.abstractions.client import ClientBase 3 | from frontends.gamespy.library.abstractions.switcher import SwitcherBase 4 | from frontends.gamespy.protocols.game_traffic_relay.applications.connection import ConnectStatus, ConnectionListener 5 | from frontends.gamespy.protocols.natneg.abstractions.contracts import MAGIC_DATA 6 | import frontends.gamespy.protocols.natneg.applications.client as natneg 7 | 8 | 9 | class ClientInfo: 10 | cookie: int | None 11 | last_receive_time: datetime 12 | status: ConnectStatus 13 | ping_recv_times: int 14 | 15 | def __init__(self) -> None: 16 | self.cookie = None 17 | self.last_receive_time = datetime.now() 18 | self.status = ConnectStatus.WAITING_FOR_ANOTHER 19 | self.ping_recv_times = 0 20 | 21 | 22 | class Client(ClientBase): 23 | info: ClientInfo 24 | 25 | def __init__( 26 | self, 27 | connection: natneg.UdpConnection, 28 | server_config: natneg.ServerConfig, 29 | logger: natneg.LogWriter, 30 | ): 31 | super().__init__(connection, server_config, logger) 32 | self.info = ClientInfo() 33 | 34 | def _create_switcher(self, buffer: bytes) -> SwitcherBase: 35 | from frontends.gamespy.protocols.game_traffic_relay.applications.switcher import Switcher 36 | return Switcher(self, buffer) -------------------------------------------------------------------------------- /src/frontends/tests/gamespy/presence_search_player/game_tests.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, cast 2 | import unittest 3 | 4 | from frontends.gamespy.protocols.presence_search_player.contracts.requests import CheckRequest 5 | from frontends.gamespy.protocols.presence_search_player.applications.switcher import Switcher 6 | import responses 7 | 8 | from frontends.gamespy.protocols.presence_search_player.contracts.responses import CheckResponse 9 | from frontends.tests.gamespy.presence_search_player.mock_objects import create_client 10 | 11 | 12 | class GameTest(unittest.TestCase): 13 | @responses.activate 14 | def test_check(self): 15 | raw = "\\check\\\\nick\\spyguy\\email\\spyguy@gamespy.com\\pass\\0000\\final\\" 16 | client = create_client() 17 | 18 | switcher = Switcher(client, raw) 19 | switcher.handle() 20 | request = switcher._handlers[0]._request 21 | if TYPE_CHECKING: 22 | request = cast(CheckRequest, request) 23 | response = switcher._handlers[0]._response 24 | if TYPE_CHECKING: 25 | response = cast(CheckResponse, response) 26 | self.assertEqual("spyguy", request.nick) 27 | self.assertEqual("spyguy@gamespy.com", request.email) 28 | self.assertEqual("4a7d1ed414474e4033ac29ccb8653d9b", request.password) 29 | self.assertEqual("\\cur\\0\\pid\\0\\final\\", response.sending_buffer) 30 | 31 | 32 | if __name__ == "__main__": 33 | unittest.main() 34 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/game_status/abstractions/contracts.py: -------------------------------------------------------------------------------- 1 | 2 | import frontends.gamespy.library.abstractions.contracts as lib 3 | from frontends.gamespy.library.extentions.gamespy_utils import convert_to_key_value 4 | from frontends.gamespy.protocols.game_status.aggregations.exceptions import GSException 5 | 6 | 7 | class RequestBase(lib.RequestBase): 8 | command_name: str 9 | raw_request: str 10 | local_id: int | None 11 | _request_dict: dict[str, str] 12 | 13 | @staticmethod 14 | def convert_game_data_to_key_values(game_data: str): 15 | assert isinstance(game_data, str) 16 | game_data = game_data.replace("\u0001", "\\") 17 | convert_to_key_value(game_data) 18 | 19 | def parse(self) -> None: 20 | self._request_dict = convert_to_key_value(self.raw_request) 21 | 22 | if "lid" in self._request_dict: 23 | try: 24 | self.local_id = int(self._request_dict["lid"]) 25 | except: 26 | raise GSException("local id is not valid.") 27 | 28 | if "id" in self._request_dict: 29 | try: 30 | self.local_id = int(self._request_dict["id"]) 31 | except: 32 | raise GSException("local id is not valid.") 33 | 34 | 35 | class ResultBase(lib.ResultBase): 36 | local_id: int 37 | pass 38 | 39 | 40 | class ResponseBase(lib.ResponseBase): 41 | _request: RequestBase 42 | _result: ResultBase 43 | sending_buffer: str 44 | -------------------------------------------------------------------------------- /src/frontends/app.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from frontends.gamespy.library.extentions.debug_helper import DebugHelper 4 | 5 | 6 | if __name__ == "__main__": 7 | from frontends.gamespy.protocols.chat.applications.server_launcher import Service as chat 8 | from frontends.gamespy.protocols.game_status.applications.server_launcher import Service as gs 9 | from frontends.gamespy.protocols.game_traffic_relay.applications.server_launcher import Service as gtr 10 | from frontends.gamespy.protocols.natneg.applications.server_launcher import Service as nn 11 | from frontends.gamespy.protocols.presence_connection_manager.applications.server_launcher import Service as pcm 12 | from frontends.gamespy.protocols.presence_search_player.applications.server_launcher import Service as psp 13 | from frontends.gamespy.protocols.query_report.applications.server_launcher import Service as qr 14 | from frontends.gamespy.protocols.server_browser.applications.server_launcher import Service as sb 15 | from frontends.gamespy.protocols.web_services.applications.server_launcher import Service as web 16 | 17 | from frontends.gamespy.library.abstractions.server_launcher import ServicesFactory 18 | launchers = [ 19 | chat(), 20 | gs(), 21 | gtr(), 22 | nn(), 23 | pcm(), 24 | psp(), 25 | qr(), 26 | sb(), 27 | web() 28 | ] 29 | 30 | factory = ServicesFactory(launchers) 31 | helper = DebugHelper("./frontends", factory) 32 | helper.start() 33 | -------------------------------------------------------------------------------- /src/.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/python 3 | { 4 | "name": "Python 3", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "runArgs": [ 7 | "--network=unispy", 8 | "--name=unispy_server_dev", 9 | "--add-host=unispy_backends:127.0.0.1" 10 | ], 11 | "image": "mcr.microsoft.com/devcontainers/python:1-3.12-bullseye", 12 | // Features to add to the dev container. More info: https://containers.dev/features. 13 | // "features": {}, 14 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 15 | "forwardPorts": [ 16 | 29910, 17 | 6667, 18 | 29920, 19 | 10086, 20 | 27901, 21 | 29900, 22 | 29901, 23 | 27900, 24 | 28910, 25 | 28900, 26 | 80 27 | ], 28 | // Use 'postCreateCommand' to run commands after the container is created. 29 | "postCreateCommand": "pip3 install --user -r requirements.txt", 30 | "customizations": { 31 | "vscode": { 32 | "extensions": [ 33 | "ms-python.vscode-pylance@2024.8.2", 34 | "ms-python.python@2024.14.1", 35 | "ms-python.debugpy@2024.8.0", 36 | "ms-python.autopep8@2025.2.0" 37 | ] 38 | } 39 | } 40 | // Configure tool-specific properties. 41 | // "customizations": {}, 42 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 43 | // "remoteUser": "root" 44 | } -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/query_report/v2/abstractions/contracts.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.protocols.query_report.v2.aggregates.enums import PacketType 2 | from frontends.gamespy.protocols.query_report.aggregates.exceptions import QRException 3 | from frontends.gamespy.protocols.query_report.v2.aggregates.enums import RequestType 4 | import frontends.gamespy.library.abstractions.contracts as lib 5 | 6 | MAGIC_DATA = [0xFE, 0xFD] 7 | 8 | 9 | class RequestBase(lib.RequestBase): 10 | instant_key: str 11 | command_name: RequestType 12 | raw_request: bytes 13 | 14 | def __init__(self, raw_request: bytes) -> None: 15 | assert isinstance(raw_request, bytes) 16 | super().__init__(raw_request) 17 | 18 | def parse(self): 19 | if len(self.raw_request) < 3: 20 | raise QRException("request length not valid") 21 | self.command_name = RequestType(self.raw_request[0]) 22 | self.instant_key = str(int.from_bytes(self.raw_request[1:5])) 23 | 24 | 25 | class ResultBase(lib.ResultBase): 26 | packet_type: PacketType 27 | command_name: RequestType 28 | instant_key: str 29 | 30 | class ResponseBase(lib.ResponseBase): 31 | _result: ResultBase 32 | sending_buffer: bytes 33 | 34 | def build(self) -> None: 35 | data = bytearray() 36 | data.extend(MAGIC_DATA) 37 | data.append(self._result.command_name.value) 38 | data.extend(int(self._result.instant_key).to_bytes(4)) 39 | self.sending_buffer = bytes(data) 40 | -------------------------------------------------------------------------------- /src/frontends/tests/gamespy/presence_search_player/mock_objects.py: -------------------------------------------------------------------------------- 1 | from typing import cast 2 | from frontends.gamespy.library.configs import CONFIG 3 | from frontends.tests.gamespy.library.mock_objects import ConnectionMock, LogMock, RequestHandlerMock, create_mock_url 4 | from frontends.gamespy.protocols.presence_search_player.applications.client import Client 5 | from frontends.gamespy.protocols.presence_search_player.applications.handlers import CheckHandler, SearchHandler 6 | from frontends.gamespy.protocols.presence_search_player.contracts.results import CheckResult, SearchResult 7 | 8 | 9 | class ClientMock(Client): 10 | pass 11 | 12 | 13 | def create_client() -> Client: 14 | CONFIG.unittest.is_raise_except = True 15 | handler = RequestHandlerMock() 16 | logger = LogMock() 17 | conn = ConnectionMock( 18 | handler=handler, 19 | config=CONFIG.servers["PresenceSearchPlayer"], t_client=ClientMock, 20 | logger=logger) 21 | config = CONFIG.servers["PresenceSearchPlayer"] 22 | create_mock_url(config, CheckHandler, CheckResult.model_validate( 23 | {"profile_id": 0}).model_dump()) 24 | 25 | create_mock_url(config, SearchHandler, SearchResult.model_validate({"data": [{"profile_id": 0, "nick": "spyguy", "uniquenick": "spyguy", 26 | "email": "spyguy@gamespy.com", "firstname": "spy", "lastname": "guy", "namespace_id": 0}]}).model_dump()) 27 | 28 | return cast(Client, conn._client) 29 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/server_browser/v2/applications/client.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | from frontends.gamespy.library.abstractions.client import ClientBase, ClientInfoBase 3 | from frontends.gamespy.library.abstractions.connections import ConnectionBase 4 | from frontends.gamespy.library.abstractions.enctypt_base import EncryptBase 5 | from frontends.gamespy.library.configs import ServerConfig 6 | from frontends.gamespy.library.log.log_manager import LogWriter 7 | from frontends.gamespy.protocols.server_browser.v2.aggregations.encryption import EnctypeX 8 | from frontends.gamespy.protocols.server_browser.v2.aggregations.enums import ServerListUpdateOption 9 | if TYPE_CHECKING: 10 | from frontends.gamespy.library.abstractions.switcher import SwitcherBase 11 | 12 | 13 | class ClientInfo(ClientInfoBase): 14 | game_secret_key: str 15 | client_challenge: str 16 | search_type: ServerListUpdateOption 17 | game_name: str 18 | 19 | 20 | class Client(ClientBase): 21 | is_log_raw: bool 22 | info: ClientInfo 23 | crypto: EnctypeX | None 24 | 25 | def __init__(self, connection: ConnectionBase, server_config: ServerConfig, logger: LogWriter): 26 | super().__init__(connection, server_config, logger) 27 | self.is_log_raw = True 28 | self.info = ClientInfo() 29 | 30 | def _create_switcher(self, buffer: bytes) -> "SwitcherBase": 31 | from frontends.gamespy.protocols.server_browser.v2.applications.switcher import Switcher 32 | return Switcher(self, buffer) 33 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/web_services/modules/direct2game/contracts/responses.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.protocols.web_services.abstractions.contracts import ResultBase 2 | from frontends.gamespy.protocols.web_services.aggregations.soap_envelop import SoapEnvelop 3 | from frontends.gamespy.protocols.web_services.modules.direct2game.abstractions.contracts import ResponseBase 4 | from frontends.gamespy.protocols.web_services.modules.direct2game.contracts.results import ( 5 | GetPurchaseHistoryResult, 6 | GetStoreAvailabilityResult, 7 | ) 8 | 9 | 10 | class GetPurchaseHistoryResponse(ResponseBase): 11 | _result: GetPurchaseHistoryResult 12 | 13 | def build(self) -> None: 14 | self._content = SoapEnvelop("GetPurchaseHistoryResult") 15 | self._content.add("status") 16 | self._content.add("code", self._result.code) 17 | self._content.change_to_element("GetPurchaseHistoryResult") 18 | self._content.add("orderpurchases") 19 | self._content.add("count", 0) 20 | super().build() 21 | 22 | 23 | class GetStoreAvailabilityResponse(ResponseBase): 24 | _result: GetStoreAvailabilityResult 25 | 26 | def build(self) -> None: 27 | self._content = SoapEnvelop("GetStoreAvailabilityResult") 28 | self._content.add("status") 29 | self._content.add("code", self._result.code) 30 | self._content.change_to_element("GetStoreAvailabilityResult") 31 | self._content.add("storestatusid", int(self._result.store_status_id)) 32 | super().build() 33 | -------------------------------------------------------------------------------- /src/backends/protocols/gamespy/query_report/requests.py: -------------------------------------------------------------------------------- 1 | 2 | from pydantic import UUID4 3 | import backends.library.abstractions.contracts as lib 4 | 5 | from frontends.gamespy.protocols.query_report.aggregates.enums import GameServerStatus 6 | from frontends.gamespy.protocols.query_report.v2.aggregates.enums import RequestType as V2RequestType 7 | 8 | from frontends.gamespy.protocols.query_report.v1.aggregates.enums import RequestType as V1RequestType 9 | 10 | 11 | class RequestBase(lib.RequestBase): 12 | instant_key: str 13 | command_name: V2RequestType 14 | raw_request: str 15 | 16 | 17 | class AvaliableRequest(RequestBase): 18 | pass 19 | 20 | 21 | class ChallengeRequest(RequestBase): 22 | pass 23 | 24 | 25 | class ClientMessageAckRequest(RequestBase): 26 | pass 27 | 28 | 29 | class ClientMessageRequest(RequestBase): 30 | server_browser_sender_id: UUID4 31 | target_query_report_id: UUID4 32 | natneg_message: str 33 | target_ip_address: str 34 | target_port: int 35 | command_name: None = None 36 | 37 | 38 | class HeartBeatRequest(RequestBase): 39 | data: dict[str, str] 40 | status: GameServerStatus 41 | group_id: int | None 42 | game_name: str 43 | 44 | 45 | 46 | 47 | class LegacyHeartbeatRequest(lib.RequestBase): 48 | command_name: V1RequestType 49 | raw_request: str 50 | query_id: str 51 | game_name: str 52 | data: dict[str, str] 53 | 54 | class EchoRequest(RequestBase): 55 | pass 56 | 57 | 58 | class KeepAliveRequest(RequestBase): 59 | pass 60 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/query_report/v1/applications/handlers.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.protocols.query_report.applications.client import Client 2 | from frontends.gamespy.protocols.query_report.v1.abstractions.contracts import RequestBase 3 | from frontends.gamespy.protocols.query_report.v1.abstractions.handlers import CmdHandlerBase 4 | from frontends.gamespy.protocols.query_report.v1.contracts.requests import LegacyHeartbeatRequest, HeartbeatPreRequest 5 | from frontends.gamespy.protocols.query_report.v1.contracts.responses import HeartbeatPreResponse 6 | from frontends.gamespy.protocols.query_report.v1.contracts.results import HeartbeatPreResult 7 | 8 | 9 | class HeartbeatPreHandler(CmdHandlerBase): 10 | _request: HeartbeatPreRequest 11 | _result: HeartbeatPreResult 12 | _response: HeartbeatPreResponse 13 | 14 | def __init__(self, client: Client, request: RequestBase) -> None: 15 | super().__init__(client, request) 16 | self._is_fetching = False 17 | self._is_uploading = False 18 | 19 | def _response_construct(self) -> None: 20 | self._result = HeartbeatPreResult( 21 | status=self._request.status, 22 | game_name=self._request.game_name) 23 | self._response = HeartbeatPreResponse(self._result) 24 | 25 | 26 | class LegacyHeartbeatHandler(CmdHandlerBase): 27 | _request: LegacyHeartbeatRequest 28 | 29 | def __init__(self, client: Client, request: RequestBase) -> None: 30 | super().__init__(client, request) 31 | self._is_fetching = False 32 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/web_services/modules/sake/contracts/responses.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.protocols.web_services.modules.sake.abstractions.generals import ResponseBase 2 | from frontends.gamespy.protocols.web_services.modules.sake.contracts.requests import CreateRecordRequest, SearchForRecordsRequest 3 | from frontends.gamespy.protocols.web_services.modules.sake.contracts.results import CreateRecordResult, SearchForRecordsResult 4 | 5 | 6 | class CreateRecordResponse(ResponseBase): 7 | _result: "CreateRecordResult" 8 | _request: "CreateRecordRequest" 9 | 10 | def build(self) -> None: 11 | self._content.add("CreateRecordResult") 12 | self._content.add("tableid", self._result.table_id) 13 | self._content.add("recordid", self._result.record_id) 14 | 15 | for field in self._result.fields: 16 | self._content.add("fields", field) 17 | 18 | super().build() 19 | 20 | 21 | class GetMyRecordResponse(ResponseBase): 22 | pass 23 | 24 | 25 | class SearchForRecordsResponse(ResponseBase): 26 | _result: "SearchForRecordsResult" 27 | _request: "SearchForRecordsRequest" 28 | 29 | def build(self) -> None: 30 | self._content.add("SearchForRecordsResponse") 31 | self._content.add("SearchForRecordsResult", "Success") 32 | if self._result.user_data is not None: 33 | for key, value in self._result.user_data.items(): 34 | self._content.add("values", value) 35 | else: 36 | self._content.add("values") 37 | 38 | super().build() 39 | -------------------------------------------------------------------------------- /src/backends/tests/gamespy/server_browser/handler_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from backends.protocols.gamespy.server_browser.handlers import ( 3 | ServerInfoHandler, 4 | ServerMainListHandler, 5 | ) 6 | from backends.protocols.gamespy.server_browser.requests import ( 7 | ServerInfoRequest, 8 | ServerListRequest, 9 | ) 10 | from backends.tests.utils import add_headers 11 | import frontends.gamespy.protocols.server_browser.v2.contracts.requests as fnt 12 | 13 | 14 | class HandlerTests(unittest.TestCase): 15 | def test_server_main_list(self): 16 | raw = b"\x00\x9a\x00\x01\x03\x8fU\x00\x00anno1701\x00anno1701\x00D:@o)Okhgroupid is null\x00\\hostname\\gamemode\\gamever\\gametype\\password\\mapname\\numplayers\\numaiplayers\\openslots\\gamevariant\x00\x00\x00\x00\x04" 17 | r = fnt.ServerListRequest(raw) 18 | data = add_headers(r) 19 | request = ServerListRequest(**data) 20 | handler = ServerMainListHandler(request) 21 | handler.handle() 22 | pass 23 | 24 | @unittest.skip("not implemented") 25 | def test_p2p_group_room_list(self): 26 | raise NotImplementedError() 27 | 28 | @unittest.skip("not implemented") 29 | def test_server_network_info_list(self): 30 | raise NotImplementedError() 31 | 32 | def test_server_info(self): 33 | raw = b"\x00\t\x01\xc0\xa8z\xe2+g" 34 | r = fnt.ServerInfoRequest(raw) 35 | data = add_headers(r) 36 | request = ServerInfoRequest(**data) 37 | handler = ServerInfoHandler(request) 38 | handler.handle() 39 | pass 40 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/web_services/modules/direct2game/applications/handlers.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.protocols.web_services.abstractions.handler import CmdHandlerBase 2 | from frontends.gamespy.protocols.web_services.applications.client import Client 3 | from frontends.gamespy.protocols.web_services.modules.direct2game.contracts.requests import ( 4 | GetPurchaseHistoryRequest, 5 | GetStoreAvailabilityRequest, 6 | ) 7 | from frontends.gamespy.protocols.web_services.modules.direct2game.contracts.responses import ( 8 | GetPurchaseHistoryResponse, 9 | GetStoreAvailabilityResponse, 10 | ) 11 | from frontends.gamespy.protocols.web_services.modules.direct2game.contracts.results import ( 12 | GetPurchaseHistoryResult, 13 | GetStoreAvailabilityResult, 14 | ) 15 | 16 | 17 | class GetPurchaseHistoryHandler(CmdHandlerBase): 18 | _request: GetPurchaseHistoryRequest 19 | _result: GetPurchaseHistoryResult 20 | _response: GetPurchaseHistoryResponse 21 | 22 | def __init__(self, client: Client, request: GetPurchaseHistoryRequest) -> None: 23 | assert isinstance(request, GetPurchaseHistoryRequest) 24 | super().__init__(client, request) 25 | 26 | 27 | class GetStoreAvailabilityHandler(CmdHandlerBase): 28 | _request: GetStoreAvailabilityRequest 29 | _result: GetStoreAvailabilityResult 30 | _response: GetStoreAvailabilityResponse 31 | 32 | def __init__(self, client: Client, request: GetStoreAvailabilityRequest) -> None: 33 | assert isinstance(request, GetStoreAvailabilityRequest) 34 | super().__init__(client, request) 35 | -------------------------------------------------------------------------------- /src/backends/protocols/gamespy/web_services/responses.py: -------------------------------------------------------------------------------- 1 | from backends.library.abstractions.contracts import DataResponse 2 | from frontends.gamespy.protocols.web_services.modules.auth.contracts.results import CreateUserAccountResult, LoginProfileResult, LoginPs3CertResult, LoginRemoteAuthResult, LoginUniqueNickResult 3 | from frontends.gamespy.protocols.web_services.modules.direct2game.contracts.results import GetPurchaseHistoryResult 4 | from frontends.gamespy.protocols.web_services.modules.sake.contracts.results import CreateRecordResult, GetMyRecordsResult, SearchForRecordsResult 5 | 6 | # region Auth 7 | 8 | 9 | class LoginProfileResponse(DataResponse): 10 | result: LoginProfileResult 11 | 12 | 13 | class LoginPS3CertRepsonse(DataResponse): 14 | result: LoginPs3CertResult 15 | 16 | 17 | class LoginRemoteAuthRepsonse(DataResponse): 18 | result: LoginRemoteAuthResult 19 | 20 | 21 | class LoginUniqueNickResponse(DataResponse): 22 | result: LoginUniqueNickResult 23 | 24 | 25 | class GetPurchaceHistoryResponse(DataResponse): 26 | result: GetPurchaseHistoryResult 27 | 28 | 29 | class CreateUserAccountResponse(DataResponse): 30 | result: CreateUserAccountResult 31 | 32 | 33 | 34 | # class GetTargettedAdResponse(DataResponse): 35 | # result: GetTargettedAdResult 36 | 37 | # region Sake 38 | 39 | 40 | class CreateRecordResponse(DataResponse): 41 | result: CreateRecordResult 42 | 43 | 44 | class GetMyRecordsResponse(DataResponse): 45 | result: GetMyRecordsResult 46 | 47 | 48 | class SearchForRecordsResponse(DataResponse): 49 | result: SearchForRecordsResult 50 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/presence_search_player/abstractions/contracts.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | import frontends.gamespy.library.abstractions.contracts as lib 3 | from frontends.gamespy.library.extentions.gamespy_utils import convert_to_key_value 4 | from frontends.gamespy.protocols.presence_search_player.aggregates.exceptions import ( 5 | GPParseException, 6 | ) 7 | 8 | 9 | class RequestBase(lib.RequestBase): 10 | _request_dict: dict[str, str] 11 | raw_request: str 12 | command_name: str 13 | operation_id: int 14 | namespace_id: int 15 | 16 | def __init__(self, raw_request: str) -> None: 17 | assert isinstance(raw_request, str) 18 | super().__init__(raw_request) 19 | self.operation_id = 0 20 | self.namespace_id = 0 21 | 22 | def parse(self) -> None: 23 | self._request_dict = convert_to_key_value(self.raw_request) 24 | self.command_name = list(self._request_dict.keys())[0] 25 | if "id" in self._request_dict.keys(): 26 | try: 27 | self.operation_id = int(self._request_dict["id"]) 28 | except ValueError: 29 | raise GPParseException("operation id is invalid.") 30 | 31 | if "namespaceid" in self._request_dict: 32 | try: 33 | self.namespace_id = int(self._request_dict["namespaceid"]) 34 | except ValueError: 35 | raise GPParseException("namespaceid is incorrect.") 36 | 37 | 38 | class ResultBase(lib.ResultBase): 39 | pass 40 | 41 | 42 | class ResponseBase(lib.ResponseBase): 43 | _result: ResultBase 44 | _request: RequestBase 45 | -------------------------------------------------------------------------------- /src/frontends/gamespy/library/encryption/xor_encryption.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | from frontends.gamespy.library.abstractions.enctypt_base import EncryptBase 4 | 5 | 6 | class XorType(IntEnum): 7 | TYPE_0 = 0 8 | TYPE_1 = 1 9 | TYPE_2 = 2 10 | TYPE_3 = 3 11 | 12 | 13 | class XorEncoding(EncryptBase): 14 | def __init__(self, xor_type): 15 | self.encryption_type = xor_type 16 | 17 | @staticmethod 18 | def encode(plaintext: bytes, enc_type: XorType): 19 | assert isinstance(plaintext, bytes) 20 | assert isinstance(enc_type, XorType) 21 | seed_0 = b"gamespy" 22 | seed_1 = b"GameSpy3D" 23 | seed_2 = b"Industries" 24 | seed_3 = b"ProjectAphex" 25 | index = 0 26 | key = seed_0 27 | if enc_type == XorType.TYPE_0: 28 | key = seed_0 29 | elif enc_type == XorType.TYPE_1: 30 | key = seed_1 31 | elif enc_type == XorType.TYPE_2: 32 | key = seed_2 33 | elif enc_type == XorType.TYPE_3: 34 | key = seed_3 35 | result = [] 36 | key_index = 0 37 | for index in range(len(plaintext)): 38 | key_index = index % len(key) 39 | enc_byte = (plaintext[index] ^ key[key_index]) % 255 40 | result.append(enc_byte) 41 | 42 | return bytes(result) 43 | 44 | def encrypt(self, data: bytes): 45 | super().encrypt(data) 46 | return XorEncoding.encode(data, self.encryption_type) 47 | 48 | def decrypt(self, data: bytes): 49 | super().decrypt(data) 50 | return XorEncoding.encode(data, self.encryption_type) 51 | -------------------------------------------------------------------------------- /src/backends/protocols/gamespy/natneg/requests.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.protocols.natneg.aggregations.enums import ( 2 | NatClientIndex, 3 | NatPortMappingScheme, 4 | NatPortType, 5 | NatType, 6 | PreInitState, 7 | RequestType, 8 | ) 9 | from typing import Union 10 | 11 | import backends.library.abstractions.contracts as lib 12 | from frontends.gamespy.protocols.natneg.contracts.results import ConnectResult 13 | 14 | # region Requests 15 | class RequestBase(lib.RequestBase): 16 | raw_request: str 17 | version: int 18 | cookie: int 19 | port_type: NatPortType 20 | command_name: RequestType 21 | 22 | 23 | class CommonRequestBase(RequestBase): 24 | client_index: NatClientIndex 25 | use_game_port: bool 26 | 27 | 28 | class AddressCheckRequest(CommonRequestBase): 29 | pass 30 | 31 | 32 | class ConnectAckRequest(RequestBase): 33 | client_index: NatClientIndex 34 | 35 | 36 | class ConnectRequest(CommonRequestBase): 37 | """ 38 | Server will send this request to client to let them connect to each other 39 | """ 40 | 41 | 42 | class ErtAckRequest(CommonRequestBase): 43 | pass 44 | 45 | 46 | class InitRequest(CommonRequestBase): 47 | game_name: str | None = None 48 | private_ip: str 49 | private_port: int 50 | 51 | 52 | class NatifyRequest(CommonRequestBase): 53 | pass 54 | 55 | 56 | class PreInitRequest(RequestBase): 57 | state: Union[PreInitState, int] 58 | target_cookie: list 59 | 60 | 61 | class ReportRequest(CommonRequestBase): 62 | is_nat_success: bool 63 | game_name: str 64 | nat_type: NatType 65 | mapping_scheme: NatPortMappingScheme 66 | 67 | -------------------------------------------------------------------------------- /src/frontends/gamespy/library/abstractions/contracts.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class RequestBase: 7 | command_name: object 8 | raw_request: object 9 | 10 | def __init__(self, raw_request: object) -> None: 11 | """ 12 | raw_request is for gamespy protocol\n 13 | json_dict is for restapi deserialization 14 | """ 15 | super().__init__() 16 | if raw_request is None: 17 | raise Exception("raw_request should not be None") 18 | 19 | if raw_request is not None: 20 | if (not isinstance(raw_request, bytes)) and ( 21 | not isinstance(raw_request, str) 22 | ): 23 | raise Exception("Unsupported raw_request type") 24 | self.raw_request = raw_request 25 | return 26 | # self.command_name = None 27 | # self.raw_request = None 28 | 29 | def parse(self) -> None: 30 | pass 31 | 32 | def to_dict(self) -> dict: 33 | """ 34 | create a json serializable dict of this class 35 | """ 36 | result = {} 37 | for key, value in self.__dict__.items(): 38 | if key[0] != "_": 39 | result[key] = value 40 | return result 41 | 42 | 43 | class ResultBase(BaseModel): 44 | pass 45 | 46 | 47 | class ResponseBase: 48 | sending_buffer: object 49 | _result: ResultBase 50 | def __init__(self, result: ResultBase) -> None: 51 | assert issubclass(type(result), ResultBase) 52 | self._result = result 53 | 54 | @abc.abstractmethod 55 | def build(self) -> None: 56 | pass 57 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/web_services/aggregations/soap_envelop.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | 3 | from frontends.gamespy.protocols.web_services.aggregations.exceptions import WebException 4 | 5 | 6 | class SoapEnvelop: 7 | current_element: ET.Element 8 | content: ET.Element 9 | 10 | def __init__(self, root_name: str): 11 | assert isinstance(root_name, str) 12 | self.content = ET.Element(root_name) 13 | self.current_element = self.content 14 | 15 | def change_to_element(self, name: str): 16 | current_element = self.content.find( 17 | f".//{name}") 18 | if current_element is None: 19 | raise WebException("can not find the node") 20 | self.current_element = current_element 21 | 22 | def go_to_content_element(self): 23 | self.current_element = self.content 24 | 25 | def add(self, name: str, value: object = None): 26 | tag = f"{name}" 27 | new_element = ET.SubElement( 28 | self.current_element, tag 29 | ) 30 | 31 | if value is None: 32 | self.parent_element = self.current_element 33 | self.current_element = new_element 34 | else: 35 | new_element.text = str(value) 36 | 37 | def __str__(self) -> str: 38 | xml_str: str = ET.tostring( 39 | self.content, 40 | encoding="unicode" 41 | ) 42 | return xml_str 43 | 44 | 45 | if __name__ == "__main__": 46 | import xml.etree.ElementTree as ET 47 | 48 | s = SoapEnvelop("test_name") 49 | s.add("level1", "1") 50 | s.add("level1.1", "1.1") 51 | s.go_to_content_element() 52 | s.add("level2", "2") 53 | str(s) 54 | -------------------------------------------------------------------------------- /src/backends/library/networks/redis_brocker.py: -------------------------------------------------------------------------------- 1 | 2 | import threading 3 | from redis.client import PubSub 4 | from typing import Callable 5 | from redis import Redis 6 | 7 | from frontends.gamespy.library.abstractions.brocker import BrockerBase 8 | from frontends.gamespy.library.configs import CONFIG 9 | 10 | 11 | class RedisBrocker(BrockerBase): 12 | _client: Redis 13 | _subscriber: PubSub 14 | 15 | def subscribe(self): 16 | self.is_started = True 17 | self._client = Redis.from_url(self.url, socket_timeout=5) 18 | self._client.ping() 19 | self._subscriber = self._client.pubsub() 20 | th = threading.Thread(target=self.get_message) 21 | th.start() 22 | 23 | def get_message(self): 24 | self._subscriber.subscribe(self._name) 25 | while True: 26 | m = self._subscriber.get_message(timeout=10) 27 | if m is not None: 28 | if "data" not in m: 29 | continue 30 | if not isinstance(m['data'], bytes): 31 | continue 32 | msg = m['data'].decode("utf-8") 33 | threading.Thread(target=self.receive_message, 34 | args=[msg]).start() 35 | 36 | def unsubscribe(self): 37 | self.is_started = False 38 | self._subscriber.unsubscribe(self._name) 39 | self._subscriber.close() 40 | 41 | def publish_message(self, message: str): 42 | assert isinstance(message, str) 43 | self._client.publish(self._name, message) 44 | 45 | 46 | if __name__ == "__main__": 47 | pass 48 | 49 | brocker = RedisBrocker("master", CONFIG.redis.url, print) 50 | brocker.subscribe() 51 | pass 52 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/query_report/v1/applications/switcher.py: -------------------------------------------------------------------------------- 1 | 2 | from frontends.gamespy.library.abstractions.handler import CmdHandlerBase 3 | from frontends.gamespy.library.abstractions.switcher import SwitcherBase 4 | from frontends.gamespy.protocols.query_report.aggregates.exceptions import QRException 5 | from frontends.gamespy.protocols.query_report.applications.client import Client 6 | from frontends.gamespy.protocols.query_report.v1.aggregates.enums import RequestType 7 | from frontends.gamespy.protocols.query_report.v1.applications.handlers import HeartbeatPreHandler, LegacyHeartbeatHandler 8 | from frontends.gamespy.protocols.query_report.v1.contracts.requests import HeartbeatPreRequest, LegacyHeartbeatRequest 9 | 10 | 11 | class Switcher(SwitcherBase): 12 | _raw_request: str 13 | _client: Client 14 | 15 | def _process_raw_request(self) -> None: 16 | if len(self._raw_request) < 4: 17 | raise QRException("Invalid request length") 18 | if self._raw_request[0] != "\\": 19 | raise QRException("Invalid queryreport v1 request") 20 | name = self._raw_request.strip("\\").split("\\")[0] 21 | if name not in RequestType: 22 | self._client.log_debug( 23 | f"Request: {name} is not a valid request.") 24 | self._requests.append((RequestType(name), self._raw_request)) 25 | 26 | def _create_cmd_handlers(self, name: RequestType, raw_request: str) -> CmdHandlerBase | None: 27 | match(name): 28 | case RequestType.HEARTBEAT: 29 | return HeartbeatPreHandler(self._client, HeartbeatPreRequest(raw_request)) 30 | case RequestType.HEARTBEAT_ACK: 31 | return LegacyHeartbeatHandler(self._client, LegacyHeartbeatRequest(raw_request)) 32 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/server_browser/v2/abstractions/handlers.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.library.abstractions.handler import CmdHandlerBase as CMB 2 | 3 | from frontends.gamespy.protocols.server_browser.v2.abstractions.contracts import ( 4 | ServerListUpdateOptionRequestBase, 5 | ServerListUpdateOptionResponseBase, 6 | ServerListUpdateOptionResultBase, 7 | ) 8 | from frontends.gamespy.protocols.server_browser.v2.aggregations.encryption import EnctypeX 9 | from frontends.gamespy.protocols.server_browser.v2.applications.client import Client 10 | from frontends.gamespy.protocols.server_browser.v2.abstractions.contracts import ( 11 | RequestBase, 12 | ResultBase, 13 | ResponseBase, 14 | ) 15 | 16 | 17 | class CmdHandlerBase(CMB): 18 | _client: Client 19 | _request: RequestBase 20 | _result: ResultBase 21 | _response: ResponseBase 22 | 23 | def __init__(self, client: Client, request: RequestBase) -> None: 24 | assert isinstance(client, Client) 25 | assert issubclass(type(request), RequestBase) 26 | super().__init__(client, request) 27 | 28 | 29 | class ServerListUpdateOptionHandlerBase(CmdHandlerBase): 30 | _request: ServerListUpdateOptionRequestBase 31 | _result: ServerListUpdateOptionResultBase 32 | _response: ServerListUpdateOptionResponseBase 33 | 34 | def _data_operate(self) -> None: 35 | # query game secret key 36 | super()._data_operate() 37 | self._client.info.client_challenge = self._request.client_challenge 38 | self._client.info.game_secret_key = self._result.game_secret_key 39 | # use secret key to construct _client.crypto 40 | self._client.crypto = EnctypeX( 41 | self._client.info.game_secret_key, self._client.info.client_challenge 42 | ) 43 | 44 | -------------------------------------------------------------------------------- /common/pg_certs/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCc2m6mPXdnbbiI 3 | 1S7ZLSHKm+Q9+ae6xmAShQxmZCusEKQqaad4Dg/Yx7iD7V0umzAPJx7KsQw0T+Ml 4 | V3pcOO66Whz308eS3++fJiwBUL9j333mDtcISTPeqi1CO4+BwocUbFrPw7YGWTr2 5 | 8Oz/K+x84ZvD1l1QNFU3xne+iBUKqUMNjLS5n4+KA3C3poLLzbquzRG38uFWFoxn 6 | +XPBDbNQH3Y6+ZFQu0ztZ1bWwD8fJKOYIFUUaxyJ9/6XBMr7UFQfR1oxSy1IhqvM 7 | w8o4sB9yimBAvOgxOfSjIMRMk3zvEBY3TZ4QYCjyiVW/PQTPjVhFP9VKBr78WnTb 8 | jY/KmDvBAgMBAAECggEADdazQ7lBVUefy/nEEP7rf+7WWbXyu5a4P/YLYKbGmB8d 9 | Ps00zHGwKfv//mrFKYEhYbrdu3wYo65nL+KajfOrc1dTPjXKAj4969SQNhr/0dHU 10 | b6VgSS+9MvBgfxsWZ5hINu/y6Kj/oKqDemlJ/Z7sVd3JUoNRlwuQ97Nr2exzb83Z 11 | ByArsrrYGhApckgJ5lO5KUoEWX0ciVb3t4rQftNYSGrJUl+AKkpNrsCzhzprfJ9Y 12 | q79/lJZ9ZBspYemF9goRtFcQ0ZuYgg05fEKNIbjm5SrNhmT0GDCFbQXcus4+aNT/ 13 | YvTtwE9O4NJ8pfEsm6BFHpmfubu8xCbX33nYSFhE+QKBgQDWYjou0HL+7KJoqzhw 14 | 23UZCvIoH7cBqtjd5+PcpiSAEuLtPT9UJ67ZoEN4zzC8gA3KenoOVkzY9teT/CYZ 15 | 3wGzJFLLYLroQ+4pFvDNreNeT72RzlF7MYu51f7n0EEv7OrKZ/KdnxtmBqj0HH6p 16 | ZbSkdknKz+wN/MgEMoYmpuepyQKBgQC7TTxtSaEllHwcPudGNIX4CRHNEKreU4RA 17 | o3ggcvlmV6YKnSlwFzaF+Lgzox2J2OyswoDSgIHfBfpCCQjE6NY6/48XLuTW8JWh 18 | kIyOaJuBS3UQDYfEdNpnpjgMU+ZwW62aC3opZCe8nK/JOWw1lCrJirZh0INM+1xy 19 | RfG+WR7+OQKBgC+rxf5U8c1H91FJCZLm6eH4siJD8yDWycSGZP/Snfkwue9BGEzx 20 | SgswfPBnOhIgc3CbzXpUrF/ue793aU2Fbk5UfGinCMjPGi1e4YsK6K03FBNRCoNX 21 | YBehwz3u7B/pEciSVru//oqwXm9xyqSGbiXH+96yX2440I1GYthDcu4pAoGARrPy 22 | IoMPzKLPcs4f+XVsOOQbjyBCj+hQ3SGYAA/Gq2ZcrFcFRGXO1CW+SufBB78WIGTP 23 | wiZ2X9zeyjykzcfizqSXvDWcdrKcmT96f2tngBge2W9yF6vQoh2xvJ2TOEizMJoy 24 | hBtlkKJJDRmbCmKjAC9Xh3bxiYa9L/nNNoBn3akCgYBrcFhAfzxR8uYOCD7p1blK 25 | xz/Q72K8wTA6IHvpwFh89oby0hRTiuulOtyHs08dC2OU2sEzJvYEyK665Q8k6ttC 26 | CVgt1nC9s/UTt7piZ1cURQhnmbw1T5wzBXyTArRXoXj7CphDY/9QxaFdSFp5pFUP 27 | 2BMWzhRZm/Q9S7oKz9TOjw== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /common/pg_certs/root/root.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDGJV2aui+kt1+V 3 | zdGzUTZpaUIFK/Iy4NGxczjc31RJvaI045teG7x7VIcon+YCk01iv3sB9HXqbMxQ 4 | +QNweTdhtgYfpyPlYNV6O4ddnPPhml5LSZwFcEycsvKvIn002lOmXiFzGodnTyzH 5 | ZesFRLaX/lR9FmHLRNIvA26mPzwZknWLgLBhENWrmRYOz11m+OXjsgh4A6+GwAc9 6 | sNIzokVslhhK5+W9rCPqfFX5FZ1iNRkC0bHDC/Ur+AanUxBCdcZEV625OzfxAfYW 7 | gbvMJGKN2lPTjZ5C7DOzSymIseQxA/UQUX5UoBGSLtK2awmOqhcFtWELk6AlmTla 8 | 3zFOYPdrAgMBAAECggEAH2LZ0eF+HHxDcso4Vkwd71KR95m/cpmz/YS/1BS4GDom 9 | kHQhLyX7lBmOkzvIxk2o62RjSqr7Zpe3QXhAODerMxoPHEJwaCwPhJ4bUhPDec/m 10 | 8cwGH8JrEEM0N9Ohu5Z9u5Obfs0L96xN7oPRV3NL7QWHQo0iDn2nQXUsuL65eV83 11 | Tgv0eK8GlVoESGgnE9/w5U2gHMEvrKD3Zxax5i8HknSSLKS235iNnhlX1anNC34H 12 | PQfWxCqapzRv+FB7t+AtlZx8RU/ky4Q6HkzeuXAZQ1qEk7+tblnt5B3DpHTf3l7w 13 | EDQSvCKeKDj6ryiu+/TWwfzJFGuFVarkv8yllUFHUQKBgQDj+Qd0zytpLHkGzTpF 14 | 53iYpLyOclNd3FXTuV1L7XQB1mwKmXvL807j9tonF4repBKiLWR64SzY81sRcNAr 15 | 8SZF/C7AFtTuDJYjB81LLdUcVC7Hmen0hjQxa0OiVGck9yOlfZCcd3Ww9NFp1ycL 16 | MnbIRWI8J8T9Qh843hMxLPpdCQKBgQDegZlze+lWlrwxn1SkYeh/bIS+WM/MwB/4 17 | +L7rx40W49EDhxVCM4fO91VkZN4nZPXvIDhF/8A3FMEuAxVGgbJnG9fln+3xbNeZ 18 | fKqAS3rIXzVtDDuwsNLVX6NQZ0CagCnJyXJqDvI+N6Mgg9cdPnCL6dbthanZVYzV 19 | Cxo9F+9B0wKBgB95SiY+U+f5U9w0iU8NXgD0/XNNJWVX/iF0/gR1jAaU6+WquwS1 20 | WrbuZb/v6CRE0q3BRpYQcHijYHdP8+2dJYOUBYBPpqYW5sN/WECA22NF3A+CmGJC 21 | BQKtpHDM5lCcLjey1jxD4ePEaQULx0Asf2m26pETjIbKkjTvtAaeBxLBAoGBAJmS 22 | V4gmgPFbjj6tmqzuSpsQGjqKb7oA7NBZVuTDUTT4Pj2yEVEk4dpOSWjGWbJU8418 23 | 7noZv+AEeiS4yglk4O5bgFKjZIYaOmBcdA2iivcbB3PhWp1kHdBZdw26hhNc2/rD 24 | CC39bOLWYcfCV0l+3A0lc0ty0r0HV/F+/TgneeIzAoGBANpri83xFrYQzS6fjm9U 25 | SB/4mTzdRRVqh44UEYKciSnMhD8I8XYKV7vV3Ix9L+yYGxTOUDfskDLrQ1t66oqV 26 | o7mmG5MFkSoTdMB1v6hZXVYhSQ5J5Q9QNdSppU8NLxXiaFScJdhZpF2wcINkbxPm 27 | j9TqJ4zJ7H+FP0RP9Kn+zK23 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /src/backends/routers/gamespy/natneg.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | from backends.library.abstractions.contracts import RESPONSES_DEF, DataResponse, OKResponse, Response 4 | from backends.protocols.gamespy.natneg.handlers import ConnectHandler, InitHandler, ReportHandler 5 | from backends.protocols.gamespy.natneg.requests import AddressCheckRequest, ConnectRequest, ErtAckRequest, InitRequest, ReportRequest 6 | from backends.protocols.gamespy.natneg.responses import ConnectResponse 7 | from backends.urls import NATNEG 8 | 9 | 10 | router = APIRouter() 11 | 12 | 13 | @router.post(f"{NATNEG}/AddressCheckHandler", responses=RESPONSES_DEF) 14 | def address_check(request: AddressCheckRequest) -> Response: 15 | raise NotImplementedError() 16 | 17 | 18 | @router.post(f"{NATNEG}/ConnectHandler", responses=RESPONSES_DEF) 19 | def connect(request: ConnectRequest) -> ConnectResponse: 20 | handler = ConnectHandler(request) 21 | handler.handle() 22 | return handler.response 23 | 24 | 25 | @router.post(f"{NATNEG}/ErtAckHandler", responses=RESPONSES_DEF) 26 | def ert_ack(request: ErtAckRequest) -> Response: 27 | raise NotImplementedError() 28 | 29 | 30 | @router.post(f"{NATNEG}/InitHandler", responses=RESPONSES_DEF) 31 | def init(request: InitRequest) -> Response: 32 | handler = InitHandler(request) 33 | handler.handle() 34 | return handler.response 35 | 36 | 37 | @router.post(f"{NATNEG}/ReportHandler", responses=RESPONSES_DEF) 38 | def report(request: ReportRequest) -> OKResponse: 39 | handler = ReportHandler(request) 40 | handler.handle() 41 | return handler.response 42 | 43 | 44 | if __name__ == "__main__": 45 | import uvicorn 46 | from fastapi import FastAPI 47 | app = FastAPI() 48 | app.include_router(router) 49 | uvicorn.run(app, host="0.0.0.0", port=8080) 50 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/web_services/modules/sake/applications/handlers.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.protocols.web_services.applications.client import Client 2 | from frontends.gamespy.protocols.web_services.modules.sake.abstractions.generals import CmdHandlerBase 3 | from frontends.gamespy.protocols.web_services.modules.sake.contracts.requests import CreateRecordRequest, GetMyRecordsRequest, SearchForRecordsRequest 4 | from frontends.gamespy.protocols.web_services.modules.sake.contracts.responses import CreateRecordResponse, GetMyRecordResponse, SearchForRecordsResponse 5 | from frontends.gamespy.protocols.web_services.modules.sake.contracts.results import CreateRecordResult, GetMyRecordsResult, SearchForRecordsResult 6 | 7 | 8 | class CreateRecordHandler(CmdHandlerBase): 9 | _request: CreateRecordRequest 10 | _result: CreateRecordResult 11 | _response: CreateRecordResponse 12 | 13 | def __init__(self, client: Client, request: CreateRecordRequest) -> None: 14 | assert isinstance(request, CreateRecordRequest) 15 | super().__init__(client, request) 16 | 17 | 18 | class GetMyRecordsHandler(CmdHandlerBase): 19 | _request: GetMyRecordsRequest 20 | _result: GetMyRecordsResult 21 | _response: GetMyRecordResponse 22 | 23 | def __init__(self, client: Client, request: GetMyRecordsRequest) -> None: 24 | assert isinstance(request, GetMyRecordsRequest) 25 | super().__init__(client, request) 26 | 27 | 28 | class SearchForRecordsHandler(CmdHandlerBase): 29 | _request: SearchForRecordsRequest 30 | _result: SearchForRecordsResult 31 | _response: SearchForRecordsResponse 32 | 33 | def __init__(self, client: Client, request: SearchForRecordsRequest) -> None: 34 | assert isinstance(request, SearchForRecordsRequest) 35 | super().__init__(client, request) 36 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/server_browser/v2/aggregations/enums.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | 4 | class PlayerSearchOptions(IntEnum): 5 | SEARCH_ALL_GAMES = 1 6 | SEARCH_LEFT_SUBSTRING = 2 7 | SEARCH_RIGHT_SUBSTRING = 4 8 | SEARCH_ANY_SUBSTRING = 8 9 | 10 | 11 | class QueryType(IntEnum): 12 | BASIC = 0 13 | FULL = 1 14 | ICMP = 2 15 | 16 | 17 | class DataKeyType(IntEnum): 18 | STRING = 0 19 | BYTE = 1 20 | SHORT = 2 21 | 22 | 23 | class RequestType(IntEnum): 24 | SERVER_LIST_REQUEST = 0x00 25 | SERVER_INFO_REQUEST = 0x01 26 | SEND_MESSAGE_REQUEST = 0x02 27 | KEEP_ALIVE_REPLY = 0x03 28 | MAP_LOOP_REQUEST = 0x04 29 | PLAYER_SEARCH_REQUEST = 0x05 30 | 31 | 32 | class ResponseType(IntEnum): 33 | PUSH_KEYS_MESSAGE = 1 34 | PUSH_SERVER_MESSAGE = 2 35 | KEEP_ALIVE_MESSAGE = 3 36 | DELETE_SERVER_MESSAGE = 4 37 | MAP_LOOP_MESSAGE = 5 38 | PLAYER_SEARCH_MESSAGE = 6 39 | 40 | 41 | class ProtocolVersion(IntEnum): 42 | LIST_PROTOCOL_VERSION_1 = 0 43 | LIST_ENCODING_VERSION = 3 44 | 45 | 46 | class ServerListUpdateOption(IntEnum): 47 | SERVER_MAIN_LIST = 0 48 | SEND_FIELD_FOR_ALL = 1 49 | SERVER_FULL_INFO_LIST = 2 50 | """ 51 | get the full info of a server 52 | """ 53 | P2P_SERVER_MAIN_LIST = 4 54 | ALTERNATE_SOURCE_IP = 8 55 | P2P_GROUP_ROOM_LIST = 32 56 | NO_LIST_CACHE = 64 57 | LIMIT_RESULT_COUNT = 128 58 | 59 | 60 | class GameServerFlags(IntEnum): 61 | UNSOLICITED_UDP_FLAG = 1 62 | PRIVATE_IP_FLAG = 2 63 | CONNECT_NEGOTIATE_FLAG = 4 64 | ICMP_IP_FLAG = 8 65 | NON_STANDARD_PORT_FLAG = 16 66 | NON_STANDARD_PRIVATE_PORT_FLAG = 32 67 | HAS_KEYS_FLAG = 64 68 | HAS_FULL_RULES_FLAG = 128 69 | 70 | 71 | if __name__ == "__main__": 72 | GameServerFlags.PRIVATE_IP_FLAG.value 73 | pass 74 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/presence_connection_manager/abstractions/contracts.py: -------------------------------------------------------------------------------- 1 | 2 | from frontends.gamespy.library.extentions.gamespy_utils import convert_to_key_value 3 | from frontends.gamespy.protocols.presence_search_player.aggregates.exceptions import ( 4 | GPParseException, 5 | ) 6 | from typing import Dict, Optional 7 | import frontends.gamespy.library.abstractions.contracts as lib 8 | 9 | 10 | def normalize_request(message: str): 11 | if "login" in message: 12 | message = message.replace("\\-", "\\") 13 | pos = message.index("\\", message.index("\\") + 1) 14 | if message[pos: pos + 2] != "\\\\": 15 | message = message[:pos] + "\\" + message[pos:] 16 | return message 17 | 18 | 19 | class RequestBase(lib.RequestBase): 20 | command_name: str 21 | operation_id: int 22 | raw_request: str 23 | _request_dict: Dict[str, str] 24 | 25 | def __init__(self, raw_request: str) -> None: 26 | assert isinstance(raw_request, str) 27 | super().__init__(raw_request) 28 | 29 | def parse(self): 30 | super().parse() 31 | self._request_dict = convert_to_key_value(self.raw_request) 32 | self.command_name = list(self._request_dict.keys())[0] 33 | 34 | if "id" in self._request_dict: 35 | try: 36 | self.operation_id = int(self._request_dict["id"]) 37 | except Exception: 38 | raise GPParseException("namespaceid is invalid.") 39 | 40 | 41 | class ResultBase(lib.ResultBase): 42 | operation_id: int 43 | pass 44 | 45 | 46 | class ResponseBase(lib.ResponseBase): 47 | _request: RequestBase 48 | _result: ResultBase 49 | sending_buffer: str 50 | 51 | def __init__(self, result: ResultBase) -> None: 52 | assert issubclass(type(result), ResultBase) 53 | super().__init__(result) 54 | -------------------------------------------------------------------------------- /src/backends/tests/gamespy/chat/lib_tests.py: -------------------------------------------------------------------------------- 1 | # from typing import cast 2 | # import unittest 3 | 4 | # from fastapi import WebSocket 5 | # from fastapi.datastructures import Address 6 | 7 | # from backends.protocols.gamespy.chat.brocker_manager import ( 8 | # ChatWebSocketClient, 9 | # ChatWebSocketManager, 10 | # ) 11 | 12 | 13 | # class WebSocketMock: 14 | # client: Address = Address("127.0.0.1", 123) 15 | 16 | 17 | # class LibTests(unittest.TestCase): 18 | # def test_ws_manager(self): 19 | # ws = WebSocketMock() 20 | # manager = ChatWebSocketManager() 21 | # ws = cast(WebSocket, ws) 22 | # manager.connect(ws) 23 | # self.assertEqual(list(manager.client_pool.values())[0].ws, ws) 24 | # manager.disconnect(ws) 25 | # self.assertEqual(len(manager.client_pool.values()), 0) 26 | 27 | # def test_chat_ws_manager(self): 28 | # ws = WebSocketMock() 29 | # manager = ChatWebSocketManager() 30 | # ws = cast(WebSocket, ws) 31 | # manager.connect(ws) 32 | # self.assertEqual(list(manager.client_pool.values())[0].ws, ws) 33 | 34 | # channel_name = "gmtest" 35 | # manager.add_to_channel(channel_name, ws) 36 | # client = manager.get_client(ws) 37 | # client = cast(ChatWebSocketClient, client) 38 | # manager.channel_info 39 | # self.assertTrue(channel_name in manager.channel_info) 40 | # self.assertTrue(len(manager.channel_info.values()) != 0) 41 | # self.assertTrue(channel_name in client.channels) 42 | # manager.remove_from_channel(channel_name, ws) 43 | # self.assertTrue(channel_name not in manager.channel_info) 44 | # self.assertTrue(len(manager.channel_info.values()) == 0) 45 | # self.assertTrue(channel_name not in client.channels) 46 | 47 | # manager.disconnect(ws) 48 | # self.assertEqual(len(manager.client_pool.values()), 0) 49 | -------------------------------------------------------------------------------- /src/backends/protocols/gamespy/game_status/requests.py: -------------------------------------------------------------------------------- 1 | from pydantic import Field, model_validator 2 | import backends.library.abstractions.contracts as lib 3 | from frontends.gamespy.protocols.game_status.aggregations.enums import ( 4 | AuthMethod, 5 | PersistStorageType, 6 | ) 7 | from frontends.gamespy.protocols.game_status.aggregations.exceptions import GSException 8 | 9 | 10 | class RequestBase(lib.RequestBase): 11 | raw_request: str 12 | local_id: int 13 | _request_dict: dict[str, str] 14 | 15 | 16 | class AuthGameRequest(RequestBase): 17 | game_name: str 18 | 19 | 20 | class AuthPlayerRequest(RequestBase): 21 | auth_type: AuthMethod 22 | profile_id: int | None = None 23 | auth_token: str | None = None 24 | cdkey_hash: str | None = None 25 | response: str | None = None 26 | nick: str | None = None 27 | 28 | 29 | class GetPlayerDataRequest(RequestBase): 30 | profile_id: int 31 | storage_type: PersistStorageType 32 | data_index: int 33 | is_get_all_data: bool = False 34 | keys: list[str] 35 | 36 | 37 | class GetProfileIdRequest(RequestBase): 38 | nick: str 39 | key_hash: str 40 | 41 | 42 | class NewGameRequest(RequestBase): 43 | is_client_local_storage_available: bool 44 | challenge: str | None = None 45 | connection_id: int = Field( 46 | description="The session key that backend send to client." 47 | ) 48 | session_key: str = Field(description="The game session key") 49 | 50 | 51 | class SetPlayerDataRequest(RequestBase): 52 | profile_id: int 53 | storage_type: PersistStorageType 54 | data_index: int 55 | length: int 56 | report: str | None = None 57 | data: str 58 | is_key_value: bool 59 | 60 | 61 | class UpdateGameRequest(RequestBase): 62 | connection_id: int 63 | is_done: bool 64 | is_client_local_storage_available: bool 65 | game_data: str 66 | game_data_dict: dict[str, str] 67 | session_key: str 68 | -------------------------------------------------------------------------------- /src/frontends/gamespy/library/exceptions/general.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | from typing import TYPE_CHECKING, Optional 3 | 4 | from frontends.gamespy.library.configs import CONFIG 5 | from frontends.gamespy.library.log.log_manager import GLOBAL_LOGGER 6 | 7 | 8 | if TYPE_CHECKING: 9 | from frontends.gamespy.library.abstractions.client import ClientBase 10 | 11 | 12 | class UniSpyException(Exception): 13 | message: str 14 | """the error message""" 15 | 16 | def __init__(self, message: str) -> None: 17 | self.message = message 18 | 19 | @staticmethod 20 | # def handle_exception(e: Exception, client: ClientBase = None): 21 | def handle_exception(e: Exception, client: Optional["ClientBase"] = None): 22 | # first log the exception 23 | if client is None: 24 | GLOBAL_LOGGER.info(str(e)) 25 | else: 26 | if issubclass(type(e), UniSpyException): 27 | ex: UniSpyException = e # type:ignore 28 | client.log_error(ex.message) 29 | elif issubclass(type(e), BrokenPipeError): 30 | client.log_warn(f"client disconnect before message send") 31 | else: 32 | client.log_error(traceback.format_exc()) 33 | # if we are unittesting we raise the exception out 34 | if CONFIG.unittest.is_raise_except: 35 | raise e 36 | 37 | def __repr__(self) -> str: 38 | # return super().__repr__() 39 | return f'Error message: "{self.message}"' 40 | 41 | 42 | class DatabaseConnectionException(UniSpyException): 43 | def __init__(self, message: str = "Can not connect to database.") -> None: 44 | super().__init__(message) 45 | 46 | 47 | class RedisConnectionException(UniSpyException): 48 | def __init__(self, message: str = "Can not connect to redis") -> None: 49 | super().__init__(message) 50 | 51 | 52 | if __name__ == "__main__": 53 | err = UniSpyException("test") 54 | pass 55 | -------------------------------------------------------------------------------- /src/frontends/tests/gamespy/library/mock_objects.py: -------------------------------------------------------------------------------- 1 | import socketserver 2 | 3 | import responses 4 | from frontends.gamespy.library.abstractions.brocker import BrockerBase 5 | from frontends.gamespy.library.abstractions.connections import ConnectionBase 6 | from frontends.gamespy.library.abstractions.handler import CmdHandlerBase 7 | from frontends.gamespy.library.log.log_manager import GLOBAL_LOGGER, LogWriter 8 | from frontends.gamespy.library.configs import CONFIG, ServerConfig 9 | 10 | 11 | class ConnectionMock(ConnectionBase): 12 | def send(self, data: bytes) -> None: 13 | pass 14 | 15 | 16 | class RequestHandlerMock(socketserver.BaseRequestHandler): 17 | client_address: tuple 18 | 19 | def __init__(self, client_address: tuple = ("192.168.0.1", 0)) -> None: 20 | self.client_address = client_address 21 | 22 | pass 23 | 24 | 25 | class LogMock(LogWriter): 26 | def __init__(self) -> None: 27 | super().__init__(None) 28 | 29 | def debug(self, message): 30 | print(message) 31 | GLOBAL_LOGGER.debug(message) 32 | 33 | def info(self, message): 34 | print(message) 35 | GLOBAL_LOGGER.info(message) 36 | 37 | def error(self, message): 38 | print(message) 39 | GLOBAL_LOGGER.error(message) 40 | 41 | def warn(self, message): 42 | print(message) 43 | GLOBAL_LOGGER.warn(message) 44 | 45 | 46 | class BrokerMock(BrockerBase): 47 | def __init__(self) -> None: 48 | pass 49 | 50 | def subscribe(self): 51 | pass 52 | 53 | def publish_message(self, message): 54 | pass 55 | 56 | def unsubscribe(self): 57 | pass 58 | 59 | 60 | def create_mock_url( 61 | config: ServerConfig, handler: type[CmdHandlerBase], data: dict 62 | ) -> None: 63 | url = f"{CONFIG.backend.url}/GameSpy/{config.server_name}/{handler.__name__}" 64 | resp = {"result": data} 65 | responses.add(responses.POST, url, json=resp, status=200) 66 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/game_status/contracts/responses.py: -------------------------------------------------------------------------------- 1 | 2 | from typing import final 3 | from frontends.gamespy.library.abstractions.contracts import ResponseBase 4 | from frontends.gamespy.protocols.game_status.contracts.results import AuthGameResult, AuthPlayerResult, GetPlayerDataResult, GetProfileIdResult, SetPlayerDataResult 5 | 6 | 7 | @final 8 | class AuthGameResponse(ResponseBase): 9 | _result: AuthGameResult 10 | 11 | def build(self) -> None: 12 | # fmt: off 13 | self.sending_buffer = f"\\sesskey\\{self._result.session_key}\\lid\\{self._result.local_id}\\final\\" 14 | # fmt: on 15 | 16 | 17 | @final 18 | class AuthPlayerResponse(ResponseBase): 19 | _result: AuthPlayerResult 20 | 21 | def build(self) -> None: 22 | self.sending_buffer = f"\\pauthr\\{self._result.profile_id}\\lid\\{self._result.local_id}\\final\\" 23 | 24 | 25 | @final 26 | class GetPlayerDataResponse(ResponseBase): 27 | _result: GetPlayerDataResult 28 | 29 | def build(self) -> None: 30 | mod_time = int(self._result.modified.timestamp()) 31 | self.sending_buffer = f"\\getpdr\\1\\pid\\{self._result.profile_id}\\lid\\{self._result.local_id}\\mod\\{mod_time}\\length\\{len(self._result.data)}\\data\\{self._result.data}\\final\\" 32 | 33 | 34 | @final 35 | class GetProfileIdResponse(ResponseBase): 36 | _result: GetProfileIdResult 37 | 38 | def build(self) -> None: 39 | # fmt: off 40 | self.sending_buffer = f"\\getpidr\\{self._result.profile_id}\\lid\\{self._result.local_id}\\final\\" 41 | # fmt: on 42 | 43 | 44 | @final 45 | class SetPlayerDataResponse(ResponseBase): 46 | _result: SetPlayerDataResult 47 | 48 | def build(self) -> None: 49 | # \\setpdr\\1\\lid\\2\\pid\\100000\\mod\\12345 50 | mod_time = int(self._result.modified.timestamp()) 51 | self.sending_buffer = f"\\setpdr\\1\\pid\\{self._result.profile_id}\\lid\\{self._result.local_id}\\mod\\{mod_time}\\final\\" 52 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/presence_connection_manager/aggregates/sdk_revision.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.protocols.presence_connection_manager.aggregates.enums import SdkRevisionType 2 | 3 | 4 | class SdkRevision: 5 | def __init__(self, sdk_type: SdkRevisionType): 6 | assert isinstance(sdk_type, SdkRevisionType) 7 | self.sdk_type = sdk_type 8 | 9 | @property 10 | def is_sdk_revision_valid(self): 11 | return False if self.sdk_type == 0 else True 12 | 13 | @property 14 | def is_support_gpi_new_auth_notification(self): 15 | return ( 16 | True 17 | if (self.sdk_type ^ SdkRevisionType.GPINEW_AUTH_NOTIFICATION) != 0 18 | else False 19 | ) 20 | 21 | @property 22 | def is_support_gpi_new_revoke_notification(self): 23 | return ( 24 | True 25 | if (self.sdk_type ^ SdkRevisionType.GPINEW_REVOKE_NOTIFICATION) != 0 26 | else False 27 | ) 28 | 29 | @property 30 | def is_support_gpi_new_status_notification(self): 31 | return ( 32 | True 33 | if (self.sdk_type ^ SdkRevisionType.GPINEW_STATUS_NOTIFICATION) != 0 34 | else False 35 | ) 36 | 37 | @property 38 | def is_support_gpi_new_list_retreval_on_login(self): 39 | return ( 40 | True 41 | if (self.sdk_type ^ SdkRevisionType.GPINEW_LIST_RETRIEVAL_ON_LOGIN) != 0 42 | else False 43 | ) 44 | 45 | @property 46 | def is_support_gpi_remote_auth_ids_notification(self): 47 | return ( 48 | True 49 | if (self.sdk_type ^ SdkRevisionType.GPIREMOTE_AUTH_IDS_NOTIFICATION) != 0 50 | else False 51 | ) 52 | 53 | @property 54 | def is_support_gpi_new_cdkey_registration(self): 55 | return ( 56 | True 57 | if (self.sdk_type ^ SdkRevisionType.GPINEW_CD_KEY_REGISTRATION) != 0 58 | else False 59 | ) 60 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/presence_search_player/contracts/results.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | from frontends.gamespy.protocols.presence_search_player.abstractions.contracts import ResultBase 4 | 5 | 6 | class CheckResult(ResultBase): 7 | profile_id: int 8 | 9 | 10 | class NewUserResult(ResultBase): 11 | user_id: int 12 | profile_id: int 13 | 14 | 15 | class NickResultData(BaseModel): 16 | nick: str 17 | uniquenick: str 18 | 19 | 20 | class NicksResult(ResultBase): 21 | data: list[NickResultData] 22 | """ [ 23 | (nick1, uniquenick1), 24 | (nick2, uniquenick2), 25 | (nick3, uniquenick3), 26 | ... 27 | ] 28 | """ 29 | is_require_uniquenicks: bool = False 30 | 31 | 32 | class OthersListData(BaseModel): 33 | profile_id: int 34 | unique_nick: str 35 | 36 | 37 | class OthersListResult(ResultBase): 38 | data: list[OthersListData] = [] 39 | """ 40 | [ 41 | (prifileid1,uniquenick1), 42 | (prifileid2,uniquenick2), 43 | (prifileid3,uniquenick3), 44 | ... 45 | ] 46 | """ 47 | 48 | 49 | class OthersResultData(BaseModel): 50 | profile_id: int 51 | nick: str 52 | uniquenick: str 53 | lastname: str 54 | firstname: str 55 | user_id: int 56 | email: str 57 | 58 | 59 | class OthersResult(ResultBase): 60 | data: list[OthersResultData] = [] 61 | 62 | 63 | class SearchResultData(BaseModel): 64 | profile_id: int 65 | nick: str 66 | uniquenick: str 67 | email: str 68 | firstname: str 69 | lastname: str 70 | namespace_id: int 71 | 72 | 73 | class SearchResult(ResultBase): 74 | data: list[SearchResultData] 75 | 76 | 77 | class SearchUniqueResult(ResultBase): 78 | data: list[SearchResultData] 79 | 80 | 81 | class UniqueSearchResult(ResultBase): 82 | is_uniquenick_exist: bool 83 | preferred_nick: str 84 | 85 | 86 | class ValidResult(ResultBase): 87 | is_account_valid: bool = False 88 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/natneg/contracts/responses.py: -------------------------------------------------------------------------------- 1 | import socket 2 | from frontends.gamespy.protocols.natneg.abstractions.contracts import ( 3 | CommonResponseBase, 4 | ResponseBase, 5 | ) 6 | from frontends.gamespy.protocols.natneg.contracts.results import ( 7 | AddressCheckResult, 8 | ConnectResult, 9 | ErtAckResult, 10 | InitResult, 11 | NatifyResult, 12 | ) 13 | 14 | 15 | class InitResponse(CommonResponseBase): 16 | _result: InitResult 17 | 18 | def __init__(self, result: InitResult) -> None: 19 | assert isinstance(result, InitResult) 20 | super().__init__(result) 21 | 22 | 23 | class ErcAckResponse(InitResponse): 24 | _result: ErtAckResult 25 | 26 | def __init__(self, result: ErtAckResult) -> None: 27 | assert isinstance(result, ErtAckResult) 28 | self._result = result 29 | 30 | 31 | class NatifyResponse(CommonResponseBase): 32 | _result: NatifyResult 33 | 34 | def __init__(self, result: NatifyResult) -> None: 35 | assert isinstance(result, NatifyResult) 36 | super().__init__(result) 37 | 38 | 39 | class AddressCheckResponse(CommonResponseBase): 40 | _result: AddressCheckResult 41 | 42 | def __init__( 43 | self, result: AddressCheckResult 44 | ) -> None: 45 | assert isinstance(result, AddressCheckResult) 46 | super().__init__(result) 47 | 48 | 49 | class ConnectResponse(ResponseBase): 50 | _result: ConnectResult 51 | 52 | def build(self) -> None: 53 | assert self._result.ip is not None 54 | assert self._result.port is not None 55 | assert self._result.status is not None 56 | super().build() 57 | data = bytearray() 58 | data.extend(self.sending_buffer) 59 | data.extend(socket.inet_aton(self._result.ip)) 60 | data.extend(self._result.port.to_bytes(2)) 61 | # got your data 62 | data.append(1) 63 | data.append(self._result.status.value) 64 | self.sending_buffer = bytes(data) 65 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/query_report/aggregates/natneg_channel.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | from frontends.gamespy.library.abstractions.brocker import BrockerBase 3 | from frontends.gamespy.library.configs import CONFIG 4 | from frontends.gamespy.library.log.log_manager import GLOBAL_LOGGER 5 | from frontends.gamespy.library.network.websocket_brocker import WebSocketBrocker 6 | from frontends.gamespy.protocols.query_report.v2.applications.handlers import ClientMessageHandler 7 | from frontends.gamespy.protocols.query_report.v2.contracts.requests import ClientMessageRequest 8 | from types import MappingProxyType 9 | 10 | if TYPE_CHECKING: 11 | from frontends.gamespy.protocols.query_report.applications.client import Client 12 | 13 | 14 | class NatNegChannel: 15 | """ 16 | todo send data to the ip endpoint when receive data, not find client from pool to save memory 17 | """ 18 | broker: BrockerBase 19 | pool: MappingProxyType[str, "Client"] 20 | 21 | def __init__(self, broker_cls: type[BrockerBase] = WebSocketBrocker) -> None: 22 | self.broker = broker_cls( 23 | "natneg", f"{CONFIG.backend.url}/QueryReport/Channel", self.recieve_message) 24 | self.pool = MappingProxyType(Client.pool) 25 | 26 | def recieve_message(self, request: bytes): 27 | message = ClientMessageRequest(request) 28 | message.parse() 29 | client = None 30 | if message.target_ip_endpoint in self.pool: 31 | client = self.pool[message.target_ip_endpoint] 32 | 33 | if client is None: 34 | GLOBAL_LOGGER.warn(f"Client:{message.target_ip_address}:{ 35 | message.target_port} not found, we ignore natneg message from SB: {message.server_browser_sender_id}") 36 | return 37 | 38 | GLOBAL_LOGGER.info(f"Get client message from server browser: { 39 | message.server_browser_sender_id} [{message.natneg_message}]") 40 | handler = ClientMessageHandler(client, message) 41 | handler.handle() 42 | -------------------------------------------------------------------------------- /src/frontends/tests/gamespy/presence_connection_manager/mock_objects.py: -------------------------------------------------------------------------------- 1 | from typing import cast 2 | from frontends.gamespy.library.configs import CONFIG 3 | from frontends.tests.gamespy.library.mock_objects import ( 4 | ConnectionMock, 5 | LogMock, 6 | RequestHandlerMock, 7 | create_mock_url, 8 | ) 9 | from frontends.gamespy.protocols.presence_connection_manager.applications.client import ( 10 | Client, 11 | ) 12 | from frontends.gamespy.protocols.presence_connection_manager.applications.handlers import ( 13 | LoginHandler, 14 | NewUserHandler, 15 | ) 16 | 17 | 18 | class ClientMock(Client): 19 | pass 20 | 21 | 22 | def create_client() -> Client: 23 | CONFIG.unittest.is_raise_except = True 24 | handler = RequestHandlerMock() 25 | logger = LogMock() 26 | conn = ConnectionMock( 27 | handler=handler, 28 | config=CONFIG.servers["PresenceConnectionManager"], 29 | t_client=ClientMock, 30 | logger=logger, 31 | ) 32 | config = CONFIG.servers["PresenceConnectionManager"] 33 | create_mock_url(config, NewUserHandler, { 34 | "user_id": 0, "profile_id": 0, "operation_id": 0}) 35 | create_mock_url( 36 | config, 37 | LoginHandler, 38 | { 39 | "response_proof": "7f2c9c6685570ea18b7207d2cbd72452", 40 | "data": { 41 | "user_id": 0, 42 | "profile_id": 0, 43 | "nick": "test", 44 | "email": "test@gamespy.com", 45 | "unique_nick": "test_unique", 46 | "password_hash": "password", 47 | "email_verified_flag": True, 48 | "namespace_id": 0, 49 | "sub_profile_id": 0, 50 | "banned_flag": False, 51 | }, 52 | "operation_id": 0, 53 | "user_data": "", 54 | "type": 0, 55 | "partner_id": 0, 56 | "user_challenge": "xMsHUXuWNXL3KMwmhoQZJrP0RVsArCYT" 57 | }, 58 | ) 59 | 60 | return cast(Client, conn._client) 61 | -------------------------------------------------------------------------------- /src/backends/protocols/gamespy/presence_search_player/requests.py: -------------------------------------------------------------------------------- 1 | 2 | from backends.library.abstractions.contracts import RequestBase as RB 3 | from frontends.gamespy.protocols.presence_search_player.aggregates.enums import SearchType 4 | 5 | 6 | class RequestBase(RB): 7 | operation_id: int 8 | 9 | # general 10 | 11 | # we just need to recreate the requests and just put the property inside it. The result we can use the results inside servers. 12 | 13 | 14 | class CheckRequest(RequestBase): 15 | nick: str 16 | password: str 17 | email: str 18 | partner_id: int 19 | 20 | 21 | class NewUserRequest(RequestBase): 22 | nick: str 23 | email: str 24 | password: str 25 | uniquenick: str 26 | namespace_id: int 27 | product_id: int 28 | game_port: int | None = None 29 | cd_key: str | None = None 30 | partner_id: int | None = None 31 | game_name: str | None = None 32 | 33 | 34 | class NicksRequest(RequestBase): 35 | password: str 36 | email: str 37 | namespace_id: int 38 | is_require_uniquenicks: bool 39 | 40 | 41 | class OthersListRequest(RequestBase): 42 | profile_ids: list[int] 43 | namespace_id: int 44 | 45 | 46 | class OthersRequest(RequestBase): 47 | profile_id: int 48 | game_name: str 49 | namespace_id: int 50 | 51 | 52 | class SearchRequest(RequestBase): 53 | skip_num: int 54 | request_type: SearchType 55 | game_name: str 56 | profile_id: int | None = None 57 | partner_id: int 58 | email: str | None = None 59 | nick: str | None = None 60 | uniquenick: str | None = None 61 | session_key: str | None = None 62 | firstname: str | None = None 63 | lastname: str | None = None 64 | icquin: str | None = None 65 | namespace_id: int 66 | 67 | 68 | class SearchUniqueRequest(RequestBase): 69 | uniquenick: str 70 | namespace_ids: list[int] 71 | 72 | 73 | class UniqueSearchRequest(RequestBase): 74 | preferred_nick: str 75 | game_name: str 76 | namespace_id: int 77 | 78 | 79 | class ValidRequest(RequestBase): 80 | email: str 81 | -------------------------------------------------------------------------------- /src/frontends/tests/gamespy/natneg/mock_objects.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.protocols.natneg.contracts.results import AddressCheckResult, ConnectResult, InitResult 2 | from frontends.tests.gamespy.library.mock_objects import ( 3 | ConnectionMock, 4 | LogMock, 5 | RequestHandlerMock, 6 | create_mock_url, 7 | ) 8 | from frontends.gamespy.library.configs import CONFIG 9 | from frontends.gamespy.protocols.natneg.applications.client import Client 10 | from typing import cast 11 | 12 | from frontends.gamespy.protocols.natneg.applications.handlers import ( 13 | AddressCheckHandler, 14 | ConnectHandler, 15 | ErtAckHandler, 16 | InitHandler, 17 | NatifyHandler, 18 | ) 19 | 20 | 21 | class ClientMock(Client): 22 | pass 23 | 24 | 25 | def create_client() -> Client: 26 | CONFIG.unittest.is_raise_except = True 27 | handler = RequestHandlerMock() 28 | logger = LogMock() 29 | conn = ConnectionMock( 30 | handler=handler, 31 | config=CONFIG.servers["NatNegotiation"], 32 | t_client=ClientMock, 33 | logger=logger, 34 | ) 35 | 36 | config = CONFIG.servers["NatNegotiation"] 37 | create_mock_url(config, InitHandler, InitResult.model_validate( 38 | {"version": 3, "cookie": 777, "public_ip_addr": "127.0.0.1", "public_port": 1234, "use_game_port": False, "port_type": 1, "client_index": 0, "use_game_port": 0}).model_dump(mode="json")) 39 | create_mock_url(config, AddressCheckHandler, AddressCheckResult.model_validate( 40 | {"version": 3, "cookie": 0, "public_ip_addr": "127.0.0.1", "public_port": 1234, "use_game_port": False, "port_type": 0, "client_index": 0, "use_game_port": 0}).model_dump(mode="json")) 41 | create_mock_url(config, NatifyHandler, {"message": "ok"}) 42 | create_mock_url(config, ErtAckHandler, {"message": "ok"}) 43 | create_mock_url(config, ConnectHandler, ConnectResult.model_validate( 44 | {"version": 3, "cookie": 0, "is_both_client_ready": True, "ip": "192.168.0.1", "port": 7890, "status": 0}).model_dump(mode="json")) 45 | return cast(Client, conn._client) 46 | -------------------------------------------------------------------------------- /src/backends/protocols/gamespy/game_traffic_relay/data.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | 3 | from uuid import UUID 4 | from backends.library.database.pg_orm import RelayServerCaches 5 | from sqlalchemy.orm import Session 6 | 7 | 8 | def search_relay_server( 9 | server_id: UUID, server_ip: str, session: Session 10 | ) -> RelayServerCaches | None: 11 | result = ( 12 | session.query(RelayServerCaches) 13 | .where( 14 | RelayServerCaches.server_id == server_id, 15 | RelayServerCaches.public_ip == server_ip, 16 | ) 17 | .first() 18 | ) 19 | return result 20 | 21 | 22 | def get_available_relay_serves(session: Session) -> list[RelayServerCaches]: 23 | """ 24 | Return 25 | ------ 26 | list of ip:port 27 | """ 28 | 29 | result: list[RelayServerCaches] = session.query(RelayServerCaches).all() 30 | return result 31 | 32 | 33 | def update_relay_server(info: RelayServerCaches, session: Session): 34 | info.update_time = datetime.now() # type: ignore 35 | 36 | session.commit() 37 | 38 | 39 | def create_relay_server(info: RelayServerCaches, session: Session): 40 | session.add(info) 41 | session.commit() 42 | 43 | 44 | def check_expired_server(session: Session): 45 | expire_time = datetime.now()-timedelta(seconds=30) 46 | session.query( 47 | RelayServerCaches 48 | ).where( 49 | RelayServerCaches.update_time < expire_time 50 | ).delete() 51 | session.commit() 52 | 53 | 54 | def delete_relay_server(server_id: UUID, ip_address: str, port: int, session: Session): 55 | assert isinstance(server_id, UUID) 56 | assert isinstance(ip_address, str) 57 | assert isinstance(port, int) 58 | 59 | info = ( 60 | session.query(RelayServerCaches) 61 | .where( 62 | RelayServerCaches.server_id == server_id, 63 | RelayServerCaches.public_ip == ip_address, 64 | RelayServerCaches.public_port == port, 65 | ) 66 | .first() 67 | ) 68 | session.delete(info) 69 | session.commit() 70 | -------------------------------------------------------------------------------- /src/backends/protocols/gamespy/chat/response.py: -------------------------------------------------------------------------------- 1 | from backends.library.abstractions.contracts import DataResponse 2 | from frontends.gamespy.protocols.chat.contracts.results import AtmResult, CryptResult, GetCKeyResult, GetChannelKeyResult, GetKeyResult, JoinResult, KickResult, ListResult, ModeResult, NamesResult, NickResult, NoticeResult, PartResult, PingResult, PrivateResult, SetCKeyResult, SetChannelKeyResult, TopicResult, UtmResult, WhoIsResult, WhoResult 3 | 4 | 5 | class PingResponse(DataResponse): 6 | result: PingResult 7 | 8 | 9 | class CryptResponse(DataResponse): 10 | result: CryptResult 11 | 12 | 13 | class GetKeyResponse(DataResponse): 14 | result: GetKeyResult 15 | 16 | 17 | class ListResponse(DataResponse): 18 | result: ListResult 19 | 20 | 21 | class NicksResponse(DataResponse): 22 | result: NickResult 23 | 24 | 25 | class WhoResponse(DataResponse): 26 | result: WhoResult 27 | 28 | 29 | class WhoIsResponse(DataResponse): 30 | result: WhoIsResult 31 | 32 | 33 | class JoinResponse(DataResponse): 34 | result: JoinResult 35 | 36 | 37 | class GetChannelKeyResponse(DataResponse): 38 | result: GetChannelKeyResult 39 | 40 | 41 | class GetCkeyResponse(DataResponse): 42 | result: GetCKeyResult 43 | 44 | 45 | class KickResponse(DataResponse): 46 | result: KickResult 47 | 48 | 49 | class ModeResponse(DataResponse): 50 | result: ModeResult 51 | 52 | 53 | class NamesResponse(DataResponse): 54 | result: NamesResult 55 | 56 | 57 | class PartResponse(DataResponse): 58 | result: PartResult 59 | 60 | 61 | class SetChannelKeyResponse(DataResponse): 62 | result: SetChannelKeyResult 63 | 64 | 65 | class SetCKeyResponse(DataResponse): 66 | result: SetCKeyResult 67 | 68 | 69 | class TopicResponse(DataResponse): 70 | result: TopicResult 71 | 72 | 73 | class AtmResponse(DataResponse): 74 | result: AtmResult 75 | 76 | 77 | class UtmResponse(DataResponse): 78 | result: UtmResult 79 | 80 | 81 | class NoticeResponse(DataResponse): 82 | result: NoticeResult 83 | 84 | 85 | class PrivateResponse(DataResponse): 86 | result: PrivateResult 87 | 88 | -------------------------------------------------------------------------------- /src/frontends/tests/gamespy/presence_connection_manager/game_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from frontends.gamespy.protocols.presence_connection_manager.contracts.requests import StatusRequest 4 | from frontends.tests.gamespy.presence_connection_manager.mock_objects import create_client 5 | import responses 6 | 7 | 8 | class GameTest(unittest.TestCase): 9 | @responses.activate 10 | def test_civilization_4(self) -> None: 11 | raw_requests = [ 12 | "\\newuser\\\\email\\civ4@unispy.org\\nick\\civ4-tk\\passwordenc\\JMHGwQ__\\productid\\10435\\gamename\\civ4\\namespaceid\\17\\uniquenick\\civ4-tk\\id\\1\\final\\", 13 | "\\login\\\\challenge\\xMsHUXuWNXL3KMwmhoQZJrP0RVsArCYT\\uniquenick\\civ4-tk\\userid\\25\\profileid\\26\\response\\7f2c9c6685570ea18b7207d2cbd72452\\firewall\\1\\port\\0\\productid\\10435\\gamename\\civ4\\namespaceid\\17\\sdkrevision\\1\\id\\1\\final\\", 14 | ] 15 | client = create_client() 16 | 17 | 18 | for x in raw_requests: 19 | client.on_received(x.encode()) 20 | pass 21 | 22 | @unittest.skip("not finished handler") 23 | @responses.activate 24 | def test_conflict_global_storm(self) -> None: 25 | # "\\lc\\1\\challenge\\NRNUJLZMLX\\id\\1\\final\\", 26 | raw_requests = [ 27 | "\\login\\\\challenge\\KMylyQbZfqzKn9otxx32q4076sOUnKif\\user\\cgs1@cgs1@rs.de\\response\\c1a6638bbcfe130e4287bfe4aa792949\\port\\-15737\\productid\\10469\\gamename\\conflictsopc\\namespaceid\\1\\id\\1\\final\\", 28 | "\\inviteto\\\\sesskey\\58366\\products\\1038\\final\\", 29 | ] 30 | client = create_client() 31 | for x in raw_requests: 32 | client.on_received(x.encode("ascii")) 33 | pass 34 | 35 | def test_sbwfrontps2(self) -> None: 36 | raw = "\\status\\1\\sesskey\\1111\\statstring\\EN LIGNE\\locstring\\\\final\\" 37 | request = StatusRequest(raw) 38 | request.parse() 39 | self.assertTrue(request.location_string == "") 40 | self.assertTrue(request.status_string == "EN LIGNE") 41 | 42 | 43 | if __name__ == "__main__": 44 | unittest.main() 45 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/presence_connection_manager/contracts/results.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | from frontends.gamespy.protocols.presence_connection_manager.abstractions.contracts import ResultBase 3 | from frontends.gamespy.protocols.presence_connection_manager.aggregates.enums import GPStatusCode, LoginType 4 | 5 | # region General 6 | 7 | 8 | class LoginData(BaseModel): 9 | user_id: int 10 | profile_id: int 11 | sub_profile_id: int 12 | nick: str 13 | email: str 14 | unique_nick: str 15 | password_hash: str 16 | email_verified_flag: bool 17 | banned_flag: bool 18 | namespace_id: int 19 | 20 | 21 | class LoginResult(ResultBase): 22 | data: LoginData 23 | user_data: str 24 | type: LoginType 25 | partner_id: int 26 | user_challenge: str 27 | 28 | 29 | class NewUserResult(ResultBase): 30 | user_id: int 31 | profile_id: int 32 | 33 | 34 | # region Buddy 35 | 36 | 37 | class AddBuddyResult(ResultBase): 38 | pass 39 | 40 | 41 | class BlockListResult(ResultBase): 42 | profile_ids: list[int] 43 | operation_id: int 44 | 45 | 46 | class BuddyListResult(ResultBase): 47 | profile_ids: list[int] 48 | 49 | 50 | class StatusInfoResult(ResultBase): 51 | profile_id: int 52 | product_id: int 53 | status_state: str 54 | buddy_ip: str 55 | host_ip: str 56 | host_private_ip: str 57 | query_report_port: int 58 | host_port: int 59 | session_flags: str 60 | rich_status: str 61 | game_type: str 62 | game_variant: str 63 | game_map_name: str 64 | quiet_mode_flags: str 65 | 66 | 67 | class StatusResult(ResultBase): 68 | status_string: str 69 | location_string: str 70 | current_status: GPStatusCode 71 | 72 | # class NewUserResult() 73 | 74 | 75 | # region Profile 76 | 77 | class GetProfileData(BaseModel): 78 | nick: str 79 | profile_id: int 80 | unique_nick: str 81 | email: str 82 | extra_infos: dict 83 | 84 | 85 | class GetProfileResult(ResultBase): 86 | user_profile: GetProfileData 87 | 88 | 89 | class NewProfileResult(ResultBase): 90 | profile_id: int 91 | 92 | 93 | class RegisterNickResult(ResultBase): 94 | pass 95 | -------------------------------------------------------------------------------- /src/frontends/tests/gamespy/game_status/game_tests.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import unittest 4 | 5 | import responses 6 | 7 | from frontends.tests.gamespy.game_status.mock_objects import create_client 8 | 9 | 10 | class GameTest(unittest.TestCase): 11 | @responses.activate 12 | def test_worm3d_20230331(self): 13 | raws1 = [ 14 | "\\auth\\\\gamename\\worms3\\response\\bc3ca727a7825879eb9f13d9fd51bbb9\\port\\0\\id\\1\\final\\", 15 | "\\newgame\\\\connid\\0\\sesskey\\144562\\final\\", 16 | "\\authp\\\\pid\\1\\resp\\7b6658e99f448388fbeddc93654e6dd4\\lid\\2\\final\\", 17 | "\\setpd\\\\pid\\1\\ptype\\1\\dindex\\0\\kv\\1\\lid\\2\\length\\111\\data\\\\report\\|title||victories|0|timestamp|66613|league|Team17|winner||crc|-1|player_0|spyguy|ip_0||pid_0|0|auth_0|[00]\\final\\", 18 | ] 19 | client = create_client() 20 | for raw in raws1: 21 | client.on_received(raw.encode()) 22 | 23 | @responses.activate 24 | def test_gmtest(self): 25 | raws = [ 26 | "\\auth\\\\gamename\\crysis2\\response\\xxxxx\\port\\30\\id\\1\\final\\", 27 | "\\getpd\\\\pid\\0\\ptype\\0\\dindex\\1\\keys\\hello\x01hi\\lid\\1\\final\\", 28 | "\\getpid\\\\nick\\xiaojiuwo\\keyhash\\00000\\lid\\1\\final\\", 29 | "\\newgame\\\\connid\\123\\sesskey\\123456\\lid\\1\\final\\", 30 | "\\newgame\\\\connid\\123\\sesskey\\2020\\lid\\1\\final\\", 31 | "\\newgame\\\\connid\\123\\sesskey\\123456\\challenge\\123456789\\lid\\1\\final\\", 32 | "\\newgame\\\\connid\\123\\sesskey\\2020\\challenge\\123456789\\lid\\1\\final\\", 33 | "\\setpd\\\\pid\\123\\ptype\\0\\dindex\\1\\kv\\1\\lid\\1\\length\\5\\data\\11\\lid\\1\\final\\", 34 | "\\updgame\\\\sesskey\\0\\done\\1\\gamedata\\hello\\lid\\1\\final\\", 35 | "\\updgame\\\\sesskey\\2020\\done\\1\\gamedata\\hello\\lid\\1\\final\\", 36 | "\\updgame\\\\sesskey\\2020\\connid\\1\\done\\1\\gamedata\\hello\\lid\\1\\final\\", 37 | "\\updgame\\\\sesskey\\0\\connid\\1\\done\\1\\gamedata\\hello\\lid\\1\\final\\"] 38 | client = create_client() 39 | for raw in raws: 40 | client.on_received(raw.encode()) 41 | -------------------------------------------------------------------------------- /src/frontends/gamespy/library/abstractions/switcher.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from frontends.gamespy.library.abstractions.client import ClientBase 3 | from frontends.gamespy.library.abstractions.handler import CmdHandlerBase 4 | 5 | 6 | class SwitcherBase: 7 | """ 8 | class member type hint can use class static member, but you can not initialize any class static member here! Init it in the __init__() function 9 | """ 10 | _handlers: list[CmdHandlerBase] 11 | _requests: list[tuple[object, object]] 12 | _raw_request: object 13 | 14 | def __init__(self, client: ClientBase, raw_request: bytes | str) -> None: 15 | assert isinstance(client, ClientBase) 16 | assert isinstance(raw_request, str) or isinstance(raw_request, bytes) 17 | self._client: ClientBase = client 18 | self._raw_request: object = raw_request 19 | self._handlers: list[CmdHandlerBase] = [] 20 | self._requests: list[tuple[object, object]] = [] 21 | """ 22 | [ 23 | (request_name,raw_request), 24 | (request_name,raw_request), 25 | (request_name,raw_request), 26 | ... 27 | ] 28 | 29 | """ 30 | 31 | def handle(self): 32 | from frontends.gamespy.library.exceptions.general import UniSpyException 33 | 34 | try: 35 | self._process_raw_request() 36 | if len(self._requests) == 0: 37 | return 38 | for request in self._requests: 39 | handler = self._create_cmd_handlers(request[0], request[1]) 40 | if handler is None: 41 | self._client.log_warn( 42 | f"Request: <{request[0]}> is ignored.") 43 | continue 44 | self._handlers.append(handler) 45 | if len(self._handlers) == 0: 46 | return 47 | 48 | for handler in self._handlers: 49 | handler.handle() 50 | except Exception as e: 51 | UniSpyException.handle_exception(e, self._client) 52 | 53 | @abstractmethod 54 | def _process_raw_request(self) -> None: 55 | pass 56 | 57 | @abstractmethod 58 | def _create_cmd_handlers(self, name: object, raw_request: object) -> CmdHandlerBase | None: 59 | pass 60 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/game_traffic_relay/applications/switcher.py: -------------------------------------------------------------------------------- 1 | 2 | from frontends.gamespy.library.abstractions.handler import CmdHandlerBase 3 | from frontends.gamespy.library.abstractions.switcher import SwitcherBase 4 | from frontends.gamespy.library.exceptions.general import UniSpyException 5 | from frontends.gamespy.protocols.game_traffic_relay.applications.client import Client 6 | from frontends.gamespy.protocols.game_traffic_relay.applications.connection import ConnectStatus, ConnectionListener 7 | from frontends.gamespy.protocols.game_traffic_relay.applications.handlers import ( 8 | MessageRelayHandler, 9 | PingHandler) 10 | from frontends.gamespy.protocols.game_traffic_relay.contracts.general import MessageRelayRequest 11 | from frontends.gamespy.protocols.natneg.aggregations.enums import RequestType 12 | from frontends.gamespy.protocols.natneg.contracts.requests import PingRequest 13 | 14 | 15 | class Switcher(SwitcherBase): 16 | _raw_request: bytes 17 | _client: Client 18 | 19 | def __init__(self, client: Client, raw_request: bytes) -> None: 20 | super().__init__(client, raw_request) 21 | assert issubclass(type(client), Client) 22 | assert isinstance(raw_request, bytes) 23 | 24 | def _process_raw_request(self) -> None: 25 | name = self._raw_request[7] 26 | if name == RequestType.PING.value: 27 | self._requests.append((RequestType.PING, self._raw_request)) 28 | else: 29 | self._requests.append((RequestType.RELAY_MSG, self._raw_request)) 30 | 31 | def _create_cmd_handlers( 32 | self, name: RequestType, raw_request: bytes 33 | ) -> CmdHandlerBase: 34 | assert isinstance(name, RequestType) 35 | assert isinstance(raw_request, bytes) 36 | saved_client = ConnectionListener.get_client_by_ip( 37 | self._client.connection.ip_endpoint) 38 | if saved_client is None: 39 | client = self._client 40 | else: 41 | client = saved_client 42 | match name: 43 | case RequestType.PING: 44 | return PingHandler(client, PingRequest(raw_request)) 45 | case RequestType.RELAY_MSG: 46 | return MessageRelayHandler(client, MessageRelayRequest(raw_request)) 47 | case _: 48 | raise UniSpyException("unable to handle the message") 49 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/presence_connection_manager/applications/client.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.library.abstractions.client import ClientBase, ClientInfoBase 2 | 3 | from frontends.gamespy.library.abstractions.switcher import SwitcherBase 4 | from frontends.gamespy.library.log.log_manager import LogWriter 5 | from frontends.gamespy.library.network.tcp_handler import TcpConnection 6 | from frontends.gamespy.library.configs import ServerConfig 7 | from frontends.gamespy.protocols.presence_connection_manager.aggregates.login_challenge import ( 8 | SERVER_CHALLENGE, 9 | ) 10 | from frontends.gamespy.protocols.presence_connection_manager.aggregates.enums import LoginStatus, SdkRevisionType 11 | 12 | LOGIN_TICKET = "0000000000000000000000__" 13 | SESSION_KEY = 1111 14 | 15 | 16 | class ClientInfo(ClientInfoBase): 17 | user_id: int 18 | profile_id: int 19 | sub_profile_id: int 20 | login_status: LoginStatus 21 | namespace_id: int 22 | sdk_revision: list[SdkRevisionType] 23 | 24 | def __init__(self) -> None: 25 | super().__init__() 26 | self.login_status = LoginStatus.CONNECTED 27 | 28 | 29 | class Client(ClientBase): 30 | info: ClientInfo 31 | client_pool: dict[str, "Client"] = {} 32 | connection: TcpConnection 33 | 34 | def __init__( 35 | self, 36 | connection: TcpConnection, 37 | server_config: ServerConfig, 38 | logger: LogWriter, 39 | ): 40 | super().__init__(connection, server_config, logger) 41 | self.info = ClientInfo() 42 | 43 | def on_connected(self) -> None: 44 | super().on_connected() 45 | if self.info.login_status != LoginStatus.CONNECTED: 46 | self.connection.disconnect() 47 | self.log_warn( 48 | "The server challenge has already been sent. Cannot send another login challenge." 49 | ) 50 | self.info.login_status = LoginStatus.PROCESSING 51 | buffer = f"\\lc\\1\\challenge\\{SERVER_CHALLENGE}\\id\\1\\final\\".encode( 52 | "ascii" 53 | ) 54 | self.log_network_sending(buffer) 55 | self.connection.send(buffer) 56 | 57 | def _create_switcher(self, buffer: bytes) -> SwitcherBase: 58 | from frontends.gamespy.protocols.presence_connection_manager.applications.switcher import Switcher 59 | return Switcher(self, buffer.decode()) 60 | -------------------------------------------------------------------------------- /src/frontends/gamespy/library/extentions/gamespy_utils.py: -------------------------------------------------------------------------------- 1 | from email_validator import validate_email, EmailNotValidError 2 | 3 | 4 | def is_email_format_correct(email: str) -> bool: 5 | assert isinstance(email, str) 6 | try: 7 | validate_email(email, check_deliverability=False) 8 | 9 | except EmailNotValidError: 10 | return False 11 | 12 | return True 13 | 14 | 15 | def convert_to_key_value(request: str) -> dict: 16 | assert isinstance(request, str) 17 | command_parts = request.replace("\\final\\", "").lstrip("\\").split("\\") 18 | 19 | parts = [part for part in command_parts if part != "final"] 20 | dict = {} 21 | try: 22 | for i in range(0, len(parts), 2): 23 | if parts[i] not in dict: 24 | dict[parts[i].lower()] = parts[i + 1] 25 | # Some game send uppercase key to us, so we have to deal with it 26 | except IndexError: 27 | pass 28 | return dict 29 | 30 | 31 | def is_valid_date(day: int, month: int, year: int) -> bool: 32 | # Check for a blank. 33 | if (day, month, year) == (0, 0, 0): 34 | return False 35 | 36 | # Validate the day of the month. 37 | match month: 38 | case 0: 39 | # Can't specify a day without a month. 40 | if day != 0: 41 | return False 42 | case 1, 3, 5, 7, 8, 10, 12: 43 | # 31-day month. 44 | if day > 31: 45 | return False 46 | case 4, 6, 9, 11: 47 | # 30-day month. 48 | if day > 30: 49 | return False 50 | case 2: 51 | # 28/29-day month. 52 | # Leap year? 53 | if ((year % 4 == 0) and (year % 100 != 0)) or (year % 400 == 0): 54 | if day > 29: 55 | return False 56 | else: 57 | if day > 28: 58 | return False 59 | case _: 60 | # Invalid month. 61 | return False 62 | 63 | # Check that the date is in the valid range. 64 | if year < 1900 or year > 2079: 65 | return False 66 | elif year == 2079: 67 | match month: 68 | case 6 if day > 6: 69 | return False 70 | case _: 71 | if month > 6: 72 | return False 73 | 74 | return True 75 | 76 | 77 | -------------------------------------------------------------------------------- /src/frontends/tests/gamespy/server_browser/encrypt_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from frontends.gamespy.protocols.server_browser.v2.aggregations.encryption import Byte, EnctypeX 4 | 5 | 6 | class EncryptionTest(unittest.TestCase): 7 | def test_enctypex(self): 8 | """ 9 | test if enctypex param init correctness 10 | """ 11 | enc = EnctypeX("000000", "00000000") 12 | self.assertTrue(enc._enc_params.index_0.value == 250) 13 | self.assertTrue(enc._enc_params.index_1.value == 220) 14 | self.assertTrue(enc._enc_params.index_2.value == 245) 15 | self.assertTrue(enc._enc_params.index_3.value == 229) 16 | self.assertTrue(enc._enc_params.index_4.value == 49) 17 | register = b"\x7A\xFA\x64\xDC\xB9\xF5\xF6\xE5\x89\x84\x9D\x66\xD7\xEA\x8E\xD8\xD4\xC0\xA1\xA4\x67\xA9\xAA\xDF\xF0\x71\x99\xEC\x87\xFD\xD0\xA2\xF3\xB5\xE3\x01\xE7\x7D\xE9\xE2\xEB\x97\xF1\x6F\x70\xFC\xD6\xFB\x82\x95\xC1\xB1\xD9\xBF\xD1\xE0\x9E\x81\xE8\xBB\xE1\xF8\xAD\xDA\xDB\xE4\x65\xAE\xCE\xAB\xB4\xBD\xA5\xB6\xB8\xEE\xF4\x75\xBC\xC3\xC6\xB7\xC8\xC9\xBA\xA6\xC4\xC5\x85\x6B\x78\x6D\xC2\xA7\xCC\xCD\x62\x9F\xBE\xA8\xCB\xB0\xE6\xDD\x79\xA3\xCA\xF2\xCF\xAC\x6A\xEF\xFE\xED\xD5\xB2\xDE\xB3\x72\xF7\x6C\xF9\xC7\x39\xA0\xAF\x77\x35\x73\x74\x68\x76\x8A\x80\x6E\x63\x7B\x7C\x7F\x98\x9B\x88\x91\x94\x83\x93\x8D\x86\x9C\x9A\x7E\x92\x8B\x8C\xD2\x96\x8F\x90\xFF\x41\x32\x33\x34\x3D\x36\x37\x38\x49\x3A\x3B\x3C\x45\x3E\x3F\x40\x51\x42\x43\x44\x4D\x46\x47\x48\x59\x4A\x4B\x4C\x55\x4E\x4F\x50\x61\x52\x53\x54\x5D\x56\x57\x58\x69\x5A\x5B\x5C\xD3\x5E\x5F\x60\x00\x09\x02\x03\x04\x05\x06\x07\x08\x11\x0A\x0B\x0C\x0D\x0E\x0F\x10\x19\x12\x13\x14\x15\x16\x17\x18\x21\x1A\x1B\x1C\x1D\x1E\x1F\x20\x29\x22\x23\x24\x25\x26\x27\x28\x31\x2A\x2B\x2C\x2D\x2E\x2F\x30" 18 | temp_register = bytes([v.value for v in enc._enc_params.register]) 19 | self.assertTrue(temp_register == register) 20 | plain_text = bytes([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) 22 | cipher = [] 23 | for i in range(len(plain_text)): 24 | c = enc.byte_shift(Byte(plain_text[i])) 25 | cipher.append(c.value) 26 | b_cipher = bytes(cipher) 27 | correct_cipher = b"\x84\xF2\xF5\x20\xE8\x4D\x86\x67\xB6\xD3\xB3\xC0\x23\x0D\x40\x74\x67\xFE\x9C\x30" 28 | self.assertTrue(b_cipher == correct_cipher) 29 | pass 30 | -------------------------------------------------------------------------------- /src/backends/tests/gamespy/server_browser/filter_tests.py: -------------------------------------------------------------------------------- 1 | # 导入 Interpreter 2 | from asteval import Interpreter 3 | import re 4 | 5 | if __name__ == "__main__": 6 | # 步骤1:创建 Interpreter 实例 7 | aeval = Interpreter() 8 | 9 | # 步骤2:定义服务器数据字典列表 10 | server_data_list = [ 11 | { 12 | "natneg": "1", 13 | "gamever": "2.00", 14 | "gravity": 800, 15 | "mapname": "gmtmap2", 16 | "gamemode": "openplaying", 17 | "gamename": "gmtest2", 18 | "gametype": "arena", 19 | "hostname": "Server 1", 20 | "hostport": "25000", 21 | "localip0": "172.19.0.4", 22 | "numteams": 2, 23 | "teamplay": 1, 24 | "fraglimit": 0, 25 | "localport": 11111, 26 | "rankingon": 1, 27 | "timelimit": 40, 28 | "maxplayers": 32, 29 | "numplayers": 8, 30 | "statechanged": 1, 31 | }, 32 | { 33 | "natneg": "1", 34 | "gamever": "2.00", 35 | "gravity": 600, 36 | "mapname": "gmtmap2", 37 | "gamemode": "openplaying", 38 | "gamename": "gmtest2", 39 | "gametype": "arena", 40 | "hostname": "Server 2", 41 | "hostport": "25000", 42 | "localip0": "172.19.0.4", 43 | "numteams": 2, 44 | "teamplay": 1, 45 | "fraglimit": 0, 46 | "localport": 11111, 47 | "rankingon": 1, 48 | "timelimit": 40, 49 | "maxplayers": 32, 50 | "numplayers": 4, 51 | "statechanged": 1, 52 | } 53 | ] 54 | 55 | # 步骤3:定义条件 56 | condition = "gravity > 700 and numplayers > 5 and gamemode == 'openplaying'" 57 | 58 | # 步骤4:提取条件中的键 59 | pattern = r'(\w+)\s*([<>!=]+)\s*([\'\"]?)(.*?)\3' 60 | matches = re.findall(pattern, condition) 61 | keys = [match[0] for match in matches] 62 | print(f"提取的键: {keys}") 63 | 64 | # 步骤5:评估每个服务器数据字典的条件 65 | for server in server_data_list: 66 | # 更新上下文:动态加载提取的键 67 | for key in keys: 68 | if key in server: 69 | aeval.symtable[key] = server[key] 70 | result = aeval(condition) 71 | print(f"服务器 {server['hostname']} (Gravity: {server['gravity']}, Players: {server['numplayers']}): {result}") 72 | -------------------------------------------------------------------------------- /src/frontends/gamespy/library/network/udp_handler.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import socketserver 3 | 4 | from frontends.gamespy.library.abstractions.client import ClientBase 5 | from frontends.gamespy.library.abstractions.connections import ( 6 | ConnectionBase, 7 | NetworkServerBase, 8 | ) 9 | from frontends.gamespy.library.configs import CONFIG, ServerConfig 10 | from frontends.gamespy.library.log.log_manager import LogWriter 11 | 12 | 13 | class UdpConnection(ConnectionBase): 14 | def send(self, data) -> None: 15 | conn: socket.socket = self.handler.request[1] 16 | conn.sendto(data, self.handler.client_address) 17 | 18 | 19 | class UdpHandler(socketserver.BaseRequestHandler): 20 | request: tuple[bytes, socket.socket] 21 | conn: UdpConnection 22 | 23 | def handle(self) -> None: 24 | data = self.request[0] 25 | conn = UdpConnection(self, *self.server.unispy_params) # type: ignore 26 | conn.on_received(data) 27 | 28 | def send(self, data: bytes) -> None: 29 | conn: socket.socket = self.request[1] 30 | conn.sendto(data, self.client_address) 31 | 32 | 33 | class UdpServer(NetworkServerBase): 34 | def __init__( 35 | self, config: ServerConfig, t_client: type[ClientBase], logger: LogWriter 36 | ) -> None: 37 | super().__init__(config, t_client, logger) 38 | self._server = socketserver.ThreadingUDPServer( 39 | (self._config.listening_address, self._config.listening_port), 40 | UdpHandler, 41 | ) 42 | # inject the handler params to ThreadingUDPServer 43 | self._server.unispy_params = (self._config, self._client_cls, self._logger) # type: ignore 44 | 45 | def __exit__(self, *args): 46 | self._server.__exit__(*args) 47 | 48 | 49 | class TestClient(ClientBase): 50 | def _create_switcher(self, buffer) -> None: 51 | # return super().create_switcher(buffer) 52 | print(buffer) 53 | pass 54 | 55 | def on_connected(self) -> None: 56 | # return super().on_connected() 57 | print("connected!") 58 | pass 59 | 60 | 61 | if __name__ == "__main__": 62 | # create_udp_server(list(CONFIG.servers.values())[0], ClientBase) 63 | from frontends.tests.gamespy.library.mock_objects import LogMock 64 | 65 | s = UdpServer(list(CONFIG.servers.values())[0], TestClient, LogMock()) 66 | s.start() 67 | pass 68 | -------------------------------------------------------------------------------- /src/backends/tests/gamespy/natneg/handler_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from backends.protocols.gamespy.natneg.handlers import ConnectHandler, InitHandler, ReportHandler 3 | from backends.protocols.gamespy.natneg.requests import InitRequest, ConnectRequest, ReportRequest 4 | from backends.tests.utils import add_headers 5 | import frontends.gamespy.protocols.natneg.contracts.requests as fnt 6 | 7 | 8 | class HandlerTests(unittest.TestCase): 9 | 10 | @unittest.skip("") 11 | def test_report(self): 12 | req_dict = {"raw_request": "\\xfd\\xfc\u001efj\\xb2\u0004\r\u0000\u0000\u0002\\x9a\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0001\u0000\u0000\u0000gmtest\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", 13 | "version": 4, "command_name": 13, "cookie": 666, "port_type": 0, "client_index": 0, "use_game_port": False, "is_nat_success": False, "nat_type": 0, "mapping_scheme": 0, "game_name": "gmtest", "client_ip": "172.19.0.5", "server_id": "950b7638-a90d-469b-ac1f-861e63c8c613", "client_port": 36229} 14 | req = ReportRequest.model_validate(req_dict) 15 | handler = ReportHandler(req) 16 | handler.handle() 17 | pass 18 | 19 | def test_connect(self): 20 | raw = b'\xfd\xfc\x1efj\xb2\x03\x00\x00\x00\x03\t\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' 21 | r = fnt.ConnectRequest(raw) 22 | data = add_headers(r) 23 | request = ConnectRequest(**data) 24 | handler = ConnectHandler(request) 25 | handler.handle() 26 | pass 27 | 28 | def test_init(self): 29 | raw = b'\xfd\xfc\x1efj\xb2\x03\x00\x00\x00\x03\t\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' 30 | r = fnt.InitRequest(raw) 31 | data = add_headers(r) 32 | request = InitRequest(**data) 33 | handler = InitHandler(request) 34 | handler.handle() 35 | pass 36 | -------------------------------------------------------------------------------- /src/frontends/gamespy/library/extentions/password_encoder.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import base64 3 | 4 | from frontends.gamespy.library.exceptions.general import UniSpyException 5 | 6 | 7 | def process_password(request: dict): 8 | """process password in standard format and return the password""" 9 | assert isinstance(request, dict) 10 | if "passwordenc" in request: 11 | md5_password = get_md5_hash(decode(request["passwordenc"])) 12 | elif "passenc" in request: 13 | md5_password = get_md5_hash(request["passenc"]) 14 | elif "pass" in request: 15 | md5_password = get_md5_hash(request["pass"]) 16 | elif "password" in request: 17 | md5_password = get_md5_hash(request["password"]) 18 | else: 19 | raise UniSpyException("Can not find password field in request") 20 | return md5_password 21 | 22 | 23 | def encode(password: str): 24 | assert isinstance(password, str) 25 | password_bytes = password.encode("utf-8") 26 | pass_encoded = base64.b64encode(game_spy_encode_method(password_bytes)) 27 | pass_encoded = pass_encoded.decode("utf-8") 28 | pass_encoded = pass_encoded.replace("=", "_").replace("+", "[").replace("/", "]") 29 | return pass_encoded 30 | 31 | 32 | def decode(password: str): 33 | assert isinstance(password, str) 34 | password = password.replace("_", "=").replace("[", "+").replace("]", "/") 35 | password_bytes = base64.b64decode(password) 36 | return game_spy_encode_method(password_bytes).decode("utf-8") 37 | 38 | 39 | def game_spy_encode_method(password_bytes: bytes): 40 | assert isinstance(password_bytes, bytes) 41 | a = 0 42 | num = 0x79707367 # gamespy 43 | temp_data = list(password_bytes) 44 | for i in range(len(password_bytes)): 45 | num = game_spy_byte_shift(num) 46 | a = num % 0xFF 47 | temp_data[i] ^= a 48 | return bytes(temp_data) 49 | 50 | 51 | def game_spy_byte_shift(num): 52 | assert isinstance(num, int) 53 | c = (num >> 16) & 0xFFFF 54 | a = num & 0xFFFF 55 | 56 | c *= 0x41A7 57 | a *= 0x41A7 58 | a += (c & 0x7FFF) << 16 59 | 60 | if a < 0: 61 | a &= 0x7FFFFFFF 62 | a += 1 63 | 64 | a += c >> 15 65 | 66 | if a < 0: 67 | a &= 0x7FFFFFFF 68 | a += 1 69 | 70 | return a 71 | 72 | 73 | def get_md5_hash(data): 74 | isinstance(data, str) 75 | md5_hash = hashlib.md5() 76 | md5_hash.update(data.encode("utf-8")) 77 | return md5_hash.hexdigest() 78 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/web_services/modules/sake/abstractions/generals.py: -------------------------------------------------------------------------------- 1 | import frontends.gamespy.protocols.web_services.abstractions.handler as h 2 | import frontends.gamespy.protocols.web_services.abstractions.contracts as lib 3 | from frontends.gamespy.protocols.web_services.aggregations.soap_envelop import SoapEnvelop 4 | from frontends.gamespy.protocols.web_services.modules.sake.exceptions.general import SakeException 5 | import xml.etree.ElementTree as ET 6 | 7 | NAMESPACE = "http://gamespy.net/sake" 8 | 9 | 10 | class RequestBase(lib.RequestBase): 11 | game_id: int 12 | secret_key: str 13 | login_ticket: str 14 | table_id: str 15 | 16 | def parse(self) -> None: 17 | super().parse() 18 | game_id = self._content_element.find(f".//{{{NAMESPACE}}}gameid") 19 | if game_id is None or game_id.text is None: 20 | raise SakeException("gameid is missing from the request.") 21 | self.game_id = int(game_id.text) 22 | 23 | secret_key = self._content_element.find( 24 | f".//{{{NAMESPACE}}}secretKey") 25 | if secret_key is None or secret_key.text is None: 26 | raise SakeException("secretkey id is missing from the request.") 27 | self.secret_key = secret_key.text 28 | 29 | login_ticket = self._content_element.find( 30 | f".//{{{NAMESPACE}}}loginTicket") 31 | if login_ticket is None or login_ticket.text is None: 32 | raise SakeException("loginTicket is missing from the request.") 33 | self.login_ticket = login_ticket.text 34 | 35 | table_id = self._content_element.find( 36 | f".//{{{NAMESPACE}}}tableid") 37 | if table_id is None or table_id.text is None: 38 | raise SakeException("tableid is missing from the request.") 39 | self.table_id = table_id.text 40 | 41 | @staticmethod 42 | def remove_namespace(tree: ET.Element): 43 | tree.tag = tree.tag.split('}', 1)[-1] 44 | for elem in tree: 45 | # Remove the namespace by splitting the tag 46 | # Keep the part after the '}' 47 | elem.tag = elem.tag.split('}', 1)[-1] 48 | return tree 49 | 50 | 51 | class ResultBase(lib.ResultBase): 52 | pass 53 | 54 | 55 | class ResponseBase(lib.ResponseBase): 56 | def __init__(self, result: ResultBase) -> None: 57 | super().__init__(result) 58 | 59 | 60 | class CmdHandlerBase(h.CmdHandlerBase): 61 | _request: "RequestBase" 62 | _result: "ResultBase" 63 | -------------------------------------------------------------------------------- /src/frontends/tests/gamespy/game_status/mock_objects.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import cast 3 | from frontends.gamespy.library.configs import CONFIG 4 | from frontends.gamespy.protocols.game_status.applications.client import Client 5 | from frontends.gamespy.protocols.game_status.applications.handlers import ( 6 | AuthGameHandler, 7 | AuthPlayerHandler, 8 | GetPlayerDataHandler, 9 | GetProfileIdHandler, 10 | NewGameHandler, 11 | SetPlayerDataHandler, 12 | UpdateGameHandler, 13 | ) 14 | from frontends.gamespy.protocols.game_status.contracts.results import ( 15 | AuthGameResult, 16 | AuthPlayerResult, 17 | GetPlayerDataResult, 18 | GetProfileIdResult, 19 | ) 20 | from frontends.tests.gamespy.library.mock_objects import ( 21 | ConnectionMock, 22 | LogMock, 23 | RequestHandlerMock, 24 | create_mock_url, 25 | ) 26 | 27 | 28 | class ClientMock(Client): 29 | pass 30 | 31 | 32 | def create_client() -> Client: 33 | CONFIG.unittest.is_raise_except = True 34 | handler = RequestHandlerMock() 35 | logger = LogMock() 36 | conn = ConnectionMock( 37 | handler=handler, 38 | config=CONFIG.servers["GameStatus"], 39 | t_client=ClientMock, 40 | logger=logger, 41 | ) 42 | config = CONFIG.servers["GameStatus"] 43 | create_mock_url(config, SetPlayerDataHandler, {"message": "ok"}) 44 | create_mock_url( 45 | config, 46 | GetPlayerDataHandler, 47 | GetPlayerDataResult( 48 | data="\\key1\\value1\\key2\\value2\\key3\\value3", 49 | local_id=0, 50 | profile_id=0, 51 | modified=datetime.now() 52 | ).model_dump(mode="json"), 53 | ) 54 | create_mock_url( 55 | config, 56 | GetProfileIdHandler, 57 | GetProfileIdResult.model_validate( 58 | {"profile_id": 1, "local_id": 0}).model_dump(), 59 | ) 60 | create_mock_url(config, UpdateGameHandler, {"message": "ok"}) 61 | create_mock_url( 62 | config, AuthPlayerHandler, AuthPlayerResult.model_validate( 63 | {"profile_id": 1, "local_id": 0}).model_dump(mode="json") 64 | ) 65 | create_mock_url(config, NewGameHandler, {"message": "ok"}) 66 | create_mock_url( 67 | config, 68 | AuthGameHandler, 69 | AuthGameResult(session_key="123456", 70 | local_id=0, 71 | game_name="gmtest").model_dump(), 72 | ) 73 | 74 | return cast(Client, conn._client) 75 | -------------------------------------------------------------------------------- /src/frontends/tests/gamespy/query_report/game_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import responses 4 | from frontends.tests.gamespy.query_report.mock_objects import create_client 5 | 6 | 7 | class GameTests(unittest.TestCase): 8 | @responses.activate 9 | def test_faltout2(self): 10 | raw = b'\x03\xc2\x94\x1c\xe7localip0\x00192.168.0.50\x00localip1\x00172.16.74.1\x00localip2\x00172.17.0.1\x00localip3\x00192.168.122.1\x00localip4\x00172.16.65.1\x00localport\x0023756\x00natneg\x001\x00statechanged\x001\x00gamename\x00flatout2pc\x00publicip\x000\x00publicport\x000\x00hostkey\x00-820966322\x00hostname\x00Spore\x00gamever\x00FO14\x00gametype\x00race\x00gamevariant\x00normal_race\x00gamemode\x00openwaiting\x00numplayers\x001\x00maxplayers\x008\x00mapname\x00Timberlands_1\x00timelimit\x000\x00password\x000\x00car_type\x000\x00car_class\x000\x00races_p\x00100\x00derbies_p\x000\x00stunts_p\x000\x00normal_race_p\x00100\x00pong_race_p\x000\x00wreck_derby_p\x000\x00survivor_derby_p\x000\x00frag_derby_p\x000\x00tag_p\x000\x00upgrades\x002\x00nitro_regeneration\x002\x00damage_level\x002\x00derby_damage_level\x001\x00next_race_type\x00normal_race\x00laps_or_timelimit\x004\x00num_races\x001\x00num_derbies\x000\x00num_stunts\x000\x00datachecksum\x003546d58093237eb33b2a96bb813370d846ffcec8\x00\x00\x00\x00\x00\x00\x00\x00' 11 | client = create_client() 12 | client.on_received(raw) 13 | 14 | @responses.activate 15 | def test_worm3d(self): 16 | raw = b'\x03Q]\xa0\xe8localip0\x00192.168.0.60\x00localport\x006500\x00natneg\x001\x00statechanged\x003\x00gamename\x00worms3\x00hostname\x00test\x00gamemode\x00openstaging\x00groupid\x00622\x00numplayers\x001\x00maxplayers\x002\x00hostname\x00test\x00hostport\x00\x00maxplayers\x002\x00numplayers\x001\x00SchemeChanging\x000\x00gamever\x001073\x00gametype\x00\x00mapname\x00\x00firewall\x000\x00publicip\x00255.255.255.255\x00privateip\x00192.168.0.60\x00gamemode\x00openstaging\x00val\x000\x00password\x000\x00\x00\x00\x01player_\x00ping_\x00hostname\x00hostport\x00maxplayers\x00numplayers\x00SchemeChanging\x00gamever\x00gametype\x00mapname\x00firewall\x00publicip\x00privateip\x00gamemode\x00val\x00password\x00\x00worms10\x000\x00\x00\x00\x00\x00\x001073\x00\x00\x001\x00255.255.255.255\x00192.168.0.60\x00\x000\x00\x00\x00\x00hostname\x00hostport\x00maxplayers\x00numplayers\x00SchemeChanging\x00gamever\x00gametype\x00mapname\x00firewall\x00publicip\x00privateip\x00gamemode\x00val\x00password\x00\x00' 17 | client = create_client() 18 | client.on_received(raw) 19 | -------------------------------------------------------------------------------- /src/frontends/gamespy/library/network/websocket_brocker.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from uuid import UUID 3 | from websockets import ConnectionClosed 4 | from frontends.gamespy.library.abstractions.brocker import BrockerBase 5 | 6 | from frontends.gamespy.library.log.log_manager import GLOBAL_LOGGER 7 | from websockets.sync.client import connect, ClientConnection 8 | 9 | 10 | class WebSocketBrocker(BrockerBase): 11 | _subscriber: ClientConnection 12 | _publisher: ClientConnection 13 | 14 | def subscribe(self): 15 | self._publisher = self._subscriber = connect(self.url) 16 | th = threading.Thread(target=self._listen) 17 | th.start() 18 | 19 | @property 20 | def ip_port(self) -> str: 21 | name = self._subscriber.socket.getsockname() 22 | return f"{name[0]}:{name[1]}" 23 | 24 | def _listen(self): 25 | # we do not listen to channel, if the call back is none 26 | if self._call_back_func is None: 27 | return 28 | 29 | try: 30 | while True: 31 | message = self._subscriber.recv() 32 | self._call_back_func(message) 33 | except ConnectionClosed: 34 | GLOBAL_LOGGER.warn("backend websocket server is not avaliable") 35 | # raise UniSpyException("websocket connection is not established") 36 | 37 | def unsubscribe(self): 38 | self._subscriber.close() 39 | 40 | def publish_message(self, message: str): 41 | super().publish_message(message) 42 | if self._publisher is None: 43 | raise ValueError("websocket connection is not established") 44 | self._publisher.send(message) 45 | 46 | 47 | COUNT = 0 48 | 49 | 50 | def call_back(str): 51 | global COUNT 52 | COUNT += 1 53 | print(COUNT) 54 | # print(f"{datetime.now()}:{str}") 55 | 56 | 57 | if __name__ == "__main__": 58 | ws = WebSocketBrocker( 59 | name="test_channel", 60 | url="ws://127.0.0.1:8080/GameSpy/Chat/ws", 61 | call_back_func=call_back, 62 | ) 63 | ws.subscribe() 64 | from frontends.gamespy.protocols.chat.abstractions.contract import BrockerMessage 65 | 66 | msg = BrockerMessage( 67 | server_id=UUID("08ed7859-1d9e-448b-8fda-dabb845d85f9"), 68 | channel_name="gmtest", 69 | sender_ip_address="192.168.0.1", 70 | sender_port=80, 71 | message="hello", 72 | ) 73 | # while True: 74 | while True: 75 | ws.publish_message(msg.model_dump_json()) 76 | pass 77 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/natneg/aggregations/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, IntEnum 2 | 3 | 4 | class RequestType(Enum): 5 | INIT = 0 6 | ERT_ACK = 3 7 | CONNECT = 5 8 | CONNECT_ACK = 6 9 | """ 10 | only used in client, not used by server 11 | """ 12 | PING = 7 13 | ADDRESS_CHECK = 10 14 | NATIFY_REQUEST = 12 15 | REPORT = 13 16 | PRE_INIT = 15 17 | RELAY_MSG = 100 18 | """ 19 | unispy custom request type for GTR 20 | """ 21 | 22 | class NatPortType(Enum): 23 | GP = 0 24 | NN1 = 1 25 | NN2 = 2 26 | NN3 = 3 27 | 28 | 29 | class ResponseType(Enum): 30 | INIT_ACK = 1 31 | ERT_TEST = 2 32 | ERT_ACK = 3 33 | CONNECT = 5 34 | ADDRESS_REPLY = 11 35 | REPORT_ACK = 14 36 | PRE_INIT_ACK = 16 37 | 38 | 39 | class ConnectPacketStatus(Enum): 40 | NO_ERROR = 0 41 | BAD_HEART_BEAT = 1 42 | INIT_PACKET_TIMEOUT = 2 43 | 44 | 45 | class NatifyPacketType(Enum): 46 | PACKET_MAP_1A = 0 47 | PACKET_MAP_2 = 1 48 | PACKET_MAP_3 = 2 49 | PACKET_MAP_1B = 3 50 | NUM_PACKETS = 4 51 | 52 | 53 | class PreInitState(Enum): 54 | WAITING_FOR_CLIENT = 0 55 | WAITING_FOR_MATCH_UP = 1 56 | READY = 2 57 | 58 | 59 | class NatType(IntEnum): 60 | NO_NAT = 0 61 | FIRE_WALL_ONLY = 1 62 | """ 63 | C发数据到210.21.12.140:8000,NAT会将数据包送到A(192.168.0.4:5000).因为NAT上已经有了192.168.0.4:5000到210.21.12.140:8000的映射 64 | """ 65 | FULL_CONE = 2 66 | """ 67 | C无法和A通信,因为A从来没有和C通信过,NAT将拒绝C试图与A连接的动作.但B可以通过210.21.12.140:8000与A的 192.168.0.4:5000通信,且这里B可以使用任何端口与A通信.如:210.15.27.166:2001 -> 210.21.12.140:8000,NAT会送到A的5000端口上 68 | """ 69 | ADDRESS_RESTRICTED_CONE = 3 70 | """ 71 | C无法与A通信,因为A从来没有和C通信过.而B也只能用它的210.15.27.166:2000与A的192.168.0.4:5000通信,因为A也从来没有和B的其他端口通信过.该类型NAT是端口受限的. 72 | """ 73 | PORT_RESTRICTED_CONE = 4 74 | """ 75 | 上面3种类型,统称为Cone NAT,有一个共同点:只要是从同一个内部地址和端口出来的包,NAT都将它转换成同一个外部地址和端口.但是Symmetric有点不同,具体表现在: 只要是从同一个内部地址和端口出来,且到同一个外部目标地址和端口,则NAT也都将它转换成同一个外部地址和端口.但如果从同一个内部地址和端口出来,是 到另一个外部目标地址和端口,则NAT将使用不同的映射,转换成不同的端口(外部地址只有一个,故不变).而且和Port Restricted Cone一样,只有曾经收到过内部地址发来包的外部地址,才能通过NAT映射后的地址向该内部地址发包. 76 | """ 77 | SYMMETRIC = 5 78 | """ 79 | 端口分配是随机的,无法确定下一次NAT映射端口. 80 | """ 81 | UNKNOWN = 6 82 | 83 | 84 | class NatPortMappingScheme(Enum): 85 | UNRECOGNIZED = 0 86 | PRIVATE_AS_PUBLIC = 1 87 | CONSISTENT_PORT = 2 88 | INCREMENTAL = 3 89 | MIXED = 4 90 | 91 | 92 | class NatClientIndex(Enum): 93 | GAME_CLIENT = 0 94 | GAME_SERVER = 1 95 | -------------------------------------------------------------------------------- /src/backends/tests/gamespy/web/handler_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from backends.protocols.gamespy.web_services.handlers import LoginRemoteAuthHandler 4 | from backends.protocols.gamespy.web_services.requests import LoginRemoteAuthRequest 5 | from frontends.gamespy.protocols.web_services.modules.auth.exceptions.general import AuthException 6 | 7 | 8 | class HandlerTests(unittest.TestCase): 9 | def test_sdk_login_remote_auth(self): 10 | with self.assertRaises(AuthException) as context: 11 | raw = {"raw_request": "1000GMTy13lsJmiY7L19ojyN3XTM08ll0C4EWWijwmJyq3ttiZmoDUQJ0OSnar9nQCu5MpOGvi4Z0EcC2uNaS4yKrUA+h+tTDDoJHF7ZjoWKOTj00yNOEdzWyG08cKdVQwFRkF+h8oG/Jd+Ik3sWviXq/+5bhZQ7iXxTbbDwNL6Lagp/pLZ9czLnYPhY7VEcoQlx9oOLH8c.DLe", 12 | "version": 1, "partner_code": 0, "namespace_id": 0, "auth_token": "GMTy13lsJmiY7L19ojyN3XTM08ll0C4EWWijwmJyq3ttiZmoDUQJ0OSnar9nQCu5MpOGvi4Z0EcC2uNaS4yKrUA+h+tTDDoJHF7ZjoWKOTj00yNOEdzWyG08cKdVQwFRkF+h8oG/Jd+Ik3sWviXq/+5bhZQ7iXxTbbDwNL6Lagp/pLZ9czLnYPhY7VEcoQlx9oO", "challenge": "LH8c.DLe", "game_id": 0, "client_ip": "172.19.0.4", "server_id": "950b7638-a90d-469b-ac1f-861e63c8c613", "client_port": 57502} 13 | request = LoginRemoteAuthRequest.model_validate(raw) 14 | handler = LoginRemoteAuthHandler(request) 15 | handler.handle() 16 | handler.response 17 | 18 | def test_sdk_login_remote_auth_fake_data(self): 19 | raw = {"raw_request": "", 20 | "version": 1, "partner_code": 0, "namespace_id": 0, "auth_token": "example_auth", "challenge": "LH8c.DLe", "game_id": 0, "client_ip": "172.19.0.4", "server_id": "950b7638-a90d-469b-ac1f-861e63c8c613", "client_port": 57502} 21 | request = LoginRemoteAuthRequest.model_validate(raw) 22 | handler = LoginRemoteAuthHandler(request) 23 | handler.handle() 24 | handler.response 25 | 26 | def test_sdk_login_uniquenick(self): 27 | raw = {} 28 | -------------------------------------------------------------------------------- /src/frontends/gamespy/library/network/http_handler.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.library.abstractions.client import ClientBase 2 | from frontends.gamespy.library.abstractions.connections import ( 3 | ConnectionBase, 4 | NetworkServerBase, 5 | ) 6 | from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer 7 | from frontends.gamespy.library.configs import CONFIG, ServerConfig 8 | from frontends.gamespy.library.log.log_manager import LogWriter 9 | 10 | 11 | class HttpConnection(ConnectionBase): 12 | handler: BaseHTTPRequestHandler 13 | 14 | def send(self, data: bytes) -> None: 15 | assert isinstance(data, bytes) 16 | self.handler.send_response(200) 17 | self.handler.send_header("Content-type", "text/xml") 18 | self.handler.send_header("Content-Length", str(len(data))) 19 | self.handler.end_headers() 20 | self.handler.wfile.write(data) 21 | 22 | class HttpHandler(BaseHTTPRequestHandler): 23 | conn: HttpConnection 24 | 25 | def do_POST(self) -> None: 26 | # parsed_url = urlparse(self.path).geturl() 27 | content_length = int(self.headers["Content-Length"]) 28 | data = self.rfile.read(content_length).decode() 29 | self.conn = HttpConnection( 30 | self, *self.server.unispy_params) # type: ignore 31 | self.conn.on_received(data.encode()) 32 | 33 | def log_message(self, format, *args): 34 | pass 35 | 36 | 37 | class HttpServer(NetworkServerBase): 38 | def __init__( 39 | self, config: ServerConfig, t_client: type[ClientBase], logger: LogWriter 40 | ) -> None: 41 | super().__init__(config, t_client, logger) 42 | self._server = ThreadingHTTPServer( 43 | (self._config.listening_address, 44 | self._config.listening_port), HttpHandler 45 | ) 46 | self._server.unispy_params = ( # type: ignore 47 | self._config, 48 | self._client_cls, 49 | self._logger) 50 | 51 | 52 | class TestClient(ClientBase): 53 | def _create_switcher(self, buffer) -> None: 54 | # return super().create_switcher(buffer) 55 | print(buffer) 56 | pass 57 | 58 | def on_connected(self) -> None: 59 | # return super().on_connected() 60 | print("connected!") 61 | pass 62 | 63 | 64 | if __name__ == "__main__": 65 | # create_http_server(list(CONFIG.servers.values())[0], ClientBase) 66 | from frontends.tests.gamespy.library.mock_objects import LogMock 67 | 68 | s = HttpServer(list(CONFIG.servers.values())[0], TestClient, LogMock()) 69 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/game_traffic_relay/applications/server_launcher.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from frontends.gamespy.library.abstractions.server_launcher import ServicesFactory, ServiceBase 3 | from frontends.gamespy.library.configs import CONFIG 4 | from frontends.gamespy.library.log.log_manager import GLOBAL_LOGGER 5 | from frontends.gamespy.library.network.udp_handler import UdpServer 6 | from frontends.gamespy.protocols.game_traffic_relay.applications.client import Client 7 | from frontends.gamespy.protocols.game_traffic_relay.applications.connection import ConnectionListener 8 | from frontends.gamespy.protocols.game_traffic_relay.contracts.general import ( 9 | GtrHeartbeat, 10 | ) 11 | 12 | 13 | class Service(ServiceBase): 14 | 15 | def __init__(self) -> None: 16 | super().__init__( 17 | config_name="GameTrafficRelay", 18 | client_cls=Client, 19 | network_server_cls=UdpServer, 20 | ) 21 | 22 | def _post_task(self): 23 | super()._post_task() 24 | self.__gtr_heartbeat() 25 | self.__check_expired_connection() 26 | 27 | def __gtr_heartbeat(self): 28 | assert self.config 29 | req = GtrHeartbeat( 30 | server_id=self.config.server_id, 31 | public_ip_address=self.config.listening_address, 32 | public_port=self.config.listening_port, 33 | client_count=len(ConnectionListener.client_pool), 34 | ) 35 | req_str = req.model_dump_json() 36 | self._heartbeat_to_backend( 37 | f"{CONFIG.backend.url}/GameSpy/GameTrafficRelay/Heartbeat", req_str 38 | ) 39 | 40 | def __check_expired_connection(self): 41 | expired_time = datetime.now() - timedelta(seconds=30) 42 | try: 43 | for key in ConnectionListener.cookie_pool.keys(): 44 | pair = ConnectionListener.cookie_pool[key] 45 | if pair[0].info.last_receive_time < expired_time: 46 | del ConnectionListener.cookie_pool[key] 47 | del ConnectionListener.client_pool[pair[0].connection.ip_endpoint] 48 | del ConnectionListener.client_pool[pair[1].connection.ip_endpoint] 49 | except Exception as e: 50 | GLOBAL_LOGGER.warn(f"Errors occured when doing cookie delete: {e}") 51 | 52 | 53 | if __name__ == "__main__": 54 | from frontends.gamespy.library.extentions.debug_helper import DebugHelper 55 | gtr = Service() 56 | helper = DebugHelper("./frontends/", ServicesFactory([gtr])) 57 | helper.start() 58 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/chat/aggregates/peer_room.py: -------------------------------------------------------------------------------- 1 | from frontends.gamespy.protocols.chat.aggregates.enums import PeerRoomType 2 | 3 | 4 | class PeerRoom: 5 | TitleRoomPrefix = "#GSP" 6 | """ When game connects to the server, the player will enter the default channel for communicating with other players.""" 7 | StagingRoomPrefix = "#GSP" 8 | """ 9 | When a player creates their own game and is waiting for others to join they are placed in a separate chat room called the "staging room"\n 10 | Staging rooms have two title seperator like #GSP!xxxx!xxxx 11 | """ 12 | GroupRoomPrefix = "#GPG" 13 | """ 14 | group rooms is used split the list of games into categories (by gametype, skill, region, etc.). In this case, when entering the title room, the user would get a list of group rooms instead of a list of games\n 15 | Group room have one title seperator like #GPG!xxxxxx 16 | """ 17 | TitleSeperator = "!" 18 | 19 | """ 20 | Group room #GPG!622\n 21 | Staging room #GSP!worms3!Ml4lz344lM\n 22 | Normal room #islanbul 23 | """ 24 | 25 | @staticmethod 26 | def get_room_type(channel_name: str) -> PeerRoomType: 27 | if PeerRoom.is_group_room(channel_name): 28 | return PeerRoomType.Group 29 | elif PeerRoom.is_staging_room(channel_name): 30 | return PeerRoomType.Staging 31 | elif PeerRoom.is_title_room(channel_name): 32 | return PeerRoomType.Title 33 | else: 34 | return PeerRoomType.Normal 35 | 36 | @staticmethod 37 | def is_staging_room(channel_name: str) -> bool: 38 | a = channel_name.count(PeerRoom.TitleSeperator) == 2 39 | b = channel_name.startswith( 40 | PeerRoom.StagingRoomPrefix, 0, len(PeerRoom.StagingRoomPrefix) 41 | ) 42 | return a and b 43 | 44 | @staticmethod 45 | def is_title_room(channel_name: str) -> bool: 46 | a = channel_name.count(PeerRoom.TitleSeperator) == 1 47 | b = channel_name.startswith( 48 | PeerRoom.TitleRoomPrefix, 0, len(PeerRoom.TitleRoomPrefix) 49 | ) 50 | return a and b 51 | 52 | @staticmethod 53 | def is_group_room(channel_name: str) -> bool: 54 | a = channel_name.count(PeerRoom.TitleSeperator) == 1 55 | b = channel_name.startswith( 56 | PeerRoom.GroupRoomPrefix, 0, len(PeerRoom.GroupRoomPrefix) 57 | ) 58 | return a and b 59 | 60 | 61 | if __name__ == "__main__": 62 | result = PeerRoom.get_room_type("#GSP!worms3!Ml4lz344lM") 63 | result = PeerRoom.get_room_type("#GPG!700") 64 | pass 65 | -------------------------------------------------------------------------------- /src/backends/routers/gamespy/query_report.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, WebSocket 2 | from backends.library.abstractions.contracts import RESPONSES_DEF, OKResponse 3 | from backends.protocols.gamespy.query_report.broker import MANAGER, launch_brocker 4 | from backends.protocols.gamespy.query_report.handlers import ( 5 | AvaliableHandler, HeartbeatHandler, KeepAliveHandler, LegacyHeartbeatHandler) 6 | from backends.protocols.gamespy.query_report.requests import ( 7 | AvaliableRequest, ChallengeRequest, ClientMessageRequest, EchoRequest, HeartBeatRequest, KeepAliveRequest, LegacyHeartbeatRequest) 8 | from backends.urls import QUERY_REPORT 9 | 10 | 11 | router = APIRouter(lifespan=launch_brocker) 12 | 13 | 14 | @router.websocket(f"{QUERY_REPORT}/ws") 15 | async def websocket_endpoint(ws: WebSocket): 16 | await MANAGER.process_websocket(ws) 17 | 18 | 19 | @router.post(f"{QUERY_REPORT}/HeartbeatHandler", responses=RESPONSES_DEF) 20 | def heartbeat(request: HeartBeatRequest) -> OKResponse: 21 | handler = HeartbeatHandler(request) 22 | handler.handle() 23 | return handler.response 24 | 25 | 26 | @router.post(f"{QUERY_REPORT}/ChallengeHanler", responses=RESPONSES_DEF) 27 | def challenge(request: ChallengeRequest) -> OKResponse: 28 | raise NotImplementedError() 29 | 30 | 31 | @router.post(f"{QUERY_REPORT}/AvailableHandler", responses=RESPONSES_DEF) 32 | def available(request: AvaliableRequest) -> OKResponse: 33 | handler = AvaliableHandler(request) 34 | handler.handle() 35 | return handler.response 36 | 37 | 38 | @router.post(f"{QUERY_REPORT}/ClientMessageAckHandler", responses=RESPONSES_DEF) 39 | def client_message(request: ClientMessageRequest) -> OKResponse: 40 | raise NotImplementedError() 41 | 42 | 43 | @router.post(f"{QUERY_REPORT}/EchoHandler", responses=RESPONSES_DEF) 44 | def echo(request: EchoRequest) -> OKResponse: 45 | raise NotImplementedError() 46 | 47 | 48 | @router.post(f"{QUERY_REPORT}/KeepAliveHandler", responses=RESPONSES_DEF) 49 | def keep_alive(request: KeepAliveRequest) -> OKResponse: 50 | handler = KeepAliveHandler(request) 51 | handler.handle() 52 | return handler.response 53 | 54 | 55 | @router.post(f"{QUERY_REPORT}/LegacyHeartbeatHandler", responses=RESPONSES_DEF) 56 | def legacy_heartbeat(request: LegacyHeartbeatRequest) -> OKResponse: 57 | handler = LegacyHeartbeatHandler(request) 58 | handler.handle() 59 | return handler.response 60 | 61 | 62 | if __name__ == "__main__": 63 | import uvicorn 64 | from fastapi import FastAPI 65 | app = FastAPI() 66 | app.include_router(router) 67 | uvicorn.run(app, host="0.0.0.0", port=8080) 68 | -------------------------------------------------------------------------------- /src/frontends/gamespy/protocols/query_report/v2/applications/switcher.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, cast 2 | from frontends.gamespy.library.abstractions.switcher import SwitcherBase 3 | from frontends.gamespy.protocols.query_report.aggregates.exceptions import QRException 4 | from frontends.gamespy.protocols.query_report.applications.client import Client 5 | from frontends.gamespy.protocols.query_report.v2.abstractions.handlers import CmdHandlerBase 6 | 7 | from frontends.gamespy.protocols.query_report.v2.contracts.requests import ( 8 | AvaliableRequest, 9 | ChallengeRequest, 10 | ClientMessageAckRequest, 11 | EchoRequest, 12 | HeartbeatRequest, 13 | KeepAliveRequest, 14 | ) 15 | from frontends.gamespy.protocols.query_report.v2.aggregates.enums import RequestType 16 | from frontends.gamespy.protocols.query_report.v2.applications.handlers import ( 17 | AvailableHandler, 18 | ChallengeHanler, 19 | ClientMessageAckHandler, 20 | EchoHandler, 21 | HeartbeatHandler, 22 | KeepAliveHandler, 23 | ) 24 | 25 | 26 | class Switcher(SwitcherBase): 27 | _raw_request: bytes 28 | 29 | def _process_raw_request(self) -> None: 30 | if len(self._raw_request) < 4: 31 | raise QRException("Invalid request") 32 | name = self._raw_request[0] 33 | if name not in RequestType: 34 | self._client.log_debug( 35 | f"Request: {name} is not a valid request.") 36 | return 37 | raw_request = self._raw_request 38 | self._requests.append((RequestType(name), raw_request)) 39 | 40 | def _create_cmd_handlers(self, name: RequestType, raw_request: bytes) -> CmdHandlerBase | None: 41 | assert isinstance(name, RequestType) 42 | if TYPE_CHECKING: 43 | self._client = cast(Client, self._client) 44 | match name: 45 | case RequestType.HEARTBEAT: 46 | return HeartbeatHandler(self._client, HeartbeatRequest(raw_request)) 47 | case RequestType.CHALLENGE: 48 | return ChallengeHanler(self._client, ChallengeRequest(raw_request)) 49 | case RequestType.AVALIABLE_CHECK: 50 | return AvailableHandler(self._client, AvaliableRequest(raw_request)) 51 | case RequestType.CLIENT_MESSAGE_ACK: 52 | return ClientMessageAckHandler(self._client, ClientMessageAckRequest(raw_request)) 53 | case RequestType.ECHO: 54 | return EchoHandler(self._client, EchoRequest(raw_request)) 55 | case RequestType.KEEP_ALIVE: 56 | return KeepAliveHandler(self._client, KeepAliveRequest(raw_request)) 57 | case _: 58 | return None 59 | --------------------------------------------------------------------------------