diff --git a/SourceQuery/Buffer.php b/SourceQuery/Buffer.php index 145e462..a7f9744 100644 --- a/SourceQuery/Buffer.php +++ b/SourceQuery/Buffer.php @@ -113,7 +113,11 @@ final class Buffer $data = unpack('v', $this->get(2)); - return (int)$data[ 1 ]; + if (!$data) { + throw new InvalidPacketException('Empty data from packet.'); + } + + return (int) $data[1]; } /** @@ -129,7 +133,11 @@ final class Buffer $data = unpack('l', $this->get(4)); - return (int)$data[ 1 ]; + if (!$data) { + throw new InvalidPacketException('Empty data from packet.'); + } + + return (int) $data[1]; } /** @@ -145,7 +153,11 @@ final class Buffer $data = unpack('f', $this->get(4)); - return (float)$data[ 1 ]; + if (!$data) { + throw new InvalidPacketException('Empty data from packet.'); + } + + return (float) $data[1]; } /** @@ -161,7 +173,11 @@ final class Buffer $data = unpack('V', $this->get(4)); - return (int)$data[ 1 ]; + if (!$data) { + throw new InvalidPacketException('Empty data from packet.'); + } + + return (int) $data[1]; } /** diff --git a/SourceQuery/Rcon/SourceRcon.php b/SourceQuery/Rcon/SourceRcon.php index 96c0f0c..6fd0ad1 100644 --- a/SourceQuery/Rcon/SourceRcon.php +++ b/SourceQuery/Rcon/SourceRcon.php @@ -173,8 +173,18 @@ final class SourceRcon extends AbstractRcon */ protected function read(): Buffer { + if (!$this->rconSocket) { + throw new InvalidPacketException('Rcon socket not open.'); + } + $buffer = new Buffer(); - $buffer->set(fread($this->rconSocket, 4)); + $socketData = fread($this->rconSocket, 4); + + if (!$socketData) { + throw new InvalidPacketException('Empty data from packet.'); + } + + $buffer->set($socketData); if ($buffer->remaining() < 4) { throw new InvalidPacketException('Rcon read: Failed to read any data from socket', InvalidPacketException::BUFFER_EMPTY); @@ -182,7 +192,13 @@ final class SourceRcon extends AbstractRcon $packetSize = $buffer->getLong(); - $buffer->set(fread($this->rconSocket, $packetSize)); + $socketData = fread($this->rconSocket, $packetSize); + + if (!$socketData) { + throw new InvalidPacketException('Empty data from packet.'); + } + + $buffer->set($socketData); $data = $buffer->get(); @@ -191,9 +207,13 @@ final class SourceRcon extends AbstractRcon while ($remaining > 0) { $data2 = fread($this->rconSocket, $remaining); + if (!$data2) { + throw new InvalidPacketException('Empty data from packet.'); + } + $packetSize = strlen($data2); - if ($packetSize === 0) { + if ($packetSize <= 0) { throw new InvalidPacketException('Read ' . strlen($data) . ' bytes from socket, ' . $remaining . ' remaining', InvalidPacketException::BUFFER_EMPTY); } @@ -211,9 +231,15 @@ final class SourceRcon extends AbstractRcon * @param string $string * * @return bool + * + * @throws InvalidPacketException */ protected function write(?int $header, string $string = ''): bool { + if (!$this->rconSocket) { + throw new InvalidPacketException('Rcon socket not open.'); + } + // Pack the packet together. $command = pack('VV', ++$this->rconRequestId, $header) . $string . "\x00\x00"; diff --git a/SourceQuery/Socket/AbstractSocket.php b/SourceQuery/Socket/AbstractSocket.php index 778ebb5..f086f3b 100644 --- a/SourceQuery/Socket/AbstractSocket.php +++ b/SourceQuery/Socket/AbstractSocket.php @@ -137,6 +137,10 @@ abstract class AbstractSocket implements SocketInterface */ public function read(int $length = 1400): Buffer { + if (!$this->socket) { + throw new InvalidPacketException('Socket not open.'); + } + $buffer = new Buffer(); $data = fread($this->socket, $length); @@ -156,9 +160,15 @@ abstract class AbstractSocket implements SocketInterface * @param string $string * * @return bool + * + * @throws InvalidPacketException */ public function write(int $header, string $string = ''): bool { + if (!$this->socket) { + throw new InvalidPacketException('Socket not open.'); + } + $command = pack('ccccca*', 0xFF, 0xFF, 0xFF, 0xFF, $header, $string); $length = strlen($command); @@ -175,8 +185,16 @@ abstract class AbstractSocket implements SocketInterface */ public function sherlock(Buffer $buffer, int $length): bool { + if (!$this->socket) { + throw new InvalidPacketException('Socket not open.'); + } + $data = fread($this->socket, $length); + if (!$data) { + throw new InvalidPacketException('Empty data from packet.'); + } + if (strlen($data) < 4) { return false; } diff --git a/SourceQuery/SourceQuery.php b/SourceQuery/SourceQuery.php index d05e47e..3a9120b 100644 --- a/SourceQuery/SourceQuery.php +++ b/SourceQuery/SourceQuery.php @@ -127,22 +127,6 @@ final class SourceQuery $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 - * - * @return bool Previous value - */ - public function SetUseOldGetChallengeMethod(bool $value): bool - { - $previous = $this->useOldGetChallengeMethod; - - $this->useOldGetChallengeMethod = $value === true; - - return $previous; - } - /** * Closes all open connections */ @@ -160,6 +144,22 @@ final class SourceQuery } } + /** + * 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 + * + * @return bool Previous value + */ + public function SetUseOldGetChallengeMethod(bool $value): bool + { + $previous = $this->useOldGetChallengeMethod; + + $this->useOldGetChallengeMethod = $value === true; + + return $previous; + } + /** * Sends ping packet to the server * NOTE: This may not work on some games (TF2 for example) @@ -279,7 +279,7 @@ final class SourceQuery $server[ 'Version' ] = $buffer->getString(); // Extra Data Flags. - if (!$buffer->isEmpty()) { + if ($buffer->remaining() > 0) { $server[ 'ExtraDataFlags' ] = $Flags = $buffer->getByte(); // S2A_EXTRA_DATA_HAS_GAME_PORT - Next 2 bytes include the game port. @@ -421,52 +421,6 @@ final class SourceQuery return $rules; } - /** - * Get challenge (used for players/rules packets) - * - * @param int $header - * @param int $expectedResult - * - * @throws InvalidPacketException - */ - private function getChallenge(int $header, int $expectedResult): void - { - if ($this->challenge) { - return; - } - - if ($this->useOldGetChallengeMethod) { - $header = self::A2S_SERVERQUERY_GETCHALLENGE; - } - - $this->socket->write($header, "\xFF\xFF\xFF\xFF"); - $buffer = $this->socket->read(); - - $type = $buffer->getByte(); - - switch ($type) { - case self::S2C_CHALLENGE: - { - $this->challenge = $buffer->get(4); - - return; - } - case $expectedResult: - { - // Goldsource (HLTV). - return; - } - case 0: - { - throw new InvalidPacketException('GetChallenge: Failed to get challenge.'); - } - default: - { - throw new InvalidPacketException('GetChallenge: Packet header mismatch. (0x' . dechex($type) . ')', InvalidPacketException::PACKET_HEADER_MISMATCH); - } - } - } - /** * Sets rcon password, for future use in Rcon() * @@ -528,4 +482,50 @@ final class SourceQuery return $this->rcon->command($command); } + + /** + * Get challenge (used for players/rules packets) + * + * @param int $header + * @param int $expectedResult + * + * @throws InvalidPacketException + */ + private function getChallenge(int $header, int $expectedResult): void + { + if ($this->challenge) { + return; + } + + if ($this->useOldGetChallengeMethod) { + $header = self::A2S_SERVERQUERY_GETCHALLENGE; + } + + $this->socket->write($header, "\xFF\xFF\xFF\xFF"); + $buffer = $this->socket->read(); + + $type = $buffer->getByte(); + + switch ($type) { + case self::S2C_CHALLENGE: + { + $this->challenge = $buffer->get(4); + + return; + } + case $expectedResult: + { + // Goldsource (HLTV). + return; + } + case 0: + { + throw new InvalidPacketException('GetChallenge: Failed to get challenge.'); + } + default: + { + throw new InvalidPacketException('GetChallenge: Packet header mismatch. (0x' . dechex($type) . ')', InvalidPacketException::PACKET_HEADER_MISMATCH); + } + } + } } diff --git a/Tests/Tests.php b/Tests/Tests.php index 4aaff0b..ed444cc 100644 --- a/Tests/Tests.php +++ b/Tests/Tests.php @@ -239,7 +239,7 @@ final class Tests extends TestCase { return [ - [ "" ], + [ '' ], [ "\xff\xff\xff\xff" ], // No type. [ "\xff\xff\xff\xff\x49" ], // Correct type, but no data after. [ "\xff\xff\xff\xff\x6D" ], // Old info packet, but tests are done for source. @@ -274,10 +274,22 @@ final class Tests extends TestCase */ public function testGetRules(array $rawInput, array $expectedOutput): void { - $this->socket->queue(hex2bin("ffffffff4104fce20e")); // Challenge. + $data = hex2bin('ffffffff4104fce20e'); + + if (!$data) { + throw new InvalidPacketException('Bad packet data'); + } + + $this->socket->queue($data); // Challenge. foreach ($rawInput as $packet) { - $this->socket->queue(hex2bin($packet)); + $data = hex2bin($packet); + + if (!$data) { + throw new InvalidPacketException('Bad packet data'); + } + + $this->socket->queue($data); } $realOutput = $this->sourceQuery->getRules(); @@ -306,10 +318,22 @@ final class Tests extends TestCase */ public function testGetPlayers(array $rawInput, array $expectedOutput): void { - $this->socket->queue(hex2bin("ffffffff4104fce20e")); // Challenge. + $data = hex2bin('ffffffff4104fce20e'); + + if (!$data) { + throw new InvalidPacketException('Bad packet data'); + } + + $this->socket->queue($data); // Challenge. foreach ($rawInput as $packet) { - $this->socket->queue(hex2bin($packet)); + $data = hex2bin($packet); + + if (!$data) { + throw new InvalidPacketException('Bad packet data'); + } + + $this->socket->queue($data); } $realOutput = $this->sourceQuery->getPlayers(); @@ -353,18 +377,36 @@ final class Tests extends TestCase $files = glob(__DIR__ . '/' . $path . '/*.raw', GLOB_ERR); + if (!$files) { + throw new RuntimeException('Could not load test data.'); + } + foreach ($files as $file) { - $content = $hexToBin - ? hex2bin(trim(file_get_contents($file))) - : file($file, FILE_SKIP_EMPTY_LINES | FILE_IGNORE_NEW_LINES); + if ($hexToBin) { + $content = file_get_contents($file); + + if (!$content) { + throw new RuntimeException('Could not load test data.'); + } + + $content = hex2bin(trim($content)); + } else { + $content = file($file, FILE_SKIP_EMPTY_LINES | FILE_IGNORE_NEW_LINES); + } + + $jsonContent = file_get_contents( + str_replace('.raw', '.json', $file) + ); + + if (!$jsonContent) { + throw new RuntimeException('Could not load test data.'); + } $dataProvider[] = [ $content, json_decode( - file_get_contents( - str_replace('.raw', '.json', $file) - ), + $jsonContent, true, 512, JSON_THROW_ON_ERROR diff --git a/phpstan.neon b/phpstan.neon index 0deb5a0..a024bb0 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,7 +1,7 @@ parameters: checkMissingIterableValueType: false checkFunctionNameCase: true - level: 6 + level: max paths: - . excludes_analyse: