diff --git a/README.md b/README.md index cd93cb0..0295195 100644 --- a/README.md +++ b/README.md @@ -13,17 +13,9 @@ PHP 8 library for Half-Life API with native IPv6 / Yggdrasil support #### Master ``` -$master = new \Yggverse\Hl\Xash3D\Master('hl.ygg', 27010); +$master = new Yggverse\Hl\Xash3d\Master('hl.ygg', 27010); var_dump( $master->getServersIPv6() ); - -var_dump( - $master->getErrors() -); -``` - -## Projects - -* [HLState](https://github.com/YGGverse/HLState) - Web Monitor for Half-Life Servers \ No newline at end of file +``` \ No newline at end of file diff --git a/composer.json b/composer.json index cf92306..55c3533 100644 --- a/composer.json +++ b/composer.json @@ -1,8 +1,6 @@ { "name": "yggverse/hl", "description": "PHP 8 library for Half-Life API with native IPv6 / Yggdrasil support ", - "keywords": [ "half-life", "hl", "xash3d", "ipv6", "yggdrasil", "master" ], - "homepage": "https://github.com/YGGverse/hl-php", "type": "library", "license": "MIT", "autoload": { diff --git a/src/Xash3D/Master.php b/src/Xash3D/Master.php index 5ea743e..c748d53 100644 --- a/src/Xash3D/Master.php +++ b/src/Xash3D/Master.php @@ -2,127 +2,66 @@ declare(strict_types=1); -namespace Yggverse\Hl\Xash3D; - -enum Family: int { - case IPv4 = 4; - case IPv6 = 16; -} - -enum Region: string { - case USEastCoast = "0x00"; - case USWestCoast = "0x01"; - case SouthAmerica = "0x02"; - case Europe = "0x03"; - case Asia = "0x04"; - case Australia = "0x05"; - case MiddleEast = "0x06"; - case Africa = "0x07"; - case World = "0xff"; -} - -enum Game: string { - case Valve = "valve"; -} +namespace Yggverse\Hl\Xash3d; class Master { - private string $_host; - private int $_port; - private int $_timeout; - - private array $_errors = []; + private $_socket; public function __construct( string $host, - int $port = 27010, + int $port, int $timeout = 5 ) { - $this->_host = $host; - $this->_port = $port; - $this->_timeout = $timeout; - } - - private function _fclose( - mixed $socket - ) - { - if (true === is_resource($socket)) - { - fclose( - $socket - ); - } - } - - // Legacy protocol implementation does not support mixed address families - // in the binary master socket response, use separated method for IPv4 servers. - public function getServers( - int $limit = 100, - string $host = "0.0.0.0", - int $port = 0, - Game $game = Game::Valve, - Region $region = Region::World - ): ?array - { - $family = filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) ? Family::IPv4 : Family::IPv6; - - // Init connection - $socket = fsockopen( - "udp://{$this->_host}", - $this->_port, - $code, - $message, - $this->_timeout + $this->_socket = fsockopen( + "udp://{$host}", + $port ); - $master = "{$this->_host}:{$this->_port}"; + stream_set_timeout( + $this->_socket, + $timeout + ); + } - // Is connected - if (true === is_resource($socket)) + public function __destruct() + { + if ($this->_socket) { - stream_set_timeout( - $socket, - $this->_timeout + fclose( + $this->_socket ); } + } - else + public static function getServersIPv6( + int $limit = 100, + string $region = "\xFF", + string $host = "0.0.0.0:0", + int $port = 0, + string $gamedir = "valve" + ): ?array + { + // Is connected + if (!$this->_socket) { - $this->_errors[] = sprintf( - _("Connection error for $master: %s"), - $message - ); - - $this->_fclose( - $socket - ); - return null; } // Filter query - if (false === fwrite($socket, "1{$region->value}{$host}:{$port}\0\\gamedir\\{$game->value}\0")) + if (!fwrite($this->_socket, "1{$region}{$host}:{$port}\0\gamedir\t{$gamedir}\0")) { - $this->_errors[] = _("Could not send socket query for $master"); - - $this->_fclose( - $socket + fclose( + $this->_socket ); return null; } // Skip header - if (false === fread($socket, 6)) + if (!fread($this->_socket, 6)) { - $this->_errors[] = _("Could not init packet header for $master"); - - $this->_fclose( - $socket - ); - return null; } @@ -131,64 +70,56 @@ class Master for ($i = 0; $i < $limit; $i++) { - // Get host bytes - if (false === $host = fread($socket, $family->value)) + // Get host + if (false === $host = fread($this->_socket, 16)) { - $this->_errors[] = _("Invalid `host` fragment in packet at $i for $master"); - break; + return null; } - // End of packet - if (true === str_ends_with(bin2hex($host), bin2hex("\0\0\0\0\0\0"))) + // Is end of packet + if (true === str_starts_with($host, 0)) { break; } - // Get host string + // Skip invalid host if (false === $host = inet_ntop($host)) { - $this->_errors[] = _("Invalid `host` value in packet at $i for $master"); - break; + continue; } - // Get port bytes - if (false === $p = fread($socket, 2)) + // Decode first byte for port + if (false === $byte1 = fread($this->_socket, 1)) { - $this->_errors[] = _("Invalid `port` fragment in packet at $i for $master"); - break; + return null; } - // Get port value - if (false === $p = unpack('nport', $p)) + // Decode second byte for port + if (false === $byte2 = fread($this->_socket, 1)) { - $this->_errors[] = _("Invalid `port` value in packet at $i for $master"); - break; + return null; } - // Validate result - if (false === filter_var($host, FILTER_VALIDATE_IP, $family == Family::IPv6 ? FILTER_FLAG_IPV6 - : FILTER_FLAG_IPV4) || empty($p['port'])) + // Calculate port value + $port = ord($byte1) * 256 + ord($byte2); + + // Validate IPv6 result + if ( + false !== strpos($host, '.') || // filter_var not always works with mixed IPv6 + false === filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) || + false === filter_var($port, FILTER_VALIDATE_INT) + ) { - $this->_errors[] = _("Invalid socket address in packet at $i for $master"); - break; + continue; } - $servers["{$host}{$p['port']}"] = // keep unique + $servers["[{$host}]:{$port}"] = // keep unique [ 'host' => $host, - 'port' => $p['port'] + 'port' => $port ]; } - $this->_fclose( - $socket - ); - return $servers; } - - public function getErrors(): array - { - return $this->_errors; - } } \ No newline at end of file