From b01c1f643fe3b127a5a5f888654e921249e9f085 Mon Sep 17 00:00:00 2001 From: Anthony Birkett Date: Mon, 31 May 2021 11:12:21 +0100 Subject: [PATCH] Split up Socket and Rcon classes to be engine specific extensions. --- Examples/Example.php | 16 +- Examples/RconExample.php | 14 +- Examples/View.php | 16 +- SourceQuery/BaseSocket.php | 177 ------------ SourceQuery/Buffer.php | 90 +++--- SourceQuery/GoldSourceRcon.php | 171 ----------- SourceQuery/Rcon/AbstractRcon.php | 28 ++ SourceQuery/Rcon/GoldSourceRcon.php | 175 +++++++++++ SourceQuery/Rcon/RconInterface.php | 44 +++ SourceQuery/Rcon/SourceRcon.php | 235 +++++++++++++++ SourceQuery/Socket.php | 130 --------- SourceQuery/Socket/AbstractSocket.php | 285 ++++++++++++++++++ SourceQuery/Socket/GoldSourceSocket.php | 38 +++ SourceQuery/Socket/SocketInterface.php | 79 +++++ SourceQuery/Socket/SocketType.php | 11 + SourceQuery/Socket/SourceSocket.php | 47 +++ SourceQuery/Socket/TestableSocket.php | 211 ++++++++++++++ SourceQuery/SourceQuery.php | 369 ++++++++++++------------ SourceQuery/SourceRcon.php | 226 --------------- Tests/Tests.php | 314 +++++++------------- 20 files changed, 1520 insertions(+), 1156 deletions(-) delete mode 100644 SourceQuery/BaseSocket.php delete mode 100644 SourceQuery/GoldSourceRcon.php create mode 100644 SourceQuery/Rcon/AbstractRcon.php create mode 100644 SourceQuery/Rcon/GoldSourceRcon.php create mode 100644 SourceQuery/Rcon/RconInterface.php create mode 100644 SourceQuery/Rcon/SourceRcon.php delete mode 100644 SourceQuery/Socket.php create mode 100644 SourceQuery/Socket/AbstractSocket.php create mode 100644 SourceQuery/Socket/GoldSourceSocket.php create mode 100644 SourceQuery/Socket/SocketInterface.php create mode 100644 SourceQuery/Socket/SocketType.php create mode 100644 SourceQuery/Socket/SourceSocket.php create mode 100644 SourceQuery/Socket/TestableSocket.php delete mode 100644 SourceQuery/SourceRcon.php diff --git a/Examples/Example.php b/Examples/Example.php index cfa061d..59ed770 100644 --- a/Examples/Example.php +++ b/Examples/Example.php @@ -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(); } diff --git a/Examples/RconExample.php b/Examples/RconExample.php index dc58692..641043e 100644 --- a/Examples/RconExample.php +++ b/Examples/RconExample.php @@ -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(); } diff --git a/Examples/View.php b/Examples/View.php index 49d798c..083a45b 100644 --- a/Examples/View.php +++ b/Examples/View.php @@ -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, '.', ''); diff --git a/SourceQuery/BaseSocket.php b/SourceQuery/BaseSocket.php deleted file mode 100644 index 3cae434..0000000 --- a/SourceQuery/BaseSocket.php +++ /dev/null @@ -1,177 +0,0 @@ -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; - } -} diff --git a/SourceQuery/Buffer.php b/SourceQuery/Buffer.php index 7eb911e..e6d9104 100644 --- a/SourceQuery/Buffer.php +++ b/SourceQuery/Buffer.php @@ -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; } } diff --git a/SourceQuery/GoldSourceRcon.php b/SourceQuery/GoldSourceRcon.php deleted file mode 100644 index dd43294..0000000 --- a/SourceQuery/GoldSourceRcon.php +++ /dev/null @@ -1,171 +0,0 @@ -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()); - } -} diff --git a/SourceQuery/Rcon/AbstractRcon.php b/SourceQuery/Rcon/AbstractRcon.php new file mode 100644 index 0000000..3938c21 --- /dev/null +++ b/SourceQuery/Rcon/AbstractRcon.php @@ -0,0 +1,28 @@ +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; + } +} diff --git a/SourceQuery/Rcon/RconInterface.php b/SourceQuery/Rcon/RconInterface.php new file mode 100644 index 0000000..d4f83e1 --- /dev/null +++ b/SourceQuery/Rcon/RconInterface.php @@ -0,0 +1,44 @@ +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; + } +} diff --git a/SourceQuery/Socket.php b/SourceQuery/Socket.php deleted file mode 100644 index 6735e74..0000000 --- a/SourceQuery/Socket.php +++ /dev/null @@ -1,130 +0,0 @@ -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; - } -} diff --git a/SourceQuery/Socket/AbstractSocket.php b/SourceQuery/Socket/AbstractSocket.php new file mode 100644 index 0000000..290b2a6 --- /dev/null +++ b/SourceQuery/Socket/AbstractSocket.php @@ -0,0 +1,285 @@ +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; + } +} diff --git a/SourceQuery/Socket/GoldSourceSocket.php b/SourceQuery/Socket/GoldSourceSocket.php new file mode 100644 index 0000000..662d6b9 --- /dev/null +++ b/SourceQuery/Socket/GoldSourceSocket.php @@ -0,0 +1,38 @@ +getByte(); + $count = $packetCountAndNumber & 0xF; + $number = $packetCountAndNumber >> 4; + $isCompressed = false; + } +} diff --git a/SourceQuery/Socket/SocketInterface.php b/SourceQuery/Socket/SocketInterface.php new file mode 100644 index 0000000..48b76bb --- /dev/null +++ b/SourceQuery/Socket/SocketInterface.php @@ -0,0 +1,79 @@ +getByte(); + $number = $buffer->getByte() + 1; + + if ($isCompressed) { + $buffer->getLong(); // Split size + + $checksum = $buffer->getUnsignedLong(); + } else { + $buffer->getShort(); // Split size + } + } +} diff --git a/SourceQuery/Socket/TestableSocket.php b/SourceQuery/Socket/TestableSocket.php new file mode 100644 index 0000000..d46187f --- /dev/null +++ b/SourceQuery/Socket/TestableSocket.php @@ -0,0 +1,211 @@ + + */ + 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 + } + } +} diff --git a/SourceQuery/SourceQuery.php b/SourceQuery/SourceQuery.php index 4d0d037..369ebc1 100644 --- a/SourceQuery/SourceQuery.php +++ b/SourceQuery/SourceQuery.php @@ -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); } } diff --git a/SourceQuery/SourceRcon.php b/SourceQuery/SourceRcon.php deleted file mode 100644 index d080db7..0000000 --- a/SourceQuery/SourceRcon.php +++ /dev/null @@ -1,226 +0,0 @@ -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); - } - } -} diff --git a/Tests/Tests.php b/Tests/Tests.php index f1c7d6e..2ee0826 100644 --- a/Tests/Tests.php +++ b/Tests/Tests.php @@ -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 - */ - 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()); } }