Split up Socket and Rcon classes to be engine specific extensions.

pull/150/head
Anthony Birkett 4 years ago
parent a2f7834ef5
commit b01c1f643f

@ -5,6 +5,8 @@ declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use xPaw\SourceQuery\SourceQuery;
use xPaw\SourceQuery\Socket\SourceSocket;
use xPaw\SourceQuery\Socket\SocketType;
// For the sake of this example
header('Content-Type: text/plain');
@ -14,19 +16,19 @@ header('X-Content-Type-Options: nosniff');
define('SQ_SERVER_ADDR', 'localhost');
define('SQ_SERVER_PORT', 27015);
define('SQ_TIMEOUT', 1);
define('SQ_ENGINE', SourceQuery::SOURCE);
define('SQ_ENGINE', SocketType::SOURCE);
// Edit this <-
$Query = new SourceQuery();
$Query = new SourceQuery(new SourceSocket());
try {
$Query->Connect(SQ_SERVER_ADDR, SQ_SERVER_PORT, SQ_TIMEOUT, SQ_ENGINE);
$Query->connect(SQ_SERVER_ADDR, SQ_SERVER_PORT, SQ_TIMEOUT, SQ_ENGINE);
print_r($Query->GetInfo());
print_r($Query->GetPlayers());
print_r($Query->GetRules());
print_r($Query->getInfo());
print_r($Query->getPlayers());
print_r($Query->getRules());
} catch (Exception $e) {
echo $e->getMessage();
} finally {
$Query->Disconnect();
$Query->disconnect();
}

@ -5,6 +5,8 @@ declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use xPaw\SourceQuery\SourceQuery;
use xPaw\SourceQuery\Socket\SourceSocket;
use xPaw\SourceQuery\Socket\SocketType;
// For the sake of this example
header('Content-Type: text/plain');
@ -14,19 +16,19 @@ header('X-Content-Type-Options: nosniff');
define('SQ_SERVER_ADDR', 'localhost');
define('SQ_SERVER_PORT', 27015);
define('SQ_TIMEOUT', 1);
define('SQ_ENGINE', SourceQuery::SOURCE);
define('SQ_ENGINE', SocketType::SOURCE);
// Edit this <-
$Query = new SourceQuery();
$Query = new SourceQuery(new SourceSocket());
try {
$Query->Connect(SQ_SERVER_ADDR, SQ_SERVER_PORT, SQ_TIMEOUT, SQ_ENGINE);
$Query->connect(SQ_SERVER_ADDR, SQ_SERVER_PORT, SQ_TIMEOUT, SQ_ENGINE);
$Query->SetRconPassword('my_awesome_password');
$Query->setRconPassword('my_awesome_password');
var_dump($Query->Rcon('say hello'));
var_dump($Query->rcon('say hello'));
} catch (Exception $e) {
echo $e->getMessage();
} finally {
$Query->Disconnect();
$Query->disconnect();
}

@ -5,17 +5,19 @@ declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use xPaw\SourceQuery\SourceQuery;
use xPaw\SourceQuery\Socket\SourceSocket;
use xPaw\SourceQuery\Socket\SocketType;
// Edit this ->
define('SQ_SERVER_ADDR', 'localhost');
define('SQ_SERVER_PORT', 27015);
define('SQ_TIMEOUT', 3);
define('SQ_ENGINE', SourceQuery::SOURCE);
define('SQ_ENGINE', SocketType::SOURCE);
// Edit this <-
$Timer = microtime(true);
$Query = new SourceQuery();
$Query = new SourceQuery(new SourceSocket());
$Info = [];
$Rules = [];
@ -23,16 +25,16 @@ $Players = [];
$Exception = null;
try {
$Query->Connect(SQ_SERVER_ADDR, SQ_SERVER_PORT, SQ_TIMEOUT, SQ_ENGINE);
$Query->connect(SQ_SERVER_ADDR, SQ_SERVER_PORT, SQ_TIMEOUT, SQ_ENGINE);
//$Query->SetUseOldGetChallengeMethod( true ); // Use this when players/rules retrieval fails on games like Starbound
$Info = $Query->GetInfo();
$Players = $Query->GetPlayers();
$Rules = $Query->GetRules();
$Info = $Query->getInfo();
$Players = $Query->getPlayers();
$Rules = $Query->getRules();
} catch (Exception $e) {
$Exception = $e;
} finally {
$Query->Disconnect();
$Query->disconnect();
}
$Timer = number_format(microtime(true) - $Timer, 4, '.', '');

@ -1,177 +0,0 @@
<?php
declare(strict_types=1);
/**
* @author Pavel Djundik
*
* @link https://xpaw.me
* @link https://github.com/xPaw/PHP-Source-Query
*
* @license GNU Lesser General Public License, version 2.1
*
* @internal
*/
namespace xPaw\SourceQuery;
use xPaw\SourceQuery\Exception\InvalidPacketException;
use xPaw\SourceQuery\Exception\SocketException;
/**
* Base socket interface
*
* @package xPaw\SourceQuery
*
* @uses InvalidPacketException
* @uses SocketException
*/
abstract class BaseSocket
{
/**
* @var resource|null
*/
public $Socket;
/**
* @var int $Engine
*/
public int $Engine = SourceQuery::SOURCE;
/**
* @var string $Address
*/
public string $Address = '';
/**
* @var int $Port
*/
public int $Port = 0;
/**
* @var int $Timeout
*/
public int $Timeout = 0;
/**
* Destructor
*/
public function __destruct()
{
$this->Close();
}
/**
* Close
*/
abstract public function Close(): void;
/**
* @param string $Address
* @param int $Port
* @param int $Timeout
* @param int $Engine
*/
abstract public function Open(string $Address, int $Port, int $Timeout, int $Engine): void;
/**
* @param int $Header
* @param string $String
*
* @return bool
*/
abstract public function Write(int $Header, string $String = ''): bool;
/**
* @param int $Length
*
* @return Buffer
*/
abstract public function Read(int $Length = 1400): Buffer;
/**
* @param Buffer $Buffer
* @param int $Length
* @param callable $SherlockFunction
*
* @return Buffer
*
* @throws InvalidPacketException
* @throws SocketException
*/
protected function ReadInternal(Buffer $Buffer, int $Length, callable $SherlockFunction): Buffer
{
if ($Buffer->Remaining() === 0) {
throw new InvalidPacketException('Failed to read any data from socket', InvalidPacketException::BUFFER_EMPTY);
}
$Header = $Buffer->GetLong();
// Single packet, do nothing.
if ($Header === -1) {
return $Buffer;
}
if ($Header === -2) { // Split packet
$Packets = [];
$IsCompressed = false;
$PacketChecksum = null;
do {
$RequestID = $Buffer->GetLong();
switch ($this->Engine) {
case SourceQuery::GOLDSOURCE:
{
$PacketCountAndNumber = $Buffer->GetByte();
$PacketCount = $PacketCountAndNumber & 0xF;
$PacketNumber = $PacketCountAndNumber >> 4;
break;
}
case SourceQuery::SOURCE:
{
$IsCompressed = ($RequestID & 0x80000000) !== 0;
$PacketCount = $Buffer->GetByte();
$PacketNumber = $Buffer->GetByte() + 1;
if ($IsCompressed) {
$Buffer->GetLong(); // Split size
$PacketChecksum = $Buffer->GetUnsignedLong();
} else {
$Buffer->GetShort(); // Split size
}
break;
}
default:
{
throw new SocketException('Unknown engine.', SocketException::INVALID_ENGINE);
}
}
$Packets[ $PacketNumber ] = $Buffer->Get();
$ReadMore = $PacketCount > count($Packets);
} while ($ReadMore && $SherlockFunction($Buffer, $Length));
$Data = implode($Packets);
// TODO: Test this
if ($IsCompressed) {
$Data = bzdecompress($Data);
if (!is_string($Data) || crc32($Data) !== $PacketChecksum) {
throw new InvalidPacketException('CRC32 checksum mismatch of uncompressed packet data.', InvalidPacketException::CHECKSUM_MISMATCH);
}
}
$Buffer->Set(substr($Data, 4));
} else {
throw new InvalidPacketException('Socket read: Raw packet header mismatch. (0x' . dechex($Header) . ')', InvalidPacketException::PACKET_HEADER_MISMATCH);
}
return $Buffer;
}
}

