Created
May 7, 2024 15:05
-
-
Save QWp6t/bcb1aa4bb78a318ee75de99f6c1c297d to your computer and use it in GitHub Desktop.
PHP Rcon Client
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| namespace App\Rcon; | |
| use Psr\Log\LoggerInterface; | |
| use Psr\Log\NullLogger; | |
| /** | |
| * Valve RCON client. | |
| * | |
| * @link https://developer.valvesoftware.com/wiki/Source_RCON_Protocol | |
| * @package App\Rcon | |
| */ | |
| class Client | |
| { | |
| /** | |
| * Authorization packet. | |
| * | |
| * This is the first packet sent by the client. | |
| * It is used to authenticate the client with the server. | |
| */ | |
| public const SERVERDATA_AUTH = 3; | |
| /** | |
| * Authorization ID. | |
| * | |
| * This is any unsigned integer. | |
| * It will be mirrored back in the response. | |
| */ | |
| public const SERVERDATA_AUTH_ID = 0; | |
| /** | |
| * Current authorization status. | |
| * | |
| * When the server receives an authorization packet, | |
| * it will respond with an empty `SERVERDATA_RESPONSE_VALUE`, | |
| * followed immediately by a `SERVERDATA_AUTH_RESPONSE`. | |
| * | |
| * When the status is successful, the value will be `SERVERDATA_AUTH_ID`, | |
| * otherwise it will be `-1`. | |
| */ | |
| public const SERVERDATA_AUTH_RESPONSE = 2; | |
| /** | |
| * Response to a command. | |
| * | |
| * The ID will be the same as the command packet. | |
| * See `SERVERDATA_EXECCOMMAND_ID`. | |
| */ | |
| public const SERVERDATA_RESPONSE_VALUE = 0; | |
| /** | |
| * Response to a command. | |
| * | |
| * This is any unsigned integer. | |
| * It will be mirrored back in the response. | |
| */ | |
| public const SERVERDATA_EXECCOMMAND_ID = 0; | |
| /** | |
| * Command packet. | |
| */ | |
| public const SERVERDATA_EXECCOMMAND = 2; | |
| protected $socket; | |
| protected bool $connected = false; | |
| protected string $lastResponse = ''; | |
| public function __construct( | |
| public readonly string $host, | |
| public readonly string $password, | |
| public readonly int $port = 25575, | |
| public readonly int $timeout = 3, | |
| protected ?LoggerInterface $logger = null | |
| ) { | |
| $this->logger ??= new NullLogger(); | |
| } | |
| public function getLastResponse() | |
| { | |
| return $this->lastResponse; | |
| } | |
| public function disconnect(): void | |
| { | |
| if (!$this->socket) { | |
| return; | |
| } | |
| $this->connected = false; | |
| fclose($this->socket); | |
| } | |
| public function isConnected(): bool | |
| { | |
| return $this->connected; | |
| } | |
| public function connect(int $retries = 3): bool | |
| { | |
| $this->socket = fsockopen($this->host, $this->port, $errno, $errstr, $this->timeout); | |
| if ($this->socket) { | |
| stream_set_timeout($this->socket, 3, 0); | |
| return $this->authorize(); | |
| } | |
| $this->lastResponse = $errstr; | |
| $this->logger->error("RCON connection failed. Retrying: {$retries}"); | |
| if ($retries > 0) { | |
| sleep(1); | |
| return $this->connect($retries - 1); | |
| } | |
| return false; | |
| } | |
| public function send(string $command): string | |
| { | |
| if (!$this->connected) { | |
| return false; | |
| } | |
| $this->write(self::SERVERDATA_EXECCOMMAND, self::SERVERDATA_EXECCOMMAND_ID, $command); | |
| [$type, $id, $body] = $this->read(); | |
| if ($id !== self::SERVERDATA_EXECCOMMAND_ID || $type !== self::SERVERDATA_EXECCOMMAND) { | |
| $this->logger->error("RCON command failed.", ['command' => $command]); | |
| } | |
| return $this->lastResponse = trim($body); | |
| } | |
| protected function authorize(): bool | |
| { | |
| $this->write(self::SERVERDATA_AUTH, self::SERVERDATA_AUTH_ID, $this->password); | |
| [$type, $id] = $this->read(); | |
| if ($type === self::SERVERDATA_AUTH_RESPONSE && $id === self::SERVERDATA_AUTH_ID) { | |
| return $this->connected = true; | |
| } | |
| $this->disconnect(); | |
| return $this->connected; | |
| } | |
| protected function write(int $type, int $id, string $body): void | |
| { | |
| $packet = pack('VV', $id, $type); | |
| $packet = "{$packet}{$body}\x00"; | |
| $packet = "{$packet}\x00"; | |
| $size = strlen($packet); | |
| $packet = pack('V', $size) . $packet; | |
| fwrite($this->socket, $packet, strlen($packet)); | |
| } | |
| /** | |
| * Read a packet from the socket. | |
| * | |
| * @return array<int, int, string> | |
| */ | |
| protected function read(): array | |
| { | |
| $packed_size = fread($this->socket, 4); | |
| ['size' => $size] = unpack('V1size', $packed_size); | |
| if ($size > 4096) { | |
| $this->logger->warning("RCON packet size is large: {$size} bytes."); | |
| } | |
| $packed_chunk = fread($this->socket, $size); | |
| ['type' => $type, 'id' => $id, 'body' => $body] = unpack('V1id/V1type/a*body', $packed_chunk); | |
| return [$type ?? -1, $id ?? -1, $body ?? '']; | |
| } | |
| public static function make(string $host, string $password, int $port = 25575, int $timeout = 3, ?LoggerInterface $logger = null): static | |
| { | |
| return new static($host, $password, $port, $timeout, $logger); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment