Remove engine from the Sockets.

Docblock changes.
Reorder methods to be logical.
pull/150/head
Anthony Birkett 4 years ago
parent b01c1f643f
commit d9bab8aa25

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

@ -6,29 +6,21 @@ require __DIR__ . '/../vendor/autoload.php';
use xPaw\SourceQuery\SourceQuery; use xPaw\SourceQuery\SourceQuery;
use xPaw\SourceQuery\Socket\SourceSocket; use xPaw\SourceQuery\Socket\SourceSocket;
use xPaw\SourceQuery\Socket\SocketType;
// For the sake of this example // For the sake of this example
header('Content-Type: text/plain'); header('Content-Type: text/plain');
header('X-Content-Type-Options: nosniff'); header('X-Content-Type-Options: nosniff');
// Edit this -> $query = new SourceQuery(new SourceSocket());
define('SQ_SERVER_ADDR', 'localhost');
define('SQ_SERVER_PORT', 27015);
define('SQ_TIMEOUT', 1);
define('SQ_ENGINE', SocketType::SOURCE);
// Edit this <-
$Query = new SourceQuery(new SourceSocket());
try { try {
$Query->connect(SQ_SERVER_ADDR, SQ_SERVER_PORT, SQ_TIMEOUT, SQ_ENGINE); $query->connect('localhost', 27015, 1);
$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) { } catch (Exception $exception) {
echo $e->getMessage(); echo $exception->getMessage();
} finally { } finally {
$Query->disconnect(); $query->disconnect();
} }

