connect(); try { $this->authenticate(); $requestId = $this->nextRequestId(); $this->writePacket($requestId, self::COMMAND_PACKET_TYPE, $command); $response = $this->readPacket(); if ($response['id'] !== $requestId) { throw new RuntimeException('Unexpected RCON response id.'); } return $response['body']; } finally { $this->disconnect(); } } private function connect(): void { $host = config('rcon.host'); $port = (int) config('rcon.port'); $timeout = (float) config('rcon.timeout'); if (blank($host)) { throw new RuntimeException('MC_RCON_HOST is not configured.'); } $socket = @stream_socket_client( "tcp://{$host}:{$port}", $errorCode, $errorMessage, $timeout, ); if ($socket === false) { throw new RuntimeException("Unable to connect to Minecraft RCON: {$errorMessage} ({$errorCode})."); } $this->socket = $socket; $seconds = (int) floor($timeout); $microseconds = (int) (($timeout - $seconds) * 1_000_000); stream_set_timeout($this->socket, $seconds, $microseconds); } private function authenticate(): void { $password = config('rcon.password'); if (blank($password)) { throw new RuntimeException('MC_RCON_PASSWORD is not configured.'); } $requestId = $this->nextRequestId(); $this->writePacket($requestId, self::AUTH_PACKET_TYPE, (string) $password); do { $response = $this->readPacket(); } while ($response['type'] !== self::COMMAND_PACKET_TYPE); if ($response['id'] === -1 || $response['id'] !== $requestId) { throw new RuntimeException('Minecraft RCON authentication failed.'); } } private function disconnect(): void { if (is_resource($this->socket)) { fclose($this->socket); } $this->socket = null; } private function nextRequestId(): int { return $this->requestId++; } private function writePacket(int $requestId, int $type, string $body): void { $payload = pack('VV', $requestId, $type).$body."\x00\x00"; $packet = pack('V', strlen($payload)).$payload; $bytesWritten = 0; while ($bytesWritten < strlen($packet)) { $written = fwrite($this->socket, substr($packet, $bytesWritten)); if ($written === false || $written === 0) { throw new RuntimeException('Unable to write RCON packet.'); } $bytesWritten += $written; } } /** * @return array{id: int, type: int, body: string} */ private function readPacket(): array { $sizeData = $this->readBytes(4); $size = unpack('V', $sizeData)[1]; if ($size < 10) { throw new RuntimeException('Invalid RCON packet size.'); } $packet = $this->readBytes($size); $header = unpack('Vid/Vtype', substr($packet, 0, 8)); $id = $header['id'] >= 0x80000000 ? $header['id'] - 0x100000000 : $header['id']; return [ 'id' => $id, 'type' => $header['type'], 'body' => substr($packet, 8, -2), ]; } private function readBytes(int $length): string { $data = ''; while (strlen($data) < $length) { $chunk = fread($this->socket, $length - strlen($data)); if ($chunk === false || $chunk === '') { throw new RuntimeException('Unable to read RCON packet.'); } $data .= $chunk; } return $data; } }