From ffe55bf03ef8f966f6fc4c19343a852e25c22ab7 Mon Sep 17 00:00:00 2001 From: devRaGonSa Date: Thu, 26 Mar 2026 10:21:42 +0100 Subject: [PATCH] Adapt RCON client to rconv2 framing --- backend/app/rcon_client.py | 99 ++++++++++++++++++++++++++++++++++---- 1 file changed, 90 insertions(+), 9 deletions(-) diff --git a/backend/app/rcon_client.py b/backend/app/rcon_client.py index 48928d2..81a2fa1 100644 --- a/backend/app/rcon_client.py +++ b/backend/app/rcon_client.py @@ -18,7 +18,8 @@ from .config import ( RCON_BUFFER_SIZE = 32768 -RCON_HEADER_FORMAT = " dict[str, object]: header_size = struct.calcsize(RCON_HEADER_FORMAT) - header_bytes = self._recv_exact(header_size, stage=response_stage) + header_bytes = self._recv_exact( + header_size, + stage=response_stage, + receive_context="response header", + ) try: - request_id, body_length = struct.unpack(RCON_HEADER_FORMAT, header_bytes) + magic_value, request_id, body_length = struct.unpack( + RCON_HEADER_FORMAT, + header_bytes, + ) except struct.error as error: raise RconQueryError( "payload-invalid", "The HLL server returned an invalid RCON response header.", error_stage=response_stage, ) from error + if magic_value != RCON_MAGIC_HEADER_VALUE: + raise RconQueryError( + "invalid-magic", + ( + "The HLL server returned an unexpected RCON magic value: " + f"{magic_value:#x} (expected {RCON_MAGIC_HEADER_VALUE:#x})." + ), + error_stage=response_stage, + ) if body_length <= 0: raise RconQueryError( "unexpected-response", @@ -218,7 +240,7 @@ class HllRconConnection: error_stage=response_stage, ) - body = self._xor(self._recv_exact(body_length, stage=response_stage)) + body = self._xor(self._recv_body(body_length, stage=response_stage)) try: parsed = json.loads(body.decode("utf-8", errors="replace")) except json.JSONDecodeError as error: @@ -237,7 +259,54 @@ class HllRconConnection: parsed["requestId"] = request_id return parsed - def _recv_exact(self, expected_length: int, *, stage: str) -> bytes: + def _recv_body(self, expected_length: int, *, stage: str) -> bytes: + chunks = bytearray() + original_timeout = self._socket.gettimeout() + body_timeout_seconds = min(3.0, original_timeout or 3.0) + self._socket.settimeout(body_timeout_seconds) + try: + while len(chunks) < expected_length: + self._current_stage = stage + try: + chunk = self._socket.recv( + min(RCON_BUFFER_SIZE, expected_length - len(chunks)) + ) + except (TimeoutError, socket.timeout) as error: + raise RconQueryError( + "timeout", + ( + f"Timed out during {stage} while waiting for response body " + f"({len(chunks)}/{expected_length} bytes received)." + ), + error_stage=stage, + ) from error + except OSError as error: + raise RconQueryError( + _classify_socket_error_type(error), + f"RCON socket error during {stage}: {error}", + error_stage=stage, + ) from error + if not chunk: + raise RconQueryError( + "connection-closed", + ( + "The HLL RCON connection closed unexpectedly while waiting for " + f"response body ({len(chunks)}/{expected_length} bytes received)." + ), + error_stage=stage, + ) + chunks.extend(chunk) + finally: + self._socket.settimeout(original_timeout) + return bytes(chunks) + + def _recv_exact( + self, + expected_length: int, + *, + stage: str, + receive_context: str, + ) -> bytes: chunks = bytearray() while len(chunks) < expected_length: self._current_stage = stage @@ -246,7 +315,10 @@ class HllRconConnection: except (TimeoutError, socket.timeout) as error: raise RconQueryError( "timeout", - f"Timed out during {stage}.", + ( + f"Timed out during {stage} while waiting for {receive_context} " + f"({len(chunks)}/{expected_length} bytes received)." + ), error_stage=stage, ) from error except OSError as error: @@ -257,8 +329,11 @@ class HllRconConnection: ) from error if not chunk: raise RconQueryError( - "unexpected-response", - "The HLL RCON connection closed unexpectedly.", + "connection-closed", + ( + "The HLL RCON connection closed unexpectedly while waiting for " + f"{receive_context} ({len(chunks)}/{expected_length} bytes received)." + ), error_stage=stage, ) chunks.extend(chunk) @@ -545,8 +620,14 @@ def _classify_runtime_error_type(error: RuntimeError) -> str: message = str(error).lower() if "auth token" in message or "login failed" in message or "status 401" in message or "status 403" in message: return "auth/login" + if "invalid magic" in message: + return "invalid-magic" + if "closed unexpectedly" in message or "closed connection" in message: + return "connection-closed" if "invalid json" in message or "unexpected payload" in message or "malformed" in message or "invalid rcon" in message: return "payload-invalid" if "timed out" in message: return "timeout" + if "unexpected" in message: + return "unexpected-response" return "other-error"