@ -6,38 +6,30 @@ require __DIR__ . '/../vendor/autoload.php';
use xPaw\SourceQuery\SourceQuery; use xPaw\SourceQuery\SourceQuery;
use xPaw\SourceQuery\Socket\SourceSocket; use xPaw\SourceQuery\Socket\SourceSocket;
use xPaw\SourceQuery\Socket\SocketType;
// Edit this -> $timer = microtime(true);
define('SQ_SERVER_ADDR', 'localhost');
define('SQ_SERVER_PORT', 27015);
define('SQ_TIMEOUT', 3);
define('SQ_ENGINE', SocketType::SOURCE);
// Edit this <-
$Timer = microtime(true); $query = new SourceQuery(new SourceSocket());
$Query = new SourceQuery(new SourceSocket()); $info = [];
$rules = [];
$Info = []; $players = [];
$Rules = []; $exception = null;
$Players = [];
$Exception = null;
try { try {
$Query->connect(SQ_SERVER_ADDR, SQ_SERVER_PORT, SQ_TIMEOUT, SQ_ENGINE); $query->connect('localhost', 27015, 3);
//$Query->SetUseOldGetChallengeMethod( true ); // Use this when players/rules retrieval fails on games like Starbound //$Query->SetUseOldGetChallengeMethod( true ); // Use this when players/rules retrieval fails on games like Starbound
$Info = $Query->getInfo(); $info = $query->getInfo();
$Players = $Query->getPlayers(); $players = $query->getPlayers();
$Rules = $Query->getRules(); $rules = $query->getRules();
} catch (Exception $e) { } catch (Exception $e) {
$Exception = $e; $exception = $e;
} finally { } finally {
$Query->disconnect(); $query->disconnect();
} }
$Timer = number_format(microtime(true) - $Timer, 4, '.', ''); $timer = number_format(microtime(true) - $timer, 4, '.', '');
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
@ -47,7 +39,7 @@ $Timer = number_format(microtime(true) - $Timer, 4, '.', '');
<title>Source Query PHP Library</title> <title>Source Query PHP Library</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
<style type="text/css"> <style>
.table { .table {
table-layout: fixed; table-layout: fixed;
border-top-color: #428BCA; border-top-color: #428BCA;
@ -89,9 +81,9 @@ $Timer = number_format(microtime(true) - $Timer, 4, '.', '');
</div> </div>
<div class="container"> <div class="container">
<?php if ($Exception !== null): ?> <?php if ($exception !== null): ?>
<div class="panel panel-error"> <div class="panel panel-error">
<pre class="panel-body"><?php echo htmlspecialchars($Exception->__toString()); ?></pre> <pre class="panel-body"><?php echo htmlspecialchars($exception->__toString()); ?></pre>
</div> </div>
<?php endif; ?> <?php endif; ?>
<div class="row"> <div class="row">
@ -100,27 +92,27 @@ $Timer = number_format(microtime(true) - $Timer, 4, '.', '');
<thead> <thead>
<tr> <tr>
<th class="info-column">Server Info</th> <th class="info-column">Server Info</th>
<th><span class="label label-<?php echo $Timer > 1.0 ? 'danger' : 'success'; ?>"><?php echo $Timer; ?>s</span></th> <th><span class="label label-<?php echo $timer > 1.0 ? 'danger' : 'success'; ?>"><?php echo $timer; ?>s</span></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php if (!empty($Info)): ?> <?php if (!empty($info)): ?>
<?php foreach ($Info as $InfoKey => $InfoValue): ?> <?php foreach ($info as $infoKey => $infoValue): ?>
<tr> <tr>
<td><?php echo htmlspecialchars($InfoKey); ?></td> <td><?php echo htmlspecialchars($infoKey); ?></td>
<td><?php <td><?php
if (is_array($InfoValue)) { if (is_array($infoValue)) {
echo "<pre>"; echo "<pre>";
print_r($InfoValue); print_r($infoValue);
echo "</pre>"; echo "</pre>";
} elseif ($InfoValue === true) { } elseif ($infoValue === true) {
echo 'true'; echo 'true';
} elseif ($InfoValue === false) { } elseif ($infoValue === false) {
echo 'false'; echo 'false';
} elseif (is_int($InfoValue)) { } elseif (is_int($infoValue)) {
echo $InfoValue; echo $infoValue;
} else { } else {
echo htmlspecialchars($InfoValue); echo htmlspecialchars($infoValue);
} }
?></td> ?></td>
</tr> </tr>
@ -137,18 +129,18 @@ $Timer = number_format(microtime(true) - $Timer, 4, '.', '');
<table class="table table-bordered table-striped"> <table class="table table-bordered table-striped">
<thead> <thead>
<tr> <tr>
<th>Player <span class="label label-info"><?php echo count($Players); ?></span></th> <th>Player <span class="label label-info"><?php echo count($players); ?></span></th>
<th class="frags-column">Frags</th> <th class="frags-column">Frags</th>
<th class="frags-column">Time</th> <th class="frags-column">Time</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php if (!empty($Players)): ?> <?php if (!empty($players)): ?>
<?php foreach ($Players as $Player): ?> <?php foreach ($players as $player): ?>
<tr> <tr>
<td><?php echo htmlspecialchars($Player[ 'Name' ]); ?></td> <td><?php echo htmlspecialchars($player[ 'Name' ]); ?></td>
<td><?php echo $Player[ 'Frags' ]; ?></td> <td><?php echo $player[ 'Frags' ]; ?></td>
<td><?php echo $Player[ 'TimeF' ]; ?></td> <td><?php echo $player[ 'TimeF' ]; ?></td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
<?php else: ?> <?php else: ?>
@ -165,15 +157,15 @@ $Timer = number_format(microtime(true) - $Timer, 4, '.', '');
<table class="table table-bordered table-striped"> <table class="table table-bordered table-striped">
<thead> <thead>
<tr> <tr>
<th colspan="2">Rules <span class="label label-info"><?php echo count($Rules); ?></span></th> <th colspan="2">Rules <span class="label label-info"><?php echo count($rules); ?></span></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php if (!empty($Rules)): ?> <?php if (!empty($rules)): ?>
<?php foreach ($Rules as $Rule => $Value): ?> <?php foreach ($rules as $ruleKey => $ruleValue): ?>
<tr> <tr>
<td><?php echo htmlspecialchars($Rule); ?></td> <td><?php echo htmlspecialchars($ruleKey); ?></td>
<td><?php echo htmlspecialchars($Value); ?></td> <td><?php echo htmlspecialchars($ruleValue); ?></td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
<?php else: ?> <?php else: ?>

@ -17,13 +17,6 @@ namespace xPaw\SourceQuery;
use xPaw\SourceQuery\Exception\InvalidPacketException; use xPaw\SourceQuery\Exception\InvalidPacketException;
/**
* Class Buffer
*
* @package xPaw\SourceQuery
*
* @uses InvalidPacketException
*/
final class Buffer final class Buffer
{ {
/** /**

@ -19,5 +19,5 @@ use Exception;
abstract class SourceQueryException extends Exception abstract class SourceQueryException extends Exception
{ {
// Base exception class // Base exception class.
} }

@ -2,6 +2,17 @@
declare(strict_types=1); 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; namespace xPaw\SourceQuery\Rcon;
use xPaw\SourceQuery\Buffer; use xPaw\SourceQuery\Buffer;
@ -10,14 +21,6 @@ use xPaw\SourceQuery\Exception\InvalidPacketException;
abstract class AbstractRcon implements RconInterface 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 AuthenticationException
* @throws InvalidPacketException * @throws InvalidPacketException
@ -25,4 +28,12 @@ abstract class AbstractRcon implements RconInterface
* @return Buffer * @return Buffer
*/ */
abstract protected function read(): Buffer; abstract protected function read(): Buffer;
/**
* @param int|null $header
* @param string $string
*
* @return bool
*/
abstract protected function write(?int $header, string $string = ''): bool;
} }

@ -21,14 +21,6 @@ use xPaw\SourceQuery\Exception\InvalidPacketException;
use xPaw\SourceQuery\Socket\SocketInterface; use xPaw\SourceQuery\Socket\SocketInterface;
use xPaw\SourceQuery\SourceQuery; use xPaw\SourceQuery\SourceQuery;
/**
* Class GoldSourceRcon
*
* @package xPaw\SourceQuery
*
* @uses AuthenticationException
* @uses InvalidPacketException
*/
final class GoldSourceRcon extends AbstractRcon final class GoldSourceRcon extends AbstractRcon
{ {
/** /**
@ -56,15 +48,6 @@ final class GoldSourceRcon extends AbstractRcon
$this->socket = $socket; $this->socket = $socket;
} }
/**
* Close
*/
public function close(): void
{
$this->rconChallenge = '';
$this->rconPassword = '';
}
/** /**
* Open * Open
*/ */
@ -73,23 +56,12 @@ final class GoldSourceRcon extends AbstractRcon
} }
/** /**
* @param string $command * Close
*
* @return string
*
* @throws AuthenticationException
* @throws InvalidPacketException
*/ */
public function command(string $command): string public function close(): void
{ {
if (!$this->rconChallenge) { $this->rconChallenge = '';
throw new AuthenticationException('Tried to execute a RCON command before successful authorization.', AuthenticationException::BAD_PASSWORD); $this->rconPassword = '';
}
$this->write(null, 'rcon ' . $this->rconChallenge . ' "' . $this->rconPassword . '" ' . $command . "\0");
$buffer = $this->read();
return $buffer->get();
} }
/** /**
@ -112,17 +84,23 @@ final class GoldSourceRcon extends AbstractRcon
} }
/** /**
* @param int|null $header * @param string $command
* @param string $string
* *
* @return bool * @return string
*
* @throws AuthenticationException
* @throws InvalidPacketException
*/ */
protected function write(?int $header, string $string = ''): bool public function command(string $command): string
{ {
$command = pack('cccca*', 0xFF, 0xFF, 0xFF, 0xFF, $string); if (!$this->rconChallenge) {
$length = strlen($command); throw new AuthenticationException('Tried to execute a RCON command before successful authorization.', AuthenticationException::BAD_PASSWORD);
}
return $length === fwrite($this->socket->getSocket(), $command, $length); $this->write(null, 'rcon ' . $this->rconChallenge . ' "' . $this->rconPassword . '" ' . $command . "\0");
$buffer = $this->read();
return $buffer->get();
} }
/** /**
@ -133,14 +111,14 @@ final class GoldSourceRcon extends AbstractRcon
*/ */
protected function read(): Buffer protected function read(): Buffer
{ {
// GoldSource RCON has same structure as Query // GoldSource RCON has same structure as Query.
$buffer = $this->socket->read(); $buffer = $this->socket->read();
$stringBuffer = ''; $stringBuffer = '';
// There is no indentifier of the end, so we just need to continue reading // There is no identifier of the end, so we just need to continue reading.
do { do {
$readMore = $buffer->remaining() > 0; $readMore = !$buffer->isEmpty();
if ($readMore) { if ($readMore) {
if ($buffer->getByte() !== SourceQuery::S2A_RCON) { if ($buffer->getByte() !== SourceQuery::S2A_RCON) {
@ -151,7 +129,7 @@ final class GoldSourceRcon extends AbstractRcon
$stringBuffer .= $packet; $stringBuffer .= $packet;
//$stringBuffer .= SubStr( $packet, 0, -2 ); //$stringBuffer .= SubStr( $packet, 0, -2 );
// Let's assume if this packet is not long enough, there are no more after this one // Let's assume if this packet is not long enough, there are no more after this one.
$readMore = strlen($packet) > 1000; // use 1300? $readMore = strlen($packet) > 1000; // use 1300?
if ($readMore) { if ($readMore) {
@ -172,4 +150,18 @@ final class GoldSourceRcon extends AbstractRcon
return $buffer; return $buffer;
} }
/**
* @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);
}
} }

@ -2,6 +2,17 @@
declare(strict_types=1); 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; namespace xPaw\SourceQuery\Rcon;
use xPaw\SourceQuery\Exception\AuthenticationException; use xPaw\SourceQuery\Exception\AuthenticationException;
@ -15,15 +26,22 @@ interface RconInterface
*/ */
public function __construct(SocketInterface $socket); public function __construct(SocketInterface $socket);
/**
* Open
*/
public function open(): void;
/** /**
* Close * Close
*/ */
public function close(): void; public function close(): void;
/** /**
* Open * @param string $password
*
* @throws AuthenticationException
*/ */
public function open(): void; public function authorize(string $password): void;
/** /**
* @param string $command * @param string $command
@ -34,11 +52,4 @@ interface RconInterface
* @throws InvalidPacketException * @throws InvalidPacketException
*/ */
public function command(string $command): string; public function command(string $command): string;
/**
* @param string $password
*
* @throws AuthenticationException
*/
public function authorize(string $password): void;
} }

@ -22,15 +22,6 @@ use xPaw\SourceQuery\Exception\SocketException;
use xPaw\SourceQuery\Socket\SocketInterface; use xPaw\SourceQuery\Socket\SocketInterface;
use xPaw\SourceQuery\SourceQuery; use xPaw\SourceQuery\SourceQuery;
/**
* Class SourceRcon
*
* @package xPaw\SourceQuery
*
* @uses AuthenticationException
* @uses InvalidPacketException
* @uses SocketException
*/
final class SourceRcon extends AbstractRcon final class SourceRcon extends AbstractRcon
{ {
/** /**
@ -56,20 +47,6 @@ final class SourceRcon extends AbstractRcon
$this->socket = $socket; $this->socket = $socket;
} }
/**
* Close
*/
public function close(): void
{
if ($this->rconSocket) {
fclose($this->rconSocket);
$this->rconSocket = null;
}
$this->rconRequestId = 0;
}
/** /**
* @throws SocketException * @throws SocketException
*/ */
@ -94,6 +71,49 @@ final class SourceRcon extends AbstractRcon
} }
} }
/**
* Close
*/
public function close(): void
{
if ($this->rconSocket) {
fclose($this->rconSocket);
$this->rconSocket = null;
}
$this->rconRequestId = 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 string $command * @param string $command
* *
@ -119,7 +139,7 @@ final class SourceRcon extends AbstractRcon
$data = $buffer->get(); $data = $buffer->get();
// We do this stupid hack to handle split packets // We do this stupid hack to handle split packets.
// See https://developer.valvesoftware.com/wiki/Source_RCON_Protocol#Multiple-packet_Responses // See https://developer.valvesoftware.com/wiki/Source_RCON_Protocol#Multiple-packet_Responses
if (strlen($data) >= 4000) { if (strlen($data) >= 4000) {
$this->write(SourceQuery::SERVERDATA_REQUESTVALUE); $this->write(SourceQuery::SERVERDATA_REQUESTVALUE);
@ -127,7 +147,7 @@ final class SourceRcon extends AbstractRcon
do { do {
$buffer = $this->read(); $buffer = $this->read();
$buffer->getLong(); // RequestID $buffer->getLong(); // RequestID.
if ($buffer->getLong() !== SourceQuery::SERVERDATA_RESPONSE_VALUE) { if ($buffer->getLong() !== SourceQuery::SERVERDATA_RESPONSE_VALUE) {
break; break;
@ -146,53 +166,6 @@ final class SourceRcon extends AbstractRcon
return rtrim($data, "\0"); 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 * @return Buffer
* *
@ -232,4 +205,22 @@ final class SourceRcon extends AbstractRcon
return $buffer; return $buffer;
} }
/**
* @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);
}
} }

@ -20,26 +20,8 @@ use xPaw\SourceQuery\Exception\InvalidArgumentException;
use xPaw\SourceQuery\Exception\InvalidPacketException; use xPaw\SourceQuery\Exception\InvalidPacketException;
use xPaw\SourceQuery\Exception\SocketException; use xPaw\SourceQuery\Exception\SocketException;
/**
* Base socket
*
* @package xPaw\SourceQuery
*
* @uses InvalidPacketException
* @uses SocketException
*/
abstract class AbstractSocket implements SocketInterface abstract class AbstractSocket implements SocketInterface
{ {
/**
* @var resource|null
*/
public $socket;
/**
* @var int
*/
public int $engine = SocketType::SOURCE;
/** /**
* @var string $address * @var string $address
*/ */
@ -50,6 +32,11 @@ abstract class AbstractSocket implements SocketInterface
*/ */
public int $port = 0; public int $port = 0;
/**
* @var resource|null
*/
public $socket;
/** /**
* @var int $timeout * @var int $timeout
*/ */
@ -79,14 +66,6 @@ abstract class AbstractSocket implements SocketInterface
return $this->port; return $this->port;
} }
/**
* @return int
*/
public function getTimeout(): int
{
return $this->timeout;
}
/** /**
* @return resource * @return resource
* *
@ -102,29 +81,23 @@ abstract class AbstractSocket implements SocketInterface
} }
/** /**
* Close * @return int
*/ */
public function close(): void public function getTimeout(): int
{ {
if ($this->socket) { return $this->timeout;
fclose($this->socket);
$this->socket = null;
}
} }
/** /**
* @param string $address * @param string $address
* @param int $port * @param int $port
* @param int $timeout * @param int $timeout
* @param int $engine
* *
* @throws SocketException * @throws SocketException
*/ */
public function open(string $address, int $port, int $timeout, int $engine): void public function open(string $address, int $port, int $timeout): void
{ {
$this->timeout = $timeout; $this->timeout = $timeout;
$this->engine = $engine;
$this->port = $port; $this->port = $port;
$this->address = $address; $this->address = $address;
@ -140,17 +113,15 @@ abstract class AbstractSocket implements SocketInterface
} }
/** /**
* @param int $header * Close
* @param string $string
*
* @return bool
*/ */
public function write(int $header, string $string = ''): bool public function close(): void
{ {
$command = pack('ccccca*', 0xFF, 0xFF, 0xFF, 0xFF, $header, $string); if ($this->socket) {
$length = strlen($command); fclose($this->socket);
return $length === fwrite($this->socket, $command, $length); $this->socket = null;
}
} }
/** /**
@ -180,6 +151,20 @@ abstract class AbstractSocket implements SocketInterface
return $buffer; return $buffer;
} }
/**
* @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);
}
/** /**
* @param Buffer $buffer * @param Buffer $buffer
* @param int $length * @param int $length
@ -230,7 +215,7 @@ abstract class AbstractSocket implements SocketInterface
*/ */
protected function readInternal(Buffer $buffer, int $length, callable $sherlockFunction): Buffer protected function readInternal(Buffer $buffer, int $length, callable $sherlockFunction): Buffer
{ {
if ($buffer->remaining() === 0) { if ($buffer->isEmpty()) {
throw new InvalidPacketException('Failed to read any data from socket', InvalidPacketException::BUFFER_EMPTY); throw new InvalidPacketException('Failed to read any data from socket', InvalidPacketException::BUFFER_EMPTY);
} }

@ -2,6 +2,17 @@
declare(strict_types=1); 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; namespace xPaw\SourceQuery\Socket;
use xPaw\SourceQuery\Buffer; use xPaw\SourceQuery\Buffer;

@ -34,41 +34,32 @@ interface SocketInterface
*/ */
public function getPort(): int; public function getPort(): int;
/**
* @return int
*/
public function getTimeout(): int;
/** /**
* @return resource * @return resource
*/ */
public function getSocket(); public function getSocket();
/** /**
* Get the socket type (goldsrc/src). * @return int
*/ */
public function getType(): int; public function getTimeout(): int;
/** /**
* Close * Get the socket type (goldsrc/src).
*/ */
public function close(): void; public function getType(): int;
/** /**
* @param string $address * @param string $address
* @param int $port * @param int $port
* @param int $timeout * @param int $timeout
* @param int $engine
*/ */
public function open(string $address, int $port, int $timeout, int $engine): void; public function open(string $address, int $port, int $timeout): void;
/** /**
* @param int $header * Close
* @param string $string
*
* @return bool
*/ */
public function write(int $header, string $string = ''): bool; public function close(): void;
/** /**
* @param int $length * @param int $length
@ -76,4 +67,12 @@ interface SocketInterface
* @return Buffer * @return Buffer
*/ */
public function read(int $length = 1400): Buffer; public function read(int $length = 1400): Buffer;
/**
* @param int $header
* @param string $string
*
* @return bool
*/
public function write(int $header, string $string = ''): bool;
} }

@ -2,6 +2,17 @@
declare(strict_types=1); 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; namespace xPaw\SourceQuery\Socket;
abstract class SocketType abstract class SocketType

@ -2,6 +2,17 @@
declare(strict_types=1); 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; namespace xPaw\SourceQuery\Socket;
use xPaw\SourceQuery\Buffer; use xPaw\SourceQuery\Buffer;
@ -37,11 +48,11 @@ final class SourceSocket extends AbstractSocket
$number = $buffer->getByte() + 1; $number = $buffer->getByte() + 1;
if ($isCompressed) { if ($isCompressed) {
$buffer->getLong(); // Split size $buffer->getLong(); // Split size.
$checksum = $buffer->getUnsignedLong(); $checksum = $buffer->getUnsignedLong();
} else { } else {
$buffer->getShort(); // Split size $buffer->getShort(); // Split size.
} }
} }
} }

@ -2,6 +2,17 @@
declare(strict_types=1); 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; namespace xPaw\SourceQuery\Socket;
use SplQueue; use SplQueue;
@ -49,36 +60,23 @@ final class TestableSocket extends AbstractSocket
$this->packetQueue->push($data); $this->packetQueue->push($data);
} }
/**
* Close.
*/
public function close(): void
{
}
/** /**
* @param string $address * @param string $address
* @param int $port * @param int $port
* @param int $timeout * @param int $timeout
* @param int $engine
*/ */
public function open(string $address, int $port, int $timeout, int $engine): void public function open(string $address, int $port, int $timeout): void
{ {
$this->timeout = $timeout; $this->timeout = $timeout;
$this->engine = $engine;
$this->port = $port; $this->port = $port;
$this->address = $address; $this->address = $address;
} }
/** /**
* @param int $header * Close.
* @param string $string
*
* @return bool
*/ */
public function write(int $header, string $string = ''): bool public function close(): void
{ {
return true;
} }
/** /**
@ -98,6 +96,17 @@ final class TestableSocket extends AbstractSocket
return $buffer; return $buffer;
} }
/**
* @param int $header
* @param string $string
*
* @return bool
*/
public function write(int $header, string $string = ''): bool
{
return true;
}
/** /**
* @param Buffer $buffer * @param Buffer $buffer
* @param int $length * @param int $length
@ -166,7 +175,7 @@ final class TestableSocket extends AbstractSocket
* @param bool $isCompressed * @param bool $isCompressed
* @param int|null $checksum * @param int|null $checksum
*/ */
protected function readInternalPacketDataGoldSource( private function readInternalPacketDataGoldSource(
Buffer $buffer, Buffer $buffer,
int &$count, int &$count,
int &$number, int &$number,
@ -190,7 +199,7 @@ final class TestableSocket extends AbstractSocket
* *
* @throws InvalidPacketException * @throws InvalidPacketException
*/ */
protected function readInternalPacketDataSource( private function readInternalPacketDataSource(
Buffer $buffer, Buffer $buffer,
int &$count, int &$count,
int &$number, int &$number,
@ -201,11 +210,11 @@ final class TestableSocket extends AbstractSocket
$number = $buffer->getByte() + 1; $number = $buffer->getByte() + 1;
if ($isCompressed) { if ($isCompressed) {
$buffer->getLong(); // Split size $buffer->getLong(); // Split size.
$checksum = $buffer->getUnsignedLong(); $checksum = $buffer->getUnsignedLong();
} else { } else {
$buffer->getShort(); // Split size $buffer->getShort(); // Split size.
} }
} }
} }

@ -26,16 +26,6 @@ use xPaw\SourceQuery\Rcon\SourceRcon;
use xPaw\SourceQuery\Socket\SocketInterface; use xPaw\SourceQuery\Socket\SocketInterface;
use xPaw\SourceQuery\Socket\SocketType; use xPaw\SourceQuery\Socket\SocketType;
/**
* Class SourceQuery
*
* @package xPaw\SourceQuery
*
* @uses AuthenticationException
* @uses InvalidArgumentException
* @uses InvalidPacketException
* @uses SocketException
*/
final class SourceQuery final class SourceQuery
{ {
/** /**
@ -53,7 +43,7 @@ final class SourceQuery
private const A2A_ACK = 0x6A; private const A2A_ACK = 0x6A;
private const S2C_CHALLENGE = 0x41; private const S2C_CHALLENGE = 0x41;
private const S2A_INFO_SRC = 0x49; private const S2A_INFO_SRC = 0x49;
private const S2A_INFO_OLD = 0x6D; // Old GoldSource, HLTV uses it (actually called S2A_INFO_DETAILED) private const S2A_INFO_OLD = 0x6D; // Old GoldSource, HLTV uses it (actually called S2A_INFO_DETAILED).
private const S2A_PLAYER = 0x44; private const S2A_PLAYER = 0x44;
private const S2A_RULES = 0x45; private const S2A_RULES = 0x45;
public const S2A_RCON = 0x6C; public const S2A_RCON = 0x6C;
@ -76,7 +66,7 @@ final class SourceQuery
* *
* @var RconInterface|null * @var RconInterface|null
*/ */
private $rcon; private ?RconInterface $rcon;
/** /**
* Points to socket class * Points to socket class
@ -104,6 +94,7 @@ final class SourceQuery
public function __construct(SocketInterface $socket) public function __construct(SocketInterface $socket)
{ {
$this->socket = $socket; $this->socket = $socket;
$this->rcon = null;
} }
/** /**
@ -120,12 +111,10 @@ final class SourceQuery
* @param string $address Server ip * @param string $address Server ip
* @param int $port Server port * @param int $port Server port
* @param int $timeout Timeout period * @param int $timeout Timeout period
* @param int $engine Engine the server runs on (goldsource, source)
* *
* @throws InvalidArgumentException * @throws InvalidArgumentException
* @throws SocketException
*/ */
public function connect(string $address, int $port, int $timeout = 3, int $engine = SocketType::SOURCE): void public function connect(string $address, int $port, int $timeout = 3): void
{ {
$this->disconnect(); $this->disconnect();
@ -133,7 +122,7 @@ final class SourceQuery
throw new InvalidArgumentException('Timeout must be a positive integer.', InvalidArgumentException::TIMEOUT_NOT_INTEGER); 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);
$this->connected = true; $this->connected = true;
} }
@ -175,7 +164,6 @@ final class SourceQuery
* Sends ping packet to the server * Sends ping packet to the server
* NOTE: This may not work on some games (TF2 for example) * NOTE: This may not work on some games (TF2 for example)
* *
* @throws InvalidPacketException
* @throws SocketException * @throws SocketException
* *
* @return bool True on success, false on failure * @return bool True on success, false on failure
@ -224,7 +212,7 @@ final class SourceQuery
$type = $buffer->getByte(); $type = $buffer->getByte();
} }
// Old GoldSource protocol, HLTV still uses it // Old GoldSource protocol, HLTV still uses it.
if ($type === self::S2A_INFO_OLD && $this->socket->getType() === SocketType::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) * If we try to read data again, and we get the result with type S2A_INFO (0x49)
@ -290,8 +278,8 @@ final class SourceQuery
$server[ 'Version' ] = $buffer->getString(); $server[ 'Version' ] = $buffer->getString();
// Extra Data Flags // Extra Data Flags.
if ($buffer->remaining() > 0) { if (!$buffer->isEmpty()) {
$server[ 'ExtraDataFlags' ] = $Flags = $buffer->getByte(); $server[ 'ExtraDataFlags' ] = $Flags = $buffer->getByte();
// S2A_EXTRA_DATA_HAS_GAME_PORT - Next 2 bytes include the game port. // S2A_EXTRA_DATA_HAS_GAME_PORT - Next 2 bytes include the game port.
@ -299,12 +287,12 @@ final class SourceQuery
$server[ 'GamePort' ] = $buffer->getShort(); $server[ 'GamePort' ] = $buffer->getShort();
} }
// S2A_EXTRA_DATA_HAS_STEAMID - Next 8 bytes are the steamID // S2A_EXTRA_DATA_HAS_STEAMID - Next 8 bytes are the steamID.
// Want to play around with this? // Want to play around with this?
// You can use https://github.com/xPaw/SteamID.php // You can use https://github.com/xPaw/SteamID.php
if ($Flags & 0x10) { if ($Flags & 0x10) {
$steamIdLower = $buffer->getUnsignedLong(); $steamIdLower = $buffer->getUnsignedLong();
$steamIdInstance = $buffer->getUnsignedLong(); // This gets shifted by 32 bits, which should be steamid instance $steamIdInstance = $buffer->getUnsignedLong(); // This gets shifted by 32 bits, which should be steamid instance.
if (PHP_INT_SIZE === 4) { if (PHP_INT_SIZE === 4) {
if (extension_loaded('gmp')) { if (extension_loaded('gmp')) {
@ -329,12 +317,12 @@ final class SourceQuery
$server[ 'SpecName' ] = $buffer->getString(); $server[ 'SpecName' ] = $buffer->getString();
} }
// S2A_EXTRA_DATA_HAS_GAMETAG_DATA - Next bytes are the game tag string // S2A_EXTRA_DATA_HAS_GAMETAG_DATA - Next bytes are the game tag string.
if ($Flags & 0x20) { if ($Flags & 0x20) {
$server[ 'GameTags' ] = $buffer->getString(); $server[ 'GameTags' ] = $buffer->getString();
} }
// S2A_EXTRA_DATA_GAMEID - Next 8 bytes are the gameID of the server // S2A_EXTRA_DATA_GAMEID - Next 8 bytes are the gameID of the server.
if ($Flags & 0x01) { if ($Flags & 0x01) {
$server[ 'GameID' ] = $buffer->getUnsignedLong() | ($buffer->getUnsignedLong() << 32); $server[ 'GameID' ] = $buffer->getUnsignedLong() | ($buffer->getUnsignedLong() << 32);
} }
@ -367,7 +355,7 @@ final class SourceQuery
$this->getChallenge(self::A2S_PLAYER, self::S2A_PLAYER); $this->getChallenge(self::A2S_PLAYER, self::S2A_PLAYER);
$this->socket->write(self::A2S_PLAYER, $this->challenge); $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 $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 // 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();
@ -379,7 +367,7 @@ final class SourceQuery
$players = []; $players = [];
$count = $buffer->getByte(); $count = $buffer->getByte();
while ($count-- > 0 && $buffer->remaining() > 0) { while ($count-- > 0 && !$buffer->isEmpty()) {
$player = []; $player = [];
$player[ 'Id' ] = $buffer->getByte(); // PlayerID, is it just always 0? $player[ 'Id' ] = $buffer->getByte(); // PlayerID, is it just always 0?
$player[ 'Name' ] = $buffer->getString(); $player[ 'Name' ] = $buffer->getString();
@ -421,7 +409,7 @@ final class SourceQuery
$rules = []; $rules = [];
$count = $buffer->getShort(); $count = $buffer->getShort();
while ($count-- > 0 && $buffer->remaining() > 0) { while ($count-- > 0 && !$buffer->isEmpty()) {
$rule = $buffer->getString(); $rule = $buffer->getString();
$value = $buffer->getString(); $value = $buffer->getString();
@ -440,7 +428,6 @@ final class SourceQuery
* @param int $expectedResult * @param int $expectedResult
* *
* @throws InvalidPacketException * @throws InvalidPacketException
* @throws SocketException
*/ */
private function getChallenge(int $header, int $expectedResult): void private function getChallenge(int $header, int $expectedResult): void
{ {
@ -466,8 +453,7 @@ final class SourceQuery
} }
case $expectedResult: case $expectedResult:
{ {
// Goldsource (HLTV) // Goldsource (HLTV).
return; return;
} }
case 0: case 0:
@ -525,7 +511,7 @@ final class SourceQuery
* @param string $command Command to execute * @param string $command Command to execute
* *
* @return string Answer from server in string * @return string Answer from server in string
*@throws InvalidPacketException * @throws InvalidPacketException
* @throws SocketException * @throws SocketException
* *
* @throws AuthenticationException * @throws AuthenticationException

@ -24,7 +24,6 @@ final class Tests extends TestCase
private SourceQuery $sourceQuery; private SourceQuery $sourceQuery;
/** /**
* @throws SocketException
* @throws InvalidArgumentException * @throws InvalidArgumentException
*/ */
public function setUp(): void public function setUp(): void
@ -46,7 +45,6 @@ final class Tests extends TestCase
/** /**
* @throws InvalidArgumentException * @throws InvalidArgumentException
* @throws SocketException
*/ */
public function testInvalidTimeout(): void public function testInvalidTimeout(): void
{ {
@ -67,7 +65,6 @@ final class Tests extends TestCase
} }
/** /**
* @throws InvalidPacketException
* @throws SocketException * @throws SocketException
*/ */
public function testNotConnectedPing(): void public function testNotConnectedPing(): void
@ -166,26 +163,7 @@ final class Tests extends TestCase
*/ */
public function infoProvider(): array public function infoProvider(): array
{ {
$dataProvider = []; return $this->getData('Info', true);
$files = glob(__DIR__ . '/Info/*.raw', GLOB_ERR);
foreach ($files as $file) {
$dataProvider[] =
[
hex2bin(trim(file_get_contents($file))),
json_decode(
file_get_contents(
str_replace('.raw', '.json', $file)
),
true,
512,
JSON_THROW_ON_ERROR
)
];
}
return $dataProvider;
} }
/** /**
@ -262,12 +240,12 @@ final class Tests extends TestCase
return return
[ [
[ "" ], [ "" ],
[ "\xff\xff\xff\xff" ], // No type [ "\xff\xff\xff\xff" ], // No type.
[ "\xff\xff\xff\xff\x49" ], // Correct type, but no data after [ "\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 [ "\xff\xff\xff\xff\x6D" ], // Old info packet, but tests are done for source.
[ "\xff\xff\xff\xff\x11" ], // Wrong type [ "\xff\xff\xff\xff\x11" ], // Wrong type.
[ "\x11\x11\x11\x11" ], // Wrong header [ "\x11\x11\x11\x11" ], // Wrong header.
[ "\xff" ], // Should be 4 bytes, but it's 1 [ "\xff" ], // Should be 4 bytes, but it's 1.
]; ];
} }
@ -296,7 +274,7 @@ final class Tests extends TestCase
*/ */
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) { foreach ($rawInput as $packet) {
$this->socket->queue(hex2bin($packet)); $this->socket->queue(hex2bin($packet));
@ -314,26 +292,7 @@ final class Tests extends TestCase
*/ */
public function rulesProvider(): array public function rulesProvider(): array
{ {
$dataProvider = []; return $this->getData('Rules');
$files = glob(__DIR__ . '/Rules/*.raw', GLOB_ERR);
foreach ($files as $file) {
$dataProvider[] =
[
file($file, FILE_SKIP_EMPTY_LINES | FILE_IGNORE_NEW_LINES),
json_decode(
file_get_contents(
str_replace('.raw', '.json', $file)
),
true,
512,
JSON_THROW_ON_ERROR
)
];
}
return $dataProvider;
} }
/** /**
@ -347,7 +306,7 @@ final class Tests extends TestCase
*/ */
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) { foreach ($rawInput as $packet) {
$this->socket->queue(hex2bin($packet)); $this->socket->queue(hex2bin($packet));
@ -365,30 +324,10 @@ final class Tests extends TestCase
*/ */
public function playersProvider(): array public function playersProvider(): array
{ {
$dataProvider = []; return $this->getData('Players');
$files = glob(__DIR__ . '/Players/*.raw', GLOB_ERR);
foreach ($files as $file) {
$dataProvider[] =
[
file($file, FILE_SKIP_EMPTY_LINES | FILE_IGNORE_NEW_LINES),
json_decode(
file_get_contents(
str_replace('.raw', '.json', $file)
),
true,
512,
JSON_THROW_ON_ERROR
)
];
}
return $dataProvider;
} }
/** /**
* @throws InvalidPacketException
* @throws SocketException * @throws SocketException
*/ */
public function testPing(): void public function testPing(): void
@ -399,4 +338,40 @@ final class Tests extends TestCase
$this->socket->queue("\xFF\xFF\xFF\xFF\xEE"); $this->socket->queue("\xFF\xFF\xFF\xFF\xEE");
self::assertFalse($this->sourceQuery->ping()); self::assertFalse($this->sourceQuery->ping());
} }
/**
* @param string $path
* @param bool $hexToBin
*
* @return array
*
* @throws JsonException
*/
private function getData(string $path, bool $hexToBin = false): array
{
$dataProvider = [];
$files = glob(__DIR__ . '/' . $path . '/*.raw', GLOB_ERR);
foreach ($files as $file) {
$content = $hexToBin
? hex2bin(trim(file_get_contents($file)))
: file($file, FILE_SKIP_EMPTY_LINES | FILE_IGNORE_NEW_LINES);
$dataProvider[] =
[
$content,
json_decode(
file_get_contents(
str_replace('.raw', '.json', $file)
),
true,
512,
JSON_THROW_ON_ERROR
)
];
}
return $dataProvider;
}
} }

Loading…
Cancel
Save