@ -29,28 +29,28 @@ final class Buffer
/**
* Buffer
*/
private string $Buffer = '';
private string $buffer = '';
/**
* Buffer length
*/
private int $Length = 0;
private int $length = 0;
/**
* Current position in buffer
*/
private int $Position = 0;
private int $position = 0;
/**
* Sets buffer
*
* @param string $Buffer
* @param string $buffer
*/
public function Set(string $Buffer): void
public function set(string $buffer): void
{
$this->Buffer = $Buffer;
$this->Length = strlen($Buffer);
$this->Position = 0;
$this->buffer = $buffer;
$this->length = strlen($buffer);
$this->position = 0;
}
/**
@ -58,9 +58,9 @@ final class Buffer
*
* @return int Remaining bytes in buffer
*/
public function Remaining(): int
public function remaining(): int
{
return $this->Length - $this->Position;
return $this->length - $this->position;
}
/**
@ -68,43 +68,43 @@ final class Buffer
*/
public function isEmpty(): bool
{
return $this->Remaining() <= 0;
return $this->remaining() <= 0;
}
/**
* Gets data from buffer
*
* @param int $Length Bytes to read
* @param int $length Bytes to read
*
* @return string
*/
public function Get(int $Length = -1): string
public function get(int $length = -1): string
{
if ($Length === 0) {
if ($length === 0) {
return '';
}
$Remaining = $this->Remaining();
$remaining = $this->remaining();
if ($Length === -1) {
$Length = $Remaining;
} elseif ($Length > $Remaining) {
if ($length === -1) {
$length = $remaining;
} elseif ($length > $remaining) {
return '';
}
$Data = substr($this->Buffer, $this->Position, $Length);
$data = substr($this->buffer, $this->position, $length);
$this->Position += $Length;
$this->position += $length;
return $Data;
return $data;
}
/**
* Get byte from buffer
*/
public function GetByte(): int
public function getByte(): int
{
return ord($this->Get(1));
return ord($this->get(1));
}
/**
@ -112,15 +112,15 @@ final class Buffer
*
* @throws InvalidPacketException
*/
public function GetShort(): int
public function getShort(): int
{
if ($this->Remaining() < 2) {
if ($this->remaining() < 2) {
throw new InvalidPacketException('Not enough data to unpack a short.', InvalidPacketException::BUFFER_EMPTY);
}
$Data = unpack('v', $this->Get(2));
$data = unpack('v', $this->get(2));
return (int)$Data[ 1 ];
return (int)$data[ 1 ];
}
/**
@ -128,15 +128,15 @@ final class Buffer
*
* @throws InvalidPacketException
*/
public function GetLong(): int
public function getLong(): int
{
if ($this->Remaining() < 4) {
if ($this->remaining() < 4) {
throw new InvalidPacketException('Not enough data to unpack a long.', InvalidPacketException::BUFFER_EMPTY);
}
$Data = unpack('l', $this->Get(4));
$data = unpack('l', $this->get(4));
return (int)$Data[ 1 ];
return (int)$data[ 1 ];
}
/**
@ -144,15 +144,15 @@ final class Buffer
*
* @throws InvalidPacketException
*/
public function GetFloat(): float
public function getFloat(): float
{
if ($this->Remaining() < 4) {
if ($this->remaining() < 4) {
throw new InvalidPacketException('Not enough data to unpack a float.', InvalidPacketException::BUFFER_EMPTY);
}
$Data = unpack('f', $this->Get(4));
$data = unpack('f', $this->get(4));
return (float)$Data[ 1 ];
return (float)$data[ 1 ];
}
/**
@ -160,32 +160,32 @@ final class Buffer
*
* @throws InvalidPacketException
*/
public function GetUnsignedLong(): int
public function getUnsignedLong(): int
{
if ($this->Remaining() < 4) {
if ($this->remaining() < 4) {
throw new InvalidPacketException('Not enough data to unpack an usigned long.', InvalidPacketException::BUFFER_EMPTY);
}
$Data = unpack('V', $this->Get(4));
$data = unpack('V', $this->get(4));
return (int)$Data[ 1 ];
return (int)$data[ 1 ];
}
/**
* Read one string from buffer ending with null byte
*/
public function GetString(): string
public function getString(): string
{
$ZeroBytePosition = strpos($this->Buffer, "\0", $this->Position);
$zeroBytePosition = strpos($this->buffer, "\0", $this->position);
if ($ZeroBytePosition === false) {
if ($zeroBytePosition === false) {
return '';
}
$String = $this->Get($ZeroBytePosition - $this->Position);
$string = $this->get($zeroBytePosition - $this->position);
$this->Position++;
$this->position++;
return $String;
return $string;
}
}

@ -1,171 +0,0 @@
<?php
declare(strict_types=1);
/**
* @author Pavel Djundik
*
* @link https://xpaw.me
* @link https://github.com/xPaw/PHP-Source-Query
*
* @license GNU Lesser General Public License, version 2.1
*
* @internal
*/
namespace xPaw\SourceQuery;
use xPaw\SourceQuery\Exception\AuthenticationException;
use xPaw\SourceQuery\Exception\InvalidPacketException;
/**
* Class GoldSourceRcon
*
* @package xPaw\SourceQuery
*
* @uses AuthenticationException
* @uses InvalidPacketException
*/
final class GoldSourceRcon
{
/**
* Points to socket class
*
* @var BaseSocket
*/
private BaseSocket $Socket;
/**
* @var string $RconPassword
*/
private string $RconPassword = '';
/**
* @var string $RconChallenge
*/
private string $RconChallenge = '';
/**
* @param BaseSocket $Socket
*/
public function __construct(BaseSocket $Socket)
{
$this->Socket = $Socket;
}
/**
* Close
*/
public function Close(): void
{
$this->RconChallenge = '';
$this->RconPassword = '';
}
/**
* Open
*/
public function Open(): void
{
}
/**
* @param string $String
*
* @return bool
*/
public function Write(string $String = ''): bool
{
$Command = pack('cccca*', 0xFF, 0xFF, 0xFF, 0xFF, $String);
$Length = strlen($Command);
return $Length === fwrite($this->Socket->Socket, $Command, $Length);
}
/**
* @throws AuthenticationException
* @throws InvalidPacketException
*
* @return Buffer
*/
public function Read(): Buffer
{
// GoldSource RCON has same structure as Query
$Buffer = $this->Socket->Read();
$StringBuffer = '';
// There is no indentifier of the end, so we just need to continue reading
do {
$ReadMore = $Buffer->Remaining() > 0;
if ($ReadMore) {
if ($Buffer->GetByte() !== SourceQuery::S2A_RCON) {
throw new InvalidPacketException('Invalid rcon response.', InvalidPacketException::PACKET_HEADER_MISMATCH);
}
$Packet = $Buffer->Get();
$StringBuffer .= $Packet;
//$StringBuffer .= SubStr( $Packet, 0, -2 );
// Let's assume if this packet is not long enough, there are no more after this one
$ReadMore = strlen($Packet) > 1000; // use 1300?
if ($ReadMore) {
$Buffer = $this->Socket->Read();
}
}
} while ($ReadMore);
$Trimmed = trim($StringBuffer);
if ($Trimmed === 'Bad rcon_password.') {
throw new AuthenticationException($Trimmed, AuthenticationException::BAD_PASSWORD);
} elseif ($Trimmed === 'You have been banned from this server.') {
throw new AuthenticationException($Trimmed, AuthenticationException::BANNED);
}
$Buffer->Set($Trimmed);
return $Buffer;
}
/**
* @param string $Command
*
* @return string
*
* @throws AuthenticationException
* @throws InvalidPacketException
*/
public function Command(string $Command): string
{
if (!$this->RconChallenge) {
throw new AuthenticationException('Tried to execute a RCON command before successful authorization.', AuthenticationException::BAD_PASSWORD);
}
$this->Write('rcon ' . $this->RconChallenge . ' "' . $this->RconPassword . '" ' . $Command . "\0");
$Buffer = $this->Read();
return $Buffer->Get();
}
/**
* @param string $Password
*
* @throws AuthenticationException
*/
public function Authorize(string $Password): void
{
$this->RconPassword = $Password;
$this->Write('challenge rcon');
$Buffer = $this->Socket->Read();
if ($Buffer->Get(14) !== 'challenge rcon') {
throw new AuthenticationException('Failed to get RCON challenge.', AuthenticationException::BAD_PASSWORD);
}
$this->RconChallenge = trim($Buffer->Get());
}
}

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace xPaw\SourceQuery\Rcon;
use xPaw\SourceQuery\Buffer;
use xPaw\SourceQuery\Exception\AuthenticationException;
use xPaw\SourceQuery\Exception\InvalidPacketException;
abstract class AbstractRcon implements RconInterface
{
/**
* @param int|null $header
* @param string $string
*
* @return bool
*/
abstract protected function write(?int $header, string $string = ''): bool;
/**
* @throws AuthenticationException
* @throws InvalidPacketException
*
* @return Buffer
*/
abstract protected function read(): Buffer;
}

@ -0,0 +1,175 @@
<?php
declare(strict_types=1);
/**
* @author Pavel Djundik
*
* @link https://xpaw.me
* @link https://github.com/xPaw/PHP-Source-Query
*
* @license GNU Lesser General Public License, version 2.1
*
* @internal
*/
namespace xPaw\SourceQuery\Rcon;
use xPaw\SourceQuery\Buffer;
use xPaw\SourceQuery\Exception\AuthenticationException;
use xPaw\SourceQuery\Exception\InvalidPacketException;
use xPaw\SourceQuery\Socket\SocketInterface;
use xPaw\SourceQuery\SourceQuery;
/**
* Class GoldSourceRcon
*
* @package xPaw\SourceQuery
*
* @uses AuthenticationException
* @uses InvalidPacketException
*/
final class GoldSourceRcon extends AbstractRcon
{
/**
* Points to socket class
*
* @var SocketInterface
*/
private SocketInterface $socket;
/**
* @var string
*/
private string $rconPassword = '';
/**
* @var string
*/
private string $rconChallenge = '';
/**
* @param SocketInterface $socket
*/
public function __construct(SocketInterface $socket)
{
$this->socket = $socket;
}
/**
* Close
*/
public function close(): void
{
$this->rconChallenge = '';
$this->rconPassword = '';
}
/**
* Open
*/
public function open(): void
{
}
/**
* @param string $command
*
* @return string
*
* @throws AuthenticationException
* @throws InvalidPacketException
*/
public function command(string $command): string
{
if (!$this->rconChallenge) {
throw new AuthenticationException('Tried to execute a RCON command before successful authorization.', AuthenticationException::BAD_PASSWORD);
}
$this->write(null, 'rcon ' . $this->rconChallenge . ' "' . $this->rconPassword . '" ' . $command . "\0");
$buffer = $this->read();
return $buffer->get();
}
/**
* @param string $password
*
* @throws AuthenticationException
*/
public function authorize(string $password): void
{
$this->rconPassword = $password;
$this->write(null, 'challenge rcon');
$buffer = $this->socket->read();
if ($buffer->get(14) !== 'challenge rcon') {
throw new AuthenticationException('Failed to get RCON challenge.', AuthenticationException::BAD_PASSWORD);
}
$this->rconChallenge = trim($buffer->get());
}
/**
* @param int|null $header
* @param string $string
*
* @return bool
*/
protected function write(?int $header, string $string = ''): bool
{
$command = pack('cccca*', 0xFF, 0xFF, 0xFF, 0xFF, $string);
$length = strlen($command);
return $length === fwrite($this->socket->getSocket(), $command, $length);
}
/**
* @throws AuthenticationException
* @throws InvalidPacketException
*
* @return Buffer
*/
protected function read(): Buffer
{
// GoldSource RCON has same structure as Query
$buffer = $this->socket->read();
$stringBuffer = '';
// There is no indentifier of the end, so we just need to continue reading
do {
$readMore = $buffer->remaining() > 0;
if ($readMore) {
if ($buffer->getByte() !== SourceQuery::S2A_RCON) {
throw new InvalidPacketException('Invalid rcon response.', InvalidPacketException::PACKET_HEADER_MISMATCH);
}
$packet = $buffer->get();
$stringBuffer .= $packet;
//$stringBuffer .= SubStr( $packet, 0, -2 );
// Let's assume if this packet is not long enough, there are no more after this one
$readMore = strlen($packet) > 1000; // use 1300?
if ($readMore) {
$buffer = $this->socket->read();
}
}
} while ($readMore);
$trimmed = trim($stringBuffer);
if ($trimmed === 'Bad rcon_password.') {
throw new AuthenticationException($trimmed, AuthenticationException::BAD_PASSWORD);
} elseif ($trimmed === 'You have been banned from this server.') {
throw new AuthenticationException($trimmed, AuthenticationException::BANNED);
}
$buffer->set($trimmed);
return $buffer;
}
}

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace xPaw\SourceQuery\Rcon;
use xPaw\SourceQuery\Exception\AuthenticationException;
use xPaw\SourceQuery\Exception\InvalidPacketException;
use xPaw\SourceQuery\Socket\SocketInterface;
interface RconInterface
{
/**
* @param SocketInterface $socket
*/
public function __construct(SocketInterface $socket);
/**
* Close
*/
public function close(): void;
/**
* Open
*/
public function open(): void;
/**
* @param string $command
*
* @return string
*
* @throws AuthenticationException
* @throws InvalidPacketException
*/
public function command(string $command): string;
/**
* @param string $password
*
* @throws AuthenticationException
*/
public function authorize(string $password): void;
}

@ -0,0 +1,235 @@
<?php
declare(strict_types=1);
/**
* @author Pavel Djundik
*
* @link https://xpaw.me
* @link https://github.com/xPaw/PHP-Source-Query
*
* @license GNU Lesser General Public License, version 2.1
*
* @internal
*/
namespace xPaw\SourceQuery\Rcon;
use xPaw\SourceQuery\Buffer;
use xPaw\SourceQuery\Exception\AuthenticationException;
use xPaw\SourceQuery\Exception\InvalidPacketException;
use xPaw\SourceQuery\Exception\SocketException;
use xPaw\SourceQuery\Socket\SocketInterface;
use xPaw\SourceQuery\SourceQuery;
/**
* Class SourceRcon
*
* @package xPaw\SourceQuery
*
* @uses AuthenticationException
* @uses InvalidPacketException
* @uses SocketException
*/
final class SourceRcon extends AbstractRcon
{
/**
* Points to socket class
*/
private SocketInterface $socket;
/**
* @var ?resource
*/
private $rconSocket;
/**
* @var int
*/
private int $rconRequestId = 0;
/**
* @param SocketInterface $socket
*/
public function __construct(SocketInterface $socket)
{
$this->socket = $socket;
}
/**
* Close
*/
public function close(): void
{
if ($this->rconSocket) {
fclose($this->rconSocket);
$this->rconSocket = null;
}
$this->rconRequestId = 0;
}
/**
* @throws SocketException
*/
public function open(): void
{
if (!$this->rconSocket) {
$rconSocket = @fsockopen(
$this->socket->getAddress(),
$this->socket->getPort(),
$errNo,
$errStr,
$this->socket->getTimeout()
);
if ($errNo || !$rconSocket) {
throw new SocketException('Can\'t connect to RCON server: ' . $errStr, SocketException::CONNECTION_FAILED);
}
$this->rconSocket = $rconSocket;
stream_set_timeout($this->rconSocket, $this->socket->getTimeout());
stream_set_blocking($this->rconSocket, true);
}
}
/**
* @param string $command
*
* @return string
*
* @throws AuthenticationException
* @throws InvalidPacketException
*/
public function command(string $command): string
{
$this->write(SourceQuery::SERVERDATA_EXECCOMMAND, $command);
$buffer = $this->read();
$buffer->getLong(); // RequestID
$type = $buffer->getLong();
if ($type === SourceQuery::SERVERDATA_AUTH_RESPONSE) {
throw new AuthenticationException('Bad rcon_password.', AuthenticationException::BAD_PASSWORD);
} elseif ($type !== SourceQuery::SERVERDATA_RESPONSE_VALUE) {
throw new InvalidPacketException('Invalid rcon response.', InvalidPacketException::PACKET_HEADER_MISMATCH);
}
$data = $buffer->get();
// We do this stupid hack to handle split packets
// See https://developer.valvesoftware.com/wiki/Source_RCON_Protocol#Multiple-packet_Responses
if (strlen($data) >= 4000) {
$this->write(SourceQuery::SERVERDATA_REQUESTVALUE);
do {
$buffer = $this->read();
$buffer->getLong(); // RequestID
if ($buffer->getLong() !== SourceQuery::SERVERDATA_RESPONSE_VALUE) {
break;
}
$data2 = $buffer->get();
if ($data2 === "\x00\x01\x00\x00\x00\x00") {
break;
}
$data .= $data2;
} while (true);
}
return rtrim($data, "\0");
}
/**
* @param string $password
*
* @throws AuthenticationException
* @throws InvalidPacketException
*/
public function authorize(string $password): void
{
$this->write(SourceQuery::SERVERDATA_AUTH, $password);
$buffer = $this->read();
$requestId = $buffer->getLong();
$type = $buffer->getLong();
// If we receive SERVERDATA_RESPONSE_VALUE, then we need to read again
// More info: https://developer.valvesoftware.com/wiki/Source_RCON_Protocol#Additional_Comments
if ($type === SourceQuery::SERVERDATA_RESPONSE_VALUE) {
$buffer = $this->read();
$requestId = $buffer->getLong();
$type = $buffer->getLong();
}
if ($requestId === -1 || $type !== SourceQuery::SERVERDATA_AUTH_RESPONSE) {
throw new AuthenticationException('RCON authorization failed.', AuthenticationException::BAD_PASSWORD);
}
}
/**
* @param int|null $header
* @param string $string
*
* @return bool
*/
protected function write(?int $header, string $string = ''): bool
{
// Pack the packet together
$command = pack('VV', ++$this->rconRequestId, $header) . $string . "\x00\x00";
// Prepend packet length
$command = pack('V', strlen($command)) . $command;
$length = strlen($command);
return $length === fwrite($this->rconSocket, $command, $length);
}
/**
* @return Buffer
*
* @throws InvalidPacketException
*/
protected function read(): Buffer
{
$buffer = new Buffer();
$buffer->set(fread($this->rconSocket, 4));
if ($buffer->remaining() < 4) {
throw new InvalidPacketException('Rcon read: Failed to read any data from socket', InvalidPacketException::BUFFER_EMPTY);
}
$packetSize = $buffer->getLong();
$buffer->set(fread($this->rconSocket, $packetSize));
$data = $buffer->get();
$remaining = $packetSize - strlen($data);
while ($remaining > 0) {
$data2 = fread($this->rconSocket, $remaining);
$packetSize = strlen($data2);
if ($packetSize === 0) {
throw new InvalidPacketException('Read ' . strlen($data) . ' bytes from socket, ' . $remaining . ' remaining', InvalidPacketException::BUFFER_EMPTY);
}
$data .= $data2;
$remaining -= $packetSize;
}
$buffer->set($data);
return $buffer;
}
}

@ -1,130 +0,0 @@
<?php
declare(strict_types=1);
/**
* @author Pavel Djundik
*
* @link https://xpaw.me
* @link https://github.com/xPaw/PHP-Source-Query
*
* @license GNU Lesser General Public License, version 2.1
*
* @internal
*/
namespace xPaw\SourceQuery;
use xPaw\SourceQuery\Exception\InvalidPacketException;
use xPaw\SourceQuery\Exception\SocketException;
/**
* Class Socket
*
* @package xPaw\SourceQuery
*
* @uses InvalidPacketException
* @uses SocketException
*/
final class Socket extends BaseSocket
{
/**
* Close
*/
public function Close(): void
{
if ($this->Socket !== null && $this->Socket !== 0) {
fclose($this->Socket);
$this->Socket = null;
}
}
/**
* @param string $Address
* @param int $Port
* @param int $Timeout
* @param int $Engine
*
* @throws SocketException
*/
public function Open(string $Address, int $Port, int $Timeout, int $Engine): void
{
$this->Timeout = $Timeout;
$this->Engine = $Engine;
$this->Port = $Port;
$this->Address = $Address;
$Socket = @fsockopen('udp://' . $Address, $Port, $ErrNo, $ErrStr, $Timeout);
if ($ErrNo || $Socket === false) {
throw new SocketException('Could not create socket: ' . $ErrStr, SocketException::COULD_NOT_CREATE_SOCKET);
}
$this->Socket = $Socket;
stream_set_timeout($this->Socket, $Timeout);
stream_set_blocking($this->Socket, true);
}
/**
* @param int $Header
* @param string $String
*
* @return bool
*/
public function Write(int $Header, string $String = ''): bool
{
$Command = pack('ccccca*', 0xFF, 0xFF, 0xFF, 0xFF, $Header, $String);
$Length = strlen($Command);
return $Length === fwrite($this->Socket, $Command, $Length);
}
/**
* Reads from socket and returns Buffer.
*
* @param int $Length
*
* @return Buffer Buffer
*
* @throws InvalidPacketException
* @throws SocketException
*
*/
public function Read(int $Length = 1400): Buffer
{
$Buffer = new Buffer();
$data = fread($this->Socket, $Length);
if (!$data) {
throw new SocketException('Failed to open socket.');
}
$Buffer->Set($data);
$this->ReadInternal($Buffer, $Length, [ $this, 'Sherlock' ]);
return $Buffer;
}
/**
* @param Buffer $Buffer
* @param int $Length
*
* @return bool
*
* @throws InvalidPacketException
*/
public function Sherlock(Buffer $Buffer, int $Length): bool
{
$Data = fread($this->Socket, $Length);
if (strlen($Data) < 4) {
return false;
}
$Buffer->Set($Data);
return $Buffer->GetLong() === -2;
}
}

@ -0,0 +1,285 @@
<?php
declare(strict_types=1);
/**
* @author Pavel Djundik
*
* @link https://xpaw.me
* @link https://github.com/xPaw/PHP-Source-Query
*
* @license GNU Lesser General Public License, version 2.1
*
* @internal
*/
namespace xPaw\SourceQuery\Socket;
use xPaw\SourceQuery\Buffer;
use xPaw\SourceQuery\Exception\InvalidArgumentException;
use xPaw\SourceQuery\Exception\InvalidPacketException;
use xPaw\SourceQuery\Exception\SocketException;
/**
* Base socket
*
* @package xPaw\SourceQuery
*
* @uses InvalidPacketException
* @uses SocketException
*/
abstract class AbstractSocket implements SocketInterface
{
/**
* @var resource|null
*/
public $socket;
/**
* @var int
*/
public int $engine = SocketType::SOURCE;
/**
* @var string $address
*/
public string $address = '';
/**
* @var int $port
*/
public int $port = 0;
/**
* @var int $timeout
*/
public int $timeout = 0;
/**
* Destructor
*/
public function __destruct()
{
$this->close();
}
/**
* @return string
*/
public function getAddress(): string
{
return $this->address;
}
/**
* @return int
*/
public function getPort(): int
{
return $this->port;
}
/**
* @return int
*/
public function getTimeout(): int
{
return $this->timeout;
}
/**
* @return resource
*
* @throws InvalidArgumentException
*/
public function getSocket()
{
if (!$this->socket) {
throw new InvalidArgumentException('Socket not open.');
}
return $this->socket;
}
/**
* Close
*/
public function close(): void
{
if ($this->socket) {
fclose($this->socket);
$this->socket = null;
}
}
/**
* @param string $address
* @param int $port
* @param int $timeout
* @param int $engine
*
* @throws SocketException
*/
public function open(string $address, int $port, int $timeout, int $engine): void
{
$this->timeout = $timeout;
$this->engine = $engine;
$this->port = $port;
$this->address = $address;
$socket = @fsockopen('udp://' . $address, $port, $errNo, $errStr, $timeout);
if ($errNo || $socket === false) {
throw new SocketException('Could not create socket: ' . $errStr, SocketException::COULD_NOT_CREATE_SOCKET);
}
$this->socket = $socket;
stream_set_timeout($this->socket, $timeout);
stream_set_blocking($this->socket, true);
}
/**
* @param int $header
* @param string $string
*
* @return bool
*/
public function write(int $header, string $string = ''): bool
{
$command = pack('ccccca*', 0xFF, 0xFF, 0xFF, 0xFF, $header, $string);
$length = strlen($command);
return $length === fwrite($this->socket, $command, $length);
}
/**
* Reads from socket and returns Buffer.
*
* @param int $length
*
* @return Buffer Buffer
*
* @throws InvalidPacketException
* @throws SocketException
*
*/
public function read(int $length = 1400): Buffer
{
$buffer = new Buffer();
$data = fread($this->socket, $length);
if (!$data) {
throw new SocketException('Failed to open socket.');
}
$buffer->set($data);
$this->readInternal($buffer, $length, [ $this, 'sherlock' ]);
return $buffer;
}
/**
* @param Buffer $buffer
* @param int $length
*
* @return bool
*
* @throws InvalidPacketException
*/
public function sherlock(Buffer $buffer, int $length): bool
{
$data = fread($this->socket, $length);
if (strlen($data) < 4) {
return false;
}
$buffer->set($data);
return $buffer->getLong() === -2;
}
/**
*
* Get packet data (count, number, checksum) from the buffer. Different for goldsrc/src.
*
* @param Buffer $buffer
* @param int $count
* @param int $number
* @param bool $isCompressed
* @param int|null $checksum
*/
abstract protected function readInternalPacketData(
Buffer $buffer,
int &$count,
int &$number,
bool &$isCompressed,
?int &$checksum
): void;
/**
* @param Buffer $buffer
* @param int $length
* @param callable $sherlockFunction
*
* @return Buffer
*
* @throws InvalidPacketException
*/
protected function readInternal(Buffer $buffer, int $length, callable $sherlockFunction): Buffer
{
if ($buffer->remaining() === 0) {
throw new InvalidPacketException('Failed to read any data from socket', InvalidPacketException::BUFFER_EMPTY);
}
$header = $buffer->getLong();
// Single packet, do nothing.
if ($header === -1) {
return $buffer;
}
if ($header === -2) { // Split packet
$packets = [];
$packetCount = 0;
$packetNumber = 0;
$packetChecksum = null;
do {
$requestId = $buffer->getLong();
$isCompressed = ($requestId & 0x80000000) !== 0;
$this->readInternalPacketData(
$buffer,
$packetCount,
$packetNumber,
$isCompressed,
$packetChecksum
);
$packets[$packetNumber] = $buffer->get();
$readMore = $packetCount > count($packets);
} while ($readMore && $sherlockFunction($buffer, $length));
$data = implode($packets);
// TODO: Test this
if ($isCompressed) {
$data = bzdecompress($data);
if (!is_string($data) || crc32($data) !== $packetChecksum) {
throw new InvalidPacketException('CRC32 checksum mismatch of uncompressed packet data.', InvalidPacketException::CHECKSUM_MISMATCH);
}
}
$buffer->set(substr($data, 4));
} else {
throw new InvalidPacketException('Socket read: Raw packet header mismatch. (0x' . dechex($header) . ')', InvalidPacketException::PACKET_HEADER_MISMATCH);
}
return $buffer;
}
}

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace xPaw\SourceQuery\Socket;
use xPaw\SourceQuery\Buffer;
final class GoldSourceSocket extends AbstractSocket
{
/**
* @return int
*/
public function getType(): int
{
return SocketType::GOLDSOURCE;
}
/**
* @param Buffer $buffer
* @param int $count
* @param int $number
* @param bool $isCompressed
* @param int|null $checksum
*/
protected function readInternalPacketData(
Buffer $buffer,
int &$count,
int &$number,
bool &$isCompressed,
?int &$checksum
): void {
$packetCountAndNumber = $buffer->getByte();
$count = $packetCountAndNumber & 0xF;
$number = $packetCountAndNumber >> 4;
$isCompressed = false;
}
}

@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
/**
* @author Pavel Djundik
*
* @link https://xpaw.me
* @link https://github.com/xPaw/PHP-Source-Query
*
* @license GNU Lesser General Public License, version 2.1
*
* @internal
*/
namespace xPaw\SourceQuery\Socket;
use xPaw\SourceQuery\Buffer;
/**
* Base socket interface
*
* @package xPaw\SourceQuery\Socket
*/
interface SocketInterface
{
/**
* @return string
*/
public function getAddress(): string;
/**
* @return int
*/
public function getPort(): int;
/**
* @return int
*/
public function getTimeout(): int;
/**
* @return resource
*/
public function getSocket();
/**
* Get the socket type (goldsrc/src).
*/
public function getType(): int;
/**
* Close
*/
public function close(): void;
/**
* @param string $address
* @param int $port
* @param int $timeout
* @param int $engine
*/
public function open(string $address, int $port, int $timeout, int $engine): void;
/**
* @param int $header
* @param string $string
*
* @return bool
*/
public function write(int $header, string $string = ''): bool;
/**
* @param int $length
*
* @return Buffer
*/
public function read(int $length = 1400): Buffer;
}

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace xPaw\SourceQuery\Socket;
abstract class SocketType
{
public const GOLDSOURCE = 0;
public const SOURCE = 1;
}

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace xPaw\SourceQuery\Socket;
use xPaw\SourceQuery\Buffer;
use xPaw\SourceQuery\Exception\InvalidPacketException;
final class SourceSocket extends AbstractSocket
{
/**
* @return int
*/
public function getType(): int
{
return SocketType::SOURCE;
}
/**
* @param Buffer $buffer
* @param int $count
* @param int $number
* @param bool $isCompressed
* @param int|null $checksum
*
* @throws InvalidPacketException
*/
protected function readInternalPacketData(
Buffer $buffer,
int &$count,
int &$number,
bool &$isCompressed,
?int &$checksum
): void {
$count = $buffer->getByte();
$number = $buffer->getByte() + 1;
if ($isCompressed) {
$buffer->getLong(); // Split size
$checksum = $buffer->getUnsignedLong();
} else {
$buffer->getShort(); // Split size
}
}
}

@ -0,0 +1,211 @@
<?php
declare(strict_types=1);
namespace xPaw\SourceQuery\Socket;
use SplQueue;
use SplDoublyLinkedList;
use xPaw\SourceQuery\Buffer;
use xPaw\SourceQuery\Exception\InvalidPacketException;
final class TestableSocket extends AbstractSocket
{
/**
* @var SplQueue<string>
*/
private SplQueue $packetQueue;
/**
* @var int
*/
private int $type;
/**
* TestableSocket constructor.
*
* @param int $type
*/
public function __construct(int $type)
{
$this->packetQueue = new SplQueue();
$this->packetQueue->setIteratorMode(SplDoublyLinkedList::IT_MODE_DELETE);
$this->type = $type;
}
/**
* @return int
*/
public function getType(): int
{
return $this->type;
}
/**
* @param string $data
*/
public function queue(string $data): void
{
$this->packetQueue->push($data);
}
/**
* Close.
*/
public function close(): void
{
}
/**
* @param string $address
* @param int $port
* @param int $timeout
* @param int $engine
*/
public function open(string $address, int $port, int $timeout, int $engine): void
{
$this->timeout = $timeout;
$this->engine = $engine;
$this->port = $port;
$this->address = $address;
}
/**
* @param int $header
* @param string $string
*
* @return bool
*/
public function write(int $header, string $string = ''): bool
{
return true;
}
/**
* @param int $length
*
* @return Buffer
*
* @throws InvalidPacketException
*/
public function read(int $length = 1400): Buffer
{
$buffer = new Buffer();
$buffer->set($this->packetQueue->shift());
$this->readInternal($buffer, $length, [ $this, 'sherlock' ]);
return $buffer;
}
/**
* @param Buffer $buffer
* @param int $length
*
* @return bool
*
* @throws InvalidPacketException
*/
public function sherlock(Buffer $buffer, int $length): bool
{
if ($this->packetQueue->isEmpty()) {
return false;
}
$buffer->set($this->packetQueue->shift());
return $buffer->getLong() === -2;
}
/**
* @param Buffer $buffer
* @param int $count
* @param int $number
* @param bool $isCompressed
* @param int|null $checksum
*
* @throws InvalidPacketException
*/
protected function readInternalPacketData(
Buffer $buffer,
int &$count,
int &$number,
bool &$isCompressed,
?int &$checksum
): void {
switch ($this->type) {
case SocketType::GOLDSOURCE:
$this->readInternalPacketDataGoldSource(
$buffer,
$count,
$number,
$isCompressed,
$checksum
);
break;
case SocketType::SOURCE:
default:
$this->readInternalPacketDataSource(
$buffer,
$count,
$number,
$isCompressed,
$checksum
);
}
}
/**
* Same as GoldSourceSocket::readInternalPacketData.
*
* @param Buffer $buffer
* @param int $count
* @param int $number
* @param bool $isCompressed
* @param int|null $checksum
*/
protected function readInternalPacketDataGoldSource(
Buffer $buffer,
int &$count,
int &$number,
bool &$isCompressed,
?int &$checksum
): void {
$packetCountAndNumber = $buffer->getByte();
$count = $packetCountAndNumber & 0xF;
$number = $packetCountAndNumber >> 4;
$isCompressed = false;
}
/**
* Same as SourceSocket::readInternalPacketData.
*
* @param Buffer $buffer
* @param int $count
* @param int $number
* @param bool $isCompressed
* @param int|null $checksum
*
* @throws InvalidPacketException
*/
protected function readInternalPacketDataSource(
Buffer $buffer,
int &$count,
int &$number,
bool &$isCompressed,
?int &$checksum
): void {
$count = $buffer->getByte();
$number = $buffer->getByte() + 1;
if ($isCompressed) {
$buffer->getLong(); // Split size
$checksum = $buffer->getUnsignedLong();
} else {
$buffer->getShort(); // Split size
}
}
}

@ -20,6 +20,11 @@ use xPaw\SourceQuery\Exception\AuthenticationException;
use xPaw\SourceQuery\Exception\InvalidArgumentException;
use xPaw\SourceQuery\Exception\InvalidPacketException;
use xPaw\SourceQuery\Exception\SocketException;
use xPaw\SourceQuery\Rcon\GoldSourceRcon;
use xPaw\SourceQuery\Rcon\RconInterface;
use xPaw\SourceQuery\Rcon\SourceRcon;
use xPaw\SourceQuery\Socket\SocketInterface;
use xPaw\SourceQuery\Socket\SocketType;
/**
* Class SourceQuery
@ -33,12 +38,6 @@ use xPaw\SourceQuery\Exception\SocketException;
*/
final class SourceQuery
{
/**
* Engines
*/
public const GOLDSOURCE = 0;
public const SOURCE = 1;
/**
* Packets sent
*/
@ -75,36 +74,36 @@ final class SourceQuery
/**
* Points to rcon class
*
* @var SourceRcon|GoldSourceRcon|null
* @var RconInterface|null
*/
private $Rcon;
private $rcon;
/**
* Points to socket class
*/
private BaseSocket $Socket;
private SocketInterface $socket;
/**
* True if connection is open, false if not
*/
private bool $Connected = false;
private bool $connected = false;
/**
* Contains challenge
*/
private string $Challenge = '';
private string $challenge = '';
/**
* Use old method for getting challenge number
*/
private bool $UseOldGetChallengeMethod = false;
private bool $useOldGetChallengeMethod = false;
/**
* @param BaseSocket|null $Socket
* @param SocketInterface $socket
*/
public function __construct(BaseSocket $Socket = null)
public function __construct(SocketInterface $socket)
{
$this->Socket = $Socket ?: new Socket();
$this->socket = $socket;
}
/**
@ -112,63 +111,63 @@ final class SourceQuery
*/
public function __destruct()
{
$this->Disconnect();
$this->disconnect();
}
/**
* Opens connection to server
*
* @param string $Address Server ip
* @param int $Port Server port
* @param int $Timeout Timeout period
* @param int $Engine Engine the server runs on (goldsource, source)
* @param string $address Server ip
* @param int $port Server port
* @param int $timeout Timeout period
* @param int $engine Engine the server runs on (goldsource, source)
*
* @throws InvalidArgumentException
* @throws SocketException
*/
public function Connect(string $Address, int $Port, int $Timeout = 3, int $Engine = self::SOURCE): void
public function connect(string $address, int $port, int $timeout = 3, int $engine = SocketType::SOURCE): void
{
$this->Disconnect();
$this->disconnect();
if ($Timeout < 0) {
if ($timeout < 0) {
throw new InvalidArgumentException('Timeout must be a positive integer.', InvalidArgumentException::TIMEOUT_NOT_INTEGER);
}
$this->Socket->Open($Address, $Port, $Timeout, $Engine);
$this->socket->open($address, $port, $timeout, $engine);
$this->Connected = true;
$this->connected = true;
}
/**
* Forces GetChallenge to use old method for challenge retrieval because some games use outdated protocol (e.g Starbound)
*
* @param bool $Value Set to true to force old method
* @param bool $value Set to true to force old method
*
* @return bool Previous value
*/
public function SetUseOldGetChallengeMethod(bool $Value): bool
public function SetUseOldGetChallengeMethod(bool $value): bool
{
$Previous = $this->UseOldGetChallengeMethod;
$previous = $this->useOldGetChallengeMethod;
$this->UseOldGetChallengeMethod = $Value === true;
$this->useOldGetChallengeMethod = $value === true;
return $Previous;
return $previous;
}
/**
* Closes all open connections
*/
public function Disconnect(): void
public function disconnect(): void
{
$this->Connected = false;
$this->Challenge = '';
$this->connected = false;
$this->challenge = '';
$this->Socket->Close();
$this->socket->close();
if ($this->Rcon) {
$this->Rcon->Close();
if ($this->rcon) {
$this->rcon->close();
$this->Rcon = null;
$this->rcon = null;
}
}
@ -181,16 +180,16 @@ final class SourceQuery
*
* @return bool True on success, false on failure
*/
public function Ping(): bool
public function ping(): bool
{
if (!$this->Connected) {
if (!$this->connected) {
throw new SocketException('Not connected.', SocketException::NOT_CONNECTED);
}
$this->Socket->Write(self::A2A_PING);
$Buffer = $this->Socket->Read();
$this->socket->write(self::A2A_PING);
$buffer = $this->socket->read();
return $Buffer->GetByte() === self::A2A_ACK;
return $buffer->getByte() === self::A2A_ACK;
}
/**
@ -201,154 +200,154 @@ final class SourceQuery
*
* @return array Returns an array with information on success
*/
public function GetInfo(): array
public function getInfo(): array
{
if (!$this->Connected) {
if (!$this->connected) {
throw new SocketException('Not connected.', SocketException::NOT_CONNECTED);
}
if ($this->Challenge) {
$this->Socket->Write(self::A2S_INFO, "Source Engine Query\0" . $this->Challenge);
if ($this->challenge) {
$this->socket->write(self::A2S_INFO, "Source Engine Query\0" . $this->challenge);
} else {
$this->Socket->Write(self::A2S_INFO, "Source Engine Query\0");
$this->socket->write(self::A2S_INFO, "Source Engine Query\0");
}
$Buffer = $this->Socket->Read();
$Type = $Buffer->GetByte();
$Server = [];
$buffer = $this->socket->read();
$type = $buffer->getByte();
$server = [];
if ($Type === self::S2C_CHALLENGE) {
$this->Challenge = $Buffer->Get(4);
if ($type === self::S2C_CHALLENGE) {
$this->challenge = $buffer->get(4);
$this->Socket->Write(self::A2S_INFO, "Source Engine Query\0" . $this->Challenge);
$Buffer = $this->Socket->Read();
$Type = $Buffer->GetByte();
$this->socket->write(self::A2S_INFO, "Source Engine Query\0" . $this->challenge);
$buffer = $this->socket->read();
$type = $buffer->getByte();
}
// Old GoldSource protocol, HLTV still uses it
if ($Type === self::S2A_INFO_OLD && $this->Socket->Engine === self::GOLDSOURCE) {
if ($type === self::S2A_INFO_OLD && $this->socket->getType() === SocketType::GOLDSOURCE) {
/**
* If we try to read data again, and we get the result with type S2A_INFO (0x49)
* That means this server is running dproto,
* Because it sends answer for both protocols
*/
$Server[ 'Address' ] = $Buffer->GetString();
$Server[ 'HostName' ] = $Buffer->GetString();
$Server[ 'Map' ] = $Buffer->GetString();
$Server[ 'ModDir' ] = $Buffer->GetString();
$Server[ 'ModDesc' ] = $Buffer->GetString();
$Server[ 'Players' ] = $Buffer->GetByte();
$Server[ 'MaxPlayers' ] = $Buffer->GetByte();
$Server[ 'Protocol' ] = $Buffer->GetByte();
$Server[ 'Dedicated' ] = chr($Buffer->GetByte());
$Server[ 'Os' ] = chr($Buffer->GetByte());
$Server[ 'Password' ] = $Buffer->GetByte() === 1;
$Server[ 'IsMod' ] = $Buffer->GetByte() === 1;
if ($Server[ 'IsMod' ]) {
$server[ 'Address' ] = $buffer->getString();
$server[ 'HostName' ] = $buffer->getString();
$server[ 'Map' ] = $buffer->getString();
$server[ 'ModDir' ] = $buffer->getString();
$server[ 'ModDesc' ] = $buffer->getString();
$server[ 'Players' ] = $buffer->getByte();
$server[ 'MaxPlayers' ] = $buffer->getByte();
$server[ 'Protocol' ] = $buffer->getByte();
$server[ 'Dedicated' ] = chr($buffer->getByte());
$server[ 'Os' ] = chr($buffer->getByte());
$server[ 'Password' ] = $buffer->getByte() === 1;
$server[ 'IsMod' ] = $buffer->getByte() === 1;
if ($server[ 'IsMod' ]) {
$Mod = [];
$Mod[ 'Url' ] = $Buffer->GetString();
$Mod[ 'Download' ] = $Buffer->GetString();
$Buffer->Get(1); // NULL byte
$Mod[ 'Version' ] = $Buffer->GetLong();
$Mod[ 'Size' ] = $Buffer->GetLong();
$Mod[ 'ServerSide' ] = $Buffer->GetByte() === 1;
$Mod[ 'CustomDLL' ] = $Buffer->GetByte() === 1;
$Server[ 'Mod' ] = $Mod;
$Mod[ 'Url' ] = $buffer->getString();
$Mod[ 'Download' ] = $buffer->getString();
$buffer->get(1); // NULL byte
$Mod[ 'Version' ] = $buffer->getLong();
$Mod[ 'Size' ] = $buffer->getLong();
$Mod[ 'ServerSide' ] = $buffer->getByte() === 1;
$Mod[ 'CustomDLL' ] = $buffer->getByte() === 1;
$server[ 'Mod' ] = $Mod;
}
$Server[ 'Secure' ] = $Buffer->GetByte() === 1;
$Server[ 'Bots' ] = $Buffer->GetByte();
$server[ 'Secure' ] = $buffer->getByte() === 1;
$server[ 'Bots' ] = $buffer->getByte();
return $Server;
return $server;
}
if ($Type !== self::S2A_INFO_SRC) {
throw new InvalidPacketException('GetInfo: Packet header mismatch. (0x' . dechex($Type) . ')', InvalidPacketException::PACKET_HEADER_MISMATCH);
if ($type !== self::S2A_INFO_SRC) {
throw new InvalidPacketException('GetInfo: Packet header mismatch. (0x' . dechex($type) . ')', InvalidPacketException::PACKET_HEADER_MISMATCH);
}
$Server[ 'Protocol' ] = $Buffer->GetByte();
$Server[ 'HostName' ] = $Buffer->GetString();
$Server[ 'Map' ] = $Buffer->GetString();
$Server[ 'ModDir' ] = $Buffer->GetString();
$Server[ 'ModDesc' ] = $Buffer->GetString();
$Server[ 'AppID' ] = $Buffer->GetShort();
$Server[ 'Players' ] = $Buffer->GetByte();
$Server[ 'MaxPlayers' ] = $Buffer->GetByte();
$Server[ 'Bots' ] = $Buffer->GetByte();
$Server[ 'Dedicated' ] = chr($Buffer->GetByte());
$Server[ 'Os' ] = chr($Buffer->GetByte());
$Server[ 'Password' ] = $Buffer->GetByte() === 1;
$Server[ 'Secure' ] = $Buffer->GetByte() === 1;
$server[ 'Protocol' ] = $buffer->getByte();
$server[ 'HostName' ] = $buffer->getString();
$server[ 'Map' ] = $buffer->getString();
$server[ 'ModDir' ] = $buffer->getString();
$server[ 'ModDesc' ] = $buffer->getString();
$server[ 'AppID' ] = $buffer->getShort();
$server[ 'Players' ] = $buffer->getByte();
$server[ 'MaxPlayers' ] = $buffer->getByte();
$server[ 'Bots' ] = $buffer->getByte();
$server[ 'Dedicated' ] = chr($buffer->getByte());
$server[ 'Os' ] = chr($buffer->getByte());
$server[ 'Password' ] = $buffer->getByte() === 1;
$server[ 'Secure' ] = $buffer->getByte() === 1;
// The Ship (they violate query protocol spec by modifying the response)
if ($Server[ 'AppID' ] === 2400) {
$Server[ 'GameMode' ] = $Buffer->GetByte();
$Server[ 'WitnessCount' ] = $Buffer->GetByte();
$Server[ 'WitnessTime' ] = $Buffer->GetByte();
if ($server[ 'AppID' ] === 2400) {
$server[ 'GameMode' ] = $buffer->getByte();
$server[ 'WitnessCount' ] = $buffer->getByte();
$server[ 'WitnessTime' ] = $buffer->getByte();
}
$Server[ 'Version' ] = $Buffer->GetString();
$server[ 'Version' ] = $buffer->getString();
// Extra Data Flags
if ($Buffer->Remaining() > 0) {
$Server[ 'ExtraDataFlags' ] = $Flags = $Buffer->GetByte();
if ($buffer->remaining() > 0) {
$server[ 'ExtraDataFlags' ] = $Flags = $buffer->getByte();
// S2A_EXTRA_DATA_HAS_GAME_PORT - Next 2 bytes include the game port.
if ($Flags & 0x80) {
$Server[ 'GamePort' ] = $Buffer->GetShort();
$server[ 'GamePort' ] = $buffer->getShort();
}
// S2A_EXTRA_DATA_HAS_STEAMID - Next 8 bytes are the steamID
// Want to play around with this?
// You can use https://github.com/xPaw/SteamID.php
if ($Flags & 0x10) {
$SteamIDLower = $Buffer->GetUnsignedLong();
$SteamIDInstance = $Buffer->GetUnsignedLong(); // This gets shifted by 32 bits, which should be steamid instance
$steamIdLower = $buffer->getUnsignedLong();
$steamIdInstance = $buffer->getUnsignedLong(); // This gets shifted by 32 bits, which should be steamid instance
if (PHP_INT_SIZE === 4) {
if (extension_loaded('gmp')) {
$SteamIDLower = gmp_abs($SteamIDLower);
$SteamIDInstance = gmp_abs($SteamIDInstance);
$SteamID = gmp_strval(gmp_or($SteamIDLower, gmp_mul($SteamIDInstance, gmp_pow(2, 32))));
$steamIdLower = gmp_abs($steamIdLower);
$steamIdInstance = gmp_abs($steamIdInstance);
$steamId = gmp_strval(gmp_or($steamIdLower, gmp_mul($steamIdInstance, gmp_pow(2, 32))));
} else {
throw new RuntimeException('Either 64-bit PHP installation or "gmp" module is required to correctly parse server\'s steamid.');
}
} else {
$SteamID = $SteamIDLower | ($SteamIDInstance << 32);
$steamId = $steamIdLower | ($steamIdInstance << 32);
}
$Server[ 'SteamID' ] = $SteamID;
$server[ 'SteamID' ] = $steamId;
unset($SteamIDLower, $SteamIDInstance, $SteamID);
unset($steamIdLower, $steamIdInstance, $steamId);
}
// S2A_EXTRA_DATA_HAS_SPECTATOR_DATA - Next 2 bytes include the spectator port, then the spectator server name.
if ($Flags & 0x40) {
$Server[ 'SpecPort' ] = $Buffer->GetShort();
$Server[ 'SpecName' ] = $Buffer->GetString();
$server[ 'SpecPort' ] = $buffer->getShort();
$server[ 'SpecName' ] = $buffer->getString();
}
// S2A_EXTRA_DATA_HAS_GAMETAG_DATA - Next bytes are the game tag string
if ($Flags & 0x20) {
$Server[ 'GameTags' ] = $Buffer->GetString();
$server[ 'GameTags' ] = $buffer->getString();
}
// S2A_EXTRA_DATA_GAMEID - Next 8 bytes are the gameID of the server
if ($Flags & 0x01) {
$Server[ 'GameID' ] = $Buffer->GetUnsignedLong() | ($Buffer->GetUnsignedLong() << 32);
$server[ 'GameID' ] = $buffer->getUnsignedLong() | ($buffer->getUnsignedLong() << 32);
}
if (!$Buffer->isEmpty()) {
if (!$buffer->isEmpty()) {
throw new InvalidPacketException(
'GetInfo: unread data? ' . $Buffer->Remaining() . ' bytes remaining in the buffer. Please report it to the library developer.',
'GetInfo: unread data? ' . $buffer->remaining() . ' bytes remaining in the buffer. Please report it to the library developer.',
InvalidPacketException::BUFFER_NOT_EMPTY
);
}
}
return $Server;
return $server;
}
/**
@ -359,39 +358,39 @@ final class SourceQuery
*
* @return array Returns an array with players on success
*/
public function GetPlayers(): array
public function getPlayers(): array
{
if (!$this->Connected) {
if (!$this->connected) {
throw new SocketException('Not connected.', SocketException::NOT_CONNECTED);
}
$this->GetChallenge(self::A2S_PLAYER, self::S2A_PLAYER);
$this->getChallenge(self::A2S_PLAYER, self::S2A_PLAYER);
$this->Socket->Write(self::A2S_PLAYER, $this->Challenge);
$Buffer = $this->Socket->Read(14000); // Moronic Arma 3 developers do not split their packets, so we have to read more data
$this->socket->write(self::A2S_PLAYER, $this->challenge);
$buffer = $this->socket->read(14000); // Moronic Arma 3 developers do not split their packets, so we have to read more data
// This violates the protocol spec, and they probably should fix it: https://developer.valvesoftware.com/wiki/Server_queries#Protocol
$Type = $Buffer->GetByte();
$type = $buffer->getByte();
if ($Type !== self::S2A_PLAYER) {
throw new InvalidPacketException('GetPlayers: Packet header mismatch. (0x' . dechex($Type) . ')', InvalidPacketException::PACKET_HEADER_MISMATCH);
if ($type !== self::S2A_PLAYER) {
throw new InvalidPacketException('GetPlayers: Packet header mismatch. (0x' . dechex($type) . ')', InvalidPacketException::PACKET_HEADER_MISMATCH);
}
$Players = [];
$Count = $Buffer->GetByte();
$players = [];
$count = $buffer->getByte();
while ($Count-- > 0 && $Buffer->Remaining() > 0) {
$Player = [];
$Player[ 'Id' ] = $Buffer->GetByte(); // PlayerID, is it just always 0?
$Player[ 'Name' ] = $Buffer->GetString();
$Player[ 'Frags' ] = $Buffer->GetLong();
$Player[ 'Time' ] = (int)$Buffer->GetFloat();
$Player[ 'TimeF' ] = gmdate(($Player[ 'Time' ] > 3600 ? 'H:i:s' : 'i:s'), $Player[ 'Time' ]);
while ($count-- > 0 && $buffer->remaining() > 0) {
$player = [];
$player[ 'Id' ] = $buffer->getByte(); // PlayerID, is it just always 0?
$player[ 'Name' ] = $buffer->getString();
$player[ 'Frags' ] = $buffer->getLong();
$player[ 'Time' ] = (int)$buffer->getFloat();
$player[ 'TimeF' ] = gmdate(($player[ 'Time' ] > 3600 ? 'H:i:s' : 'i:s'), $player[ 'Time' ]);
$Players[ ] = $Player;
$players[] = $player;
}
return $Players;
return $players;
}
/**
@ -402,70 +401,70 @@ final class SourceQuery
*
* @return array Returns an array with rules on success
*/
public function GetRules(): array
public function getRules(): array
{
if (!$this->Connected) {
if (!$this->connected) {
throw new SocketException('Not connected.', SocketException::NOT_CONNECTED);
}
$this->GetChallenge(self::A2S_RULES, self::S2A_RULES);
$this->getChallenge(self::A2S_RULES, self::S2A_RULES);
$this->Socket->Write(self::A2S_RULES, $this->Challenge);
$Buffer = $this->Socket->Read();
$this->socket->write(self::A2S_RULES, $this->challenge);
$buffer = $this->socket->read();
$Type = $Buffer->GetByte();
$type = $buffer->getByte();
if ($Type !== self::S2A_RULES) {
throw new InvalidPacketException('GetRules: Packet header mismatch. (0x' . dechex($Type) . ')', InvalidPacketException::PACKET_HEADER_MISMATCH);
if ($type !== self::S2A_RULES) {
throw new InvalidPacketException('GetRules: Packet header mismatch. (0x' . dechex($type) . ')', InvalidPacketException::PACKET_HEADER_MISMATCH);
}
$Rules = [];
$Count = $Buffer->GetShort();
$rules = [];
$count = $buffer->getShort();
while ($Count-- > 0 && $Buffer->Remaining() > 0) {
$Rule = $Buffer->GetString();
$Value = $Buffer->GetString();
while ($count-- > 0 && $buffer->remaining() > 0) {
$rule = $buffer->getString();
$value = $buffer->getString();
if (!empty($Rule)) {
$Rules[ $Rule ] = $Value;
if (!empty($rule)) {
$rules[$rule] = $value;
}
}
return $Rules;
return $rules;
}
/**
* Get challenge (used for players/rules packets)
*
* @param int $Header
* @param int $ExpectedResult
* @param int $header
* @param int $expectedResult
*
* @throws InvalidPacketException
* @throws SocketException
*/
private function GetChallenge(int $Header, int $ExpectedResult): void
private function getChallenge(int $header, int $expectedResult): void
{
if ($this->Challenge) {
if ($this->challenge) {
return;
}
if ($this->UseOldGetChallengeMethod) {
$Header = self::A2S_SERVERQUERY_GETCHALLENGE;
if ($this->useOldGetChallengeMethod) {
$header = self::A2S_SERVERQUERY_GETCHALLENGE;
}
$this->Socket->Write($Header, "\xFF\xFF\xFF\xFF");
$Buffer = $this->Socket->Read();
$this->socket->write($header, "\xFF\xFF\xFF\xFF");
$buffer = $this->socket->read();
$Type = $Buffer->GetByte();
$type = $buffer->getByte();
switch ($Type) {
switch ($type) {
case self::S2C_CHALLENGE:
{
$this->Challenge = $Buffer->Get(4);
$this->challenge = $buffer->get(4);
return;
}
case $ExpectedResult:
case $expectedResult:
{
// Goldsource (HLTV)
@ -477,7 +476,7 @@ final class SourceQuery
}
default:
{
throw new InvalidPacketException('GetChallenge: Packet header mismatch. (0x' . dechex($Type) . ')', InvalidPacketException::PACKET_HEADER_MISMATCH);
throw new InvalidPacketException('GetChallenge: Packet header mismatch. (0x' . dechex($type) . ')', InvalidPacketException::PACKET_HEADER_MISMATCH);
}
}
}
@ -485,28 +484,28 @@ final class SourceQuery
/**
* Sets rcon password, for future use in Rcon()
*
* @param string $Password Rcon Password
* @param string $password Rcon Password
*
* @throws AuthenticationException
* @throws InvalidPacketException
* @throws SocketException
*/
public function SetRconPassword(string $Password): void
public function setRconPassword(string $password): void
{
if (!$this->Connected) {
if (!$this->connected) {
throw new SocketException('Not connected.', SocketException::NOT_CONNECTED);
}
switch ($this->Socket->Engine) {
case self::GOLDSOURCE:
switch ($this->socket->getType()) {
case SocketType::GOLDSOURCE:
{
$this->Rcon = new GoldSourceRcon($this->Socket);
$this->rcon = new GoldSourceRcon($this->socket);
break;
}
case self::SOURCE:
case SocketType::SOURCE:
{
$this->Rcon = new SourceRcon($this->Socket);
$this->rcon = new SourceRcon($this->socket);
break;
}
@ -516,31 +515,31 @@ final class SourceQuery
}
}
$this->Rcon->Open();
$this->Rcon->Authorize($Password);
$this->rcon->open();
$this->rcon->authorize($password);
}
/**
* Sends a command to the server for execution.
*
* @param string $Command Command to execute
* @param string $command Command to execute
*
* @throws AuthenticationException
* @throws InvalidPacketException
* @return string Answer from server in string
*@throws InvalidPacketException
* @throws SocketException
*
* @return string Answer from server in string
* @throws AuthenticationException
*/
public function Rcon(string $Command): string
public function rcon(string $command): string
{
if (!$this->Connected) {
if (!$this->connected) {
throw new SocketException('Not connected.', SocketException::NOT_CONNECTED);
}
if ($this->Rcon === null) {
if ($this->rcon === null) {
throw new SocketException('You must set a RCON password before trying to execute a RCON command.', SocketException::NOT_CONNECTED);
}
return $this->Rcon->Command($Command);
return $this->rcon->command($command);
}
}

@ -1,226 +0,0 @@
<?php
declare(strict_types=1);
/**
* @author Pavel Djundik
*
* @link https://xpaw.me
* @link https://github.com/xPaw/PHP-Source-Query
*
* @license GNU Lesser General Public License, version 2.1
*
* @internal
*/
namespace xPaw\SourceQuery;
use xPaw\SourceQuery\Exception\AuthenticationException;
use xPaw\SourceQuery\Exception\InvalidPacketException;
use xPaw\SourceQuery\Exception\SocketException;
/**
* Class SourceRcon
*
* @package xPaw\SourceQuery
*
* @uses AuthenticationException
* @uses InvalidPacketException
* @uses SocketException
*/
final class SourceRcon
{
/**
* Points to socket class
*/
private BaseSocket $Socket;
/**
* @var ?resource
*/
private $RconSocket;
/**
* @var int $RconRequestId
*/
private int $RconRequestId = 0;
/**
* @param BaseSocket $Socket
*/
public function __construct(BaseSocket $Socket)
{
$this->Socket = $Socket;
}
/**
* Close
*/
public function Close(): void
{
if ($this->RconSocket) {
fclose($this->RconSocket);
$this->RconSocket = null;
}
$this->RconRequestId = 0;
}
/**
* @throws SocketException
*/
public function Open(): void
{
if (!$this->RconSocket) {
$RconSocket = @fsockopen($this->Socket->Address, $this->Socket->Port, $ErrNo, $ErrStr, $this->Socket->Timeout);
if ($ErrNo || !$RconSocket) {
throw new SocketException('Can\'t connect to RCON server: ' . $ErrStr, SocketException::CONNECTION_FAILED);
}
$this->RconSocket = $RconSocket;
stream_set_timeout($this->RconSocket, $this->Socket->Timeout);
stream_set_blocking($this->RconSocket, true);
}
}
/**
* @param int $Header
* @param string $String
*
* @return bool
*/
public function Write(int $Header, string $String = ''): bool
{
// Pack the packet together
$Command = pack('VV', ++$this->RconRequestId, $Header) . $String . "\x00\x00";
// Prepend packet length
$Command = pack('V', strlen($Command)) . $Command;
$Length = strlen($Command);
return $Length === fwrite($this->RconSocket, $Command, $Length);
}
/**
* @return Buffer
*
* @throws InvalidPacketException
*/
public function Read(): Buffer
{
$Buffer = new Buffer();
$Buffer->Set(fread($this->RconSocket, 4));
if ($Buffer->Remaining() < 4) {
throw new InvalidPacketException('Rcon read: Failed to read any data from socket', InvalidPacketException::BUFFER_EMPTY);
}
$PacketSize = $Buffer->GetLong();
$Buffer->Set(fread($this->RconSocket, $PacketSize));
$Data = $Buffer->Get();
$Remaining = $PacketSize - strlen($Data);
while ($Remaining > 0) {
$Data2 = fread($this->RconSocket, $Remaining);
$PacketSize = strlen($Data2);
if ($PacketSize === 0) {
throw new InvalidPacketException('Read ' . strlen($Data) . ' bytes from socket, ' . $Remaining . ' remaining', InvalidPacketException::BUFFER_EMPTY);
}
$Data .= $Data2;
$Remaining -= $PacketSize;
}
$Buffer->Set($Data);
return $Buffer;
}
/**
* @param string $Command
*
* @return string
*
* @throws AuthenticationException
* @throws InvalidPacketException
*/
public function Command(string $Command): string
{
$this->Write(SourceQuery::SERVERDATA_EXECCOMMAND, $Command);
$Buffer = $this->Read();
$Buffer->GetLong(); // RequestID
$Type = $Buffer->GetLong();
if ($Type === SourceQuery::SERVERDATA_AUTH_RESPONSE) {
throw new AuthenticationException('Bad rcon_password.', AuthenticationException::BAD_PASSWORD);
} elseif ($Type !== SourceQuery::SERVERDATA_RESPONSE_VALUE) {
throw new InvalidPacketException('Invalid rcon response.', InvalidPacketException::PACKET_HEADER_MISMATCH);
}
$Data = $Buffer->Get();
// We do this stupid hack to handle split packets
// See https://developer.valvesoftware.com/wiki/Source_RCON_Protocol#Multiple-packet_Responses
if (strlen($Data) >= 4000) {
$this->Write(SourceQuery::SERVERDATA_REQUESTVALUE);
do {
$Buffer = $this->Read();
$Buffer->GetLong(); // RequestID
if ($Buffer->GetLong() !== SourceQuery::SERVERDATA_RESPONSE_VALUE) {
break;
}
$Data2 = $Buffer->Get();
if ($Data2 === "\x00\x01\x00\x00\x00\x00") {
break;
}
$Data .= $Data2;
} while (true);
}
return rtrim($Data, "\0");
}
/**
* @param string $Password
*
* @throws AuthenticationException
* @throws InvalidPacketException
*/
public function Authorize(string $Password): void
{
$this->Write(SourceQuery::SERVERDATA_AUTH, $Password);
$Buffer = $this->Read();
$RequestID = $Buffer->GetLong();
$Type = $Buffer->GetLong();
// If we receive SERVERDATA_RESPONSE_VALUE, then we need to read again
// More info: https://developer.valvesoftware.com/wiki/Source_RCON_Protocol#Additional_Comments
if ($Type === SourceQuery::SERVERDATA_RESPONSE_VALUE) {
$Buffer = $this->Read();
$RequestID = $Buffer->GetLong();
$Type = $Buffer->GetLong();
}
if ($RequestID === -1 || $Type !== SourceQuery::SERVERDATA_AUTH_RESPONSE) {
throw new AuthenticationException('RCON authorization failed.', AuthenticationException::BAD_PASSWORD);
}
}
}

@ -3,118 +3,25 @@
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
use xPaw\SourceQuery\BaseSocket;
use xPaw\SourceQuery\SourceQuery;
use xPaw\SourceQuery\Buffer;
use xPaw\SourceQuery\Exception\AuthenticationException;
use xPaw\SourceQuery\Exception\InvalidArgumentException;
use xPaw\SourceQuery\Exception\InvalidPacketException;
use xPaw\SourceQuery\Exception\SocketException;
final class TestableSocket extends BaseSocket
{
/**
* @var SplQueue<string>
*/
private SplQueue $PacketQueue;
/**
* TestableSocket constructor.
*/
public function __construct()
{
$this->PacketQueue = new SplQueue();
$this->PacketQueue->setIteratorMode(SplDoublyLinkedList::IT_MODE_DELETE);
}
/**
* @param string $Data
*/
public function Queue(string $Data): void
{
$this->PacketQueue->push($Data);
}
/**
* Close.
*/
public function Close(): void
{
}
/**
* @param string $Address
* @param int $Port
* @param int $Timeout
* @param int $Engine
*/
public function Open(string $Address, int $Port, int $Timeout, int $Engine): void
{
$this->Timeout = $Timeout;
$this->Engine = $Engine;
$this->Port = $Port;
$this->Address = $Address;
}
/**
* @param int $Header
* @param string $String
*
* @return bool
*/
public function Write(int $Header, string $String = ''): bool
{
return true;
}
/**
* @param int $Length
*
* @return Buffer
*
* @throws InvalidPacketException
* @throws SocketException
*/
public function Read(int $Length = 1400): Buffer
{
$Buffer = new Buffer();
$Buffer->Set($this->PacketQueue->shift());
$this->ReadInternal($Buffer, $Length, [ $this, 'Sherlock' ]);
return $Buffer;
}
/**
* @param Buffer $Buffer
*
* @return bool
*
* @throws InvalidPacketException
*/
public function Sherlock(Buffer $Buffer): bool
{
if ($this->PacketQueue->isEmpty()) {
return false;
}
$Buffer->Set($this->PacketQueue->shift());
return $Buffer->GetLong() === -2;
}
}
use xPaw\SourceQuery\Socket\SocketType;
use xPaw\SourceQuery\Socket\TestableSocket;
final class Tests extends TestCase
{
/**
* @var TestableSocket $Socket
* @var TestableSocket
*/
private TestableSocket $Socket;
private TestableSocket $socket;
/**
* @var SourceQuery $SourceQuery
* @var SourceQuery
*/
private SourceQuery $SourceQuery;
private SourceQuery $sourceQuery;
/**
* @throws SocketException
@ -122,9 +29,9 @@ final class Tests extends TestCase
*/
public function setUp(): void
{
$this->Socket = new TestableSocket();
$this->SourceQuery = new SourceQuery($this->Socket);
$this->SourceQuery->Connect('', 2);
$this->socket = new TestableSocket(SocketType::SOURCE);
$this->sourceQuery = new SourceQuery($this->socket);
$this->sourceQuery->connect('', 2);
}
/**
@ -132,9 +39,9 @@ final class Tests extends TestCase
*/
public function tearDown(): void
{
$this->SourceQuery->Disconnect();
$this->sourceQuery->disconnect();
unset($this->Socket, $this->SourceQuery);
unset($this->socket, $this->sourceQuery);
}
/**
@ -144,8 +51,8 @@ final class Tests extends TestCase
public function testInvalidTimeout(): void
{
$this->expectException(InvalidArgumentException::class);
$SourceQuery = new SourceQuery();
$SourceQuery->Connect('', 2, -1);
$SourceQuery = new SourceQuery($this->socket);
$SourceQuery->connect('', 2, -1);
}
/**
@ -155,8 +62,8 @@ final class Tests extends TestCase
public function testNotConnectedGetInfo(): void
{
$this->expectException(SocketException::class);
$this->SourceQuery->Disconnect();
$this->SourceQuery->GetInfo();
$this->sourceQuery->disconnect();
$this->sourceQuery->getInfo();
}
/**
@ -166,8 +73,8 @@ final class Tests extends TestCase
public function testNotConnectedPing(): void
{
$this->expectException(SocketException::class);
$this->SourceQuery->Disconnect();
$this->SourceQuery->Ping();
$this->sourceQuery->disconnect();
$this->sourceQuery->ping();
}
/**
@ -177,8 +84,8 @@ final class Tests extends TestCase
public function testNotConnectedGetPlayers(): void
{
$this->expectException(SocketException::class);
$this->SourceQuery->Disconnect();
$this->SourceQuery->GetPlayers();
$this->sourceQuery->disconnect();
$this->sourceQuery->getPlayers();
}
/**
@ -188,8 +95,8 @@ final class Tests extends TestCase
public function testNotConnectedGetRules(): void
{
$this->expectException(SocketException::class);
$this->SourceQuery->Disconnect();
$this->SourceQuery->GetRules();
$this->sourceQuery->disconnect();
$this->sourceQuery->getRules();
}
/**
@ -200,8 +107,8 @@ final class Tests extends TestCase
public function testNotConnectedSetRconPassword(): void
{
$this->expectException(SocketException::class);
$this->SourceQuery->Disconnect();
$this->SourceQuery->SetRconPassword('a');
$this->sourceQuery->disconnect();
$this->sourceQuery->setRconPassword('a');
}
/**
@ -212,8 +119,8 @@ final class Tests extends TestCase
public function testNotConnectedRcon(): void
{
$this->expectException(SocketException::class);
$this->SourceQuery->Disconnect();
$this->SourceQuery->Rcon('a');
$this->sourceQuery->disconnect();
$this->sourceQuery->rcon('a');
}
/**
@ -224,29 +131,32 @@ final class Tests extends TestCase
public function testRconWithoutPassword(): void
{
$this->expectException(SocketException::class);
$this->SourceQuery->Rcon('a');
$this->sourceQuery->rcon('a');
}
/**
* @param string $RawInput
* @param array $ExpectedOutput
* @param string $rawInput
* @param array $expectedOutput
*
* @throws InvalidArgumentException
* @throws InvalidPacketException
* @throws SocketException
*
* @dataProvider InfoProvider
* @dataProvider infoProvider
*/
public function testGetInfo(string $RawInput, array $ExpectedOutput): void
public function testGetInfo(string $rawInput, array $expectedOutput): void
{
if (isset($ExpectedOutput[ 'IsMod' ])) {
$this->Socket->Engine = SourceQuery::GOLDSOURCE;
if (isset($expectedOutput[ 'IsMod' ])) {
$this->socket = new TestableSocket(SocketType::GOLDSOURCE);
$this->sourceQuery = new SourceQuery($this->socket);
$this->sourceQuery->connect('', 2);
}
$this->Socket->Queue($RawInput);
$this->socket->queue($rawInput);
$RealOutput = $this->SourceQuery->GetInfo();
$realOutput = $this->sourceQuery->getInfo();
self::assertEquals($ExpectedOutput, $RealOutput);
self::assertEquals($expectedOutput, $realOutput);
}
/**
@ -254,19 +164,19 @@ final class Tests extends TestCase
*
* @throws JsonException
*/
public function InfoProvider(): array
public function infoProvider(): array
{
$DataProvider = [];
$dataProvider = [];
$Files = glob(__DIR__ . '/Info/*.raw', GLOB_ERR);
$files = glob(__DIR__ . '/Info/*.raw', GLOB_ERR);
foreach ($Files as $File) {
$DataProvider[] =
foreach ($files as $file) {
$dataProvider[] =
[
hex2bin(trim(file_get_contents($File))),
hex2bin(trim(file_get_contents($file))),
json_decode(
file_get_contents(
str_replace('.raw', '.json', $File)
str_replace('.raw', '.json', $file)
),
true,
512,
@ -275,79 +185,79 @@ final class Tests extends TestCase
];
}
return $DataProvider;
return $dataProvider;
}
/**
* @param string $Data
* @param string $data
*
* @throws InvalidPacketException
* @throws SocketException
*
* @dataProvider BadPacketProvider
* @dataProvider badPacketProvider
*/
public function testBadGetInfo(string $Data): void
public function testBadGetInfo(string $data): void
{
$this->expectException(InvalidPacketException::class);
$this->Socket->Queue($Data);
$this->socket->queue($data);
$this->SourceQuery->GetInfo();
$this->sourceQuery->getInfo();
}
/**
* @param string $Data
* @param string $data
*
* @throws InvalidPacketException
* @throws SocketException
*
* @dataProvider BadPacketProvider
* @dataProvider badPacketProvider
*/
public function testBadGetChallengeViaPlayers(string $Data): void
public function testBadGetChallengeViaPlayers(string $data): void
{
$this->expectException(InvalidPacketException::class);
$this->Socket->Queue($Data);
$this->socket->queue($data);
$this->SourceQuery->GetPlayers();
$this->sourceQuery->getPlayers();
}
/**
* @param string $Data
* @param string $data
*
* @throws InvalidPacketException
* @throws SocketException
*
* @dataProvider BadPacketProvider
* @dataProvider badPacketProvider
*/
public function testBadGetPlayersAfterCorrectChallenge(string $Data): void
public function testBadGetPlayersAfterCorrectChallenge(string $data): void
{
$this->expectException(InvalidPacketException::class);
$this->Socket->Queue("\xFF\xFF\xFF\xFF\x41\x11\x11\x11\x11");
$this->Socket->Queue($Data);
$this->socket->queue("\xFF\xFF\xFF\xFF\x41\x11\x11\x11\x11");
$this->socket->queue($data);
$this->SourceQuery->GetPlayers();
$this->sourceQuery->getPlayers();
}
/**
* @param string $Data
* @param string $data
*
* @throws InvalidPacketException
* @throws SocketException
*
* @dataProvider BadPacketProvider
* @dataProvider badPacketProvider
*/
public function testBadGetRulesAfterCorrectChallenge(string $Data): void
public function testBadGetRulesAfterCorrectChallenge(string $data): void
{
$this->expectException(InvalidPacketException::class);
$this->Socket->Queue("\xFF\xFF\xFF\xFF\x41\x11\x11\x11\x11");
$this->Socket->Queue($Data);
$this->socket->queue("\xFF\xFF\xFF\xFF\x41\x11\x11\x11\x11");
$this->socket->queue($data);
$this->SourceQuery->GetRules();
$this->sourceQuery->getRules();
}
/**
* @return string[][]
*/
public function BadPacketProvider(): array
public function badPacketProvider(): array
{
return
[
@ -367,34 +277,34 @@ final class Tests extends TestCase
*/
public function testGetChallengeTwice(): void
{
$this->Socket->Queue("\xFF\xFF\xFF\xFF\x41\x11\x11\x11\x11");
$this->Socket->Queue("\xFF\xFF\xFF\xFF\x45\x01\x00ayy\x00lmao\x00");
self::assertEquals([ 'ayy' => 'lmao' ], $this->SourceQuery->GetRules());
$this->socket->queue("\xFF\xFF\xFF\xFF\x41\x11\x11\x11\x11");
$this->socket->queue("\xFF\xFF\xFF\xFF\x45\x01\x00ayy\x00lmao\x00");
self::assertEquals([ 'ayy' => 'lmao' ], $this->sourceQuery->getRules());
$this->Socket->Queue("\xFF\xFF\xFF\xFF\x45\x01\x00wow\x00much\x00");
self::assertEquals([ 'wow' => 'much' ], $this->SourceQuery->GetRules());
$this->socket->queue("\xFF\xFF\xFF\xFF\x45\x01\x00wow\x00much\x00");
self::assertEquals([ 'wow' => 'much' ], $this->sourceQuery->getRules());
}
/**
* @param array $RawInput
* @param array $ExpectedOutput
* @param string[] $rawInput
* @param array $expectedOutput
*
* @throws InvalidPacketException
* @throws SocketException
*
* @dataProvider RulesProvider
* @dataProvider rulesProvider
*/
public function testGetRules(array $RawInput, array $ExpectedOutput): void
public function testGetRules(array $rawInput, array $expectedOutput): void
{
$this->Socket->Queue(hex2bin("ffffffff4104fce20e")); // Challenge
$this->socket->queue(hex2bin("ffffffff4104fce20e")); // Challenge
foreach ($RawInput as $Packet) {
$this->Socket->Queue(hex2bin($Packet));
foreach ($rawInput as $packet) {
$this->socket->queue(hex2bin($packet));
}
$RealOutput = $this->SourceQuery->GetRules();
$realOutput = $this->sourceQuery->getRules();
self::assertEquals($ExpectedOutput, $RealOutput);
self::assertEquals($expectedOutput, $realOutput);
}
/**
@ -402,19 +312,19 @@ final class Tests extends TestCase
*
* @throws JsonException
*/
public function RulesProvider(): array
public function rulesProvider(): array
{
$DataProvider = [];
$dataProvider = [];
$Files = glob(__DIR__ . '/Rules/*.raw', GLOB_ERR);
$files = glob(__DIR__ . '/Rules/*.raw', GLOB_ERR);
foreach ($Files as $File) {
$DataProvider[] =
foreach ($files as $file) {
$dataProvider[] =
[
file($File, FILE_SKIP_EMPTY_LINES | FILE_IGNORE_NEW_LINES),
file($file, FILE_SKIP_EMPTY_LINES | FILE_IGNORE_NEW_LINES),
json_decode(
file_get_contents(
str_replace('.raw', '.json', $File)
str_replace('.raw', '.json', $file)
),
true,
512,
@ -423,29 +333,29 @@ final class Tests extends TestCase
];
}
return $DataProvider;
return $dataProvider;
}
/**
* @param string[] $RawInput
* @param array $ExpectedOutput
* @param string[] $rawInput
* @param array $expectedOutput
*
* @throws InvalidPacketException
* @throws SocketException
*
* @dataProvider PlayersProvider
* @dataProvider playersProvider
*/
public function testGetPlayers(array $RawInput, array $ExpectedOutput): void
public function testGetPlayers(array $rawInput, array $expectedOutput): void
{
$this->Socket->Queue(hex2bin("ffffffff4104fce20e")); // Challenge
$this->socket->queue(hex2bin("ffffffff4104fce20e")); // Challenge
foreach ($RawInput as $Packet) {
$this->Socket->Queue(hex2bin($Packet));
foreach ($rawInput as $packet) {
$this->socket->queue(hex2bin($packet));
}
$RealOutput = $this->SourceQuery->GetPlayers();
$realOutput = $this->sourceQuery->getPlayers();
self::assertEquals($ExpectedOutput, $RealOutput);
self::assertEquals($expectedOutput, $realOutput);
}
/**
@ -453,19 +363,19 @@ final class Tests extends TestCase
*
* @throws JsonException
*/
public function PlayersProvider(): array
public function playersProvider(): array
{
$DataProvider = [];
$dataProvider = [];
$Files = glob(__DIR__ . '/Players/*.raw', GLOB_ERR);
$files = glob(__DIR__ . '/Players/*.raw', GLOB_ERR);
foreach ($Files as $File) {
$DataProvider[] =
foreach ($files as $file) {
$dataProvider[] =
[
file($File, FILE_SKIP_EMPTY_LINES | FILE_IGNORE_NEW_LINES),
file($file, FILE_SKIP_EMPTY_LINES | FILE_IGNORE_NEW_LINES),
json_decode(
file_get_contents(
str_replace('.raw', '.json', $File)
str_replace('.raw', '.json', $file)
),
true,
512,
@ -474,7 +384,7 @@ final class Tests extends TestCase
];
}
return $DataProvider;
return $dataProvider;
}
/**
@ -483,10 +393,10 @@ final class Tests extends TestCase
*/
public function testPing(): void
{
$this->Socket->Queue("\xFF\xFF\xFF\xFF\x6A\x00");
self::assertTrue($this->SourceQuery->Ping());
$this->socket->queue("\xFF\xFF\xFF\xFF\x6A\x00");
self::assertTrue($this->sourceQuery->ping());
$this->Socket->Queue("\xFF\xFF\xFF\xFF\xEE");
self::assertFalse($this->SourceQuery->Ping());
$this->socket->queue("\xFF\xFF\xFF\xFF\xEE");
self::assertFalse($this->sourceQuery->ping());
}
}

Loading…
Cancel
Save