Skip to content

Instantly share code, notes, and snippets.

@QWp6t
Created May 7, 2024 15:05
Show Gist options
  • Select an option

  • Save QWp6t/bcb1aa4bb78a318ee75de99f6c1c297d to your computer and use it in GitHub Desktop.

Select an option

Save QWp6t/bcb1aa4bb78a318ee75de99f6c1c297d to your computer and use it in GitHub Desktop.
PHP Rcon Client
<?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