diff --git a/game_coordinator/application/coordinator.py b/game_coordinator/application/coordinator.py index fc6b5de..f6e548a 100644 --- a/game_coordinator/application/coordinator.py +++ b/game_coordinator/application/coordinator.py @@ -54,6 +54,14 @@ def disconnect(self, source): def delete_token(self, token): del self._tokens[token] + def _remove_broken_server(self, server_id, error_no, error_detail): + broken_server = self._servers[server_id] + if isinstance(broken_server, ServerExternal): + return + + asyncio.create_task(broken_server.send_error_and_close(error_no, error_detail)) + del self._servers[server_id] + async def add_turn_server(self, connection_string): if connection_string not in self.turn_servers: self.turn_servers.append(connection_string) @@ -76,8 +84,15 @@ async def update_external_server(self, server_id, info): self._servers[server_id] = ServerExternal(self, server_id) if not isinstance(self._servers[server_id], ServerExternal): - log.error("Internal error: update_external_server() called on a server managed by us") - return + # Two servers could announce themselves with the same server-id. + # Best way to deal with the situation is to assume the new instance + # is in good contact with the server, and for us to drop our + # connection with the old. + + self._remove_broken_server( + server_id, NetworkCoordinatorErrorType.NETWORK_COORDINATOR_ERROR_REUSE_OF_INVITE_CODE, server_id + ) + self._servers[server_id] = ServerExternal(self, server_id) await self._servers[server_id].update(info) @@ -86,8 +101,15 @@ async def update_newgrf_external_server(self, server_id, newgrfs_indexed): self._servers[server_id] = ServerExternal(self, server_id) if not isinstance(self._servers[server_id], ServerExternal): - log.error("Internal error: update_external_server() called on a server managed by us") - return + # Two servers could announce themselves with the same server-id. + # Best way to deal with the situation is to assume the new instance + # is in good contact with the server, and for us to drop our + # connection with the old. + + self._remove_broken_server( + server_id, NetworkCoordinatorErrorType.NETWORK_COORDINATOR_ERROR_REUSE_OF_INVITE_CODE, server_id + ) + self._servers[server_id] = ServerExternal(self, server_id) await self._servers[server_id].update_newgrf(newgrfs_indexed) @@ -183,7 +205,19 @@ async def receive_PACKET_COORDINATOR_SERVER_REGISTER( invite_code_secret = generate_invite_code_secret(self._shared_secret, server_id) - source.server = Server(self, server_id, game_type, source, server_port, invite_code_secret) + source.server = Server(self, server_id, game_type, source, protocol_version, server_port, invite_code_secret) + + if source.server.server_id in self._servers: + # We replace a server already known; possibly two servers are using + # the same invite-code. There is not much we can do about this, + # other than disconnect the old, and hope the server-owner notices + # that they are constantly battling for the same invite-code. + self._remove_broken_server( + source.server.server_id, + NetworkCoordinatorErrorType.NETWORK_COORDINATOR_ERROR_REUSE_OF_INVITE_CODE, + source.server.server_id, + ) + self._servers[source.server.server_id] = source.server # Find an unused token. diff --git a/game_coordinator/application/helpers/server.py b/game_coordinator/application/helpers/server.py index 31ffc90..9e40475 100644 --- a/game_coordinator/application/helpers/server.py +++ b/game_coordinator/application/helpers/server.py @@ -6,6 +6,7 @@ NewGRFSerializationType, ServerGameType, ) +from openttd_protocol.wire.exceptions import SocketClosed log = logging.getLogger(__name__) @@ -74,10 +75,11 @@ async def send_connect_failed(self, protocol_version, token): class Server: - def __init__(self, application, server_id, game_type, source, server_port, invite_code_secret): + def __init__(self, application, server_id, game_type, source, protocol_version, server_port, invite_code_secret): self._application = application self._source = source self._invite_code_secret = invite_code_secret + self._protocol_version = protocol_version self.info = {} self.game_type = game_type @@ -94,6 +96,24 @@ def __init__(self, application, server_id, game_type, source, server_port, invit async def disconnect(self): await self._application.database.server_offline(self.server_id) + async def send_error_and_close(self, error_no, error_detail): + try: + await self._source.protocol.send_PACKET_COORDINATOR_GC_ERROR( + self._protocol_version, + error_no, + error_detail, + ) + + # Give it a second for the above packet to arrive. + await asyncio.sleep(1) + except SocketClosed: + # Socket already closed, so we can clean up the socket. + pass + + # Make sure disconnect() is not called on the object anymore. + del self._source.server + self._source.protocol.transport.abort() + async def update_newgrf(self, newgrf_serialization_type, newgrfs): if newgrfs is None: return diff --git a/requirements.txt b/requirements.txt index fe0d36d..d7ac75b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ hiredis==2.0.0 idna==3.2 multidict==5.1.0 openttd-helpers==1.0.1 -openttd-protocol==1.0.0 +openttd-protocol==1.1.0 pproxy==2.7.8 sentry-sdk==1.1.0 typing-extensions==3.10.0.0