Compare commits

..

26 commits
1.0.0 ... main

Author SHA1 Message Date
yggverse
42b1195b83 add all supported regions 2026-03-05 03:26:58 +02:00
yggverse
9f4e924592 use break as nothing to continue on any piece fail 2026-03-05 01:11:24 +02:00
yggverse
795f304789 use enumeration types 2026-03-05 01:08:13 +02:00
yggverse
a101a55715 revert getServersIPv6 method as mixed address families are not supported by legacy protocol implementation 2026-03-05 00:42:21 +02:00
yggverse
13a7c607e1 update parsing conditions, remove hardcoded ipv6 impl 2026-03-04 23:19:46 +02:00
yggverse
34390a0713 simplify port decoding 2026-03-04 22:56:37 +02:00
yggverse
167f092a28 continue, not break 2026-03-04 22:32:21 +02:00
yggverse
bacfce5f1b reorder arguments 2026-03-04 21:24:12 +02:00
yggverse
82c8564645 fix syntax error 2026-03-04 20:50:03 +02:00
yggverse
b1fce6a668 fix ipv6/xash3d-master compatibility; update method name 2026-03-04 20:47:48 +02:00
ghost
4f8fffed17 make socket connections isolated 2024-01-13 20:03:23 +02:00
ghost
550efd120f fix packet end condition 2024-01-13 18:36:00 +02:00
ghost
3ffb314fa4 update readme 2024-01-12 20:28:03 +02:00
ghost
15035498b3 add socket check 2024-01-12 20:26:51 +02:00
ghost
27fbf92c6b add __destruct method 2024-01-12 20:09:20 +02:00
ghost
726463e94e fix packet reader 2024-01-12 20:06:45 +02:00
ghost
33a77a858b add strict datatype validation 2024-01-12 19:34:06 +02:00
ghost
a97a2de151 add meta info 2024-01-12 19:29:57 +02:00
ghost
be344abd36 add errors debug 2024-01-12 19:10:43 +02:00
ghost
9c9625cb21 add resource validation 2024-01-12 18:49:53 +02:00
ghost
b7a5314030 remove __destruct method 2024-01-12 18:46:34 +02:00
ghost
d07ba95654 fix data type 2024-01-12 18:44:56 +02:00
ghost
c942dc98d0 fix method type 2024-01-12 18:43:05 +02:00
ghost
6479e40496 add socket connection closers 2024-01-12 18:40:21 +02:00
ghost
49c5c76908 add socket validation 2024-01-12 18:36:47 +02:00
ghost
cd22acf846 fix namespaces 2024-01-12 18:32:51 +02:00
3 changed files with 130 additions and 51 deletions

View file

@ -13,9 +13,17 @@ 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

View file

@ -1,6 +1,8 @@
{
"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": {

View file

@ -2,66 +2,127 @@
declare(strict_types=1);
namespace Yggverse\Hl\Xash3d;
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";
}
class Master
{
private $_socket;
private string $_host;
private int $_port;
private int $_timeout;
private array $_errors = [];
public function __construct(
string $host,
int $port,
int $port = 27010,
int $timeout = 5
)
{
$this->_socket = fsockopen(
"udp://{$host}",
$port
);
stream_set_timeout(
$this->_socket,
$timeout
);
$this->_host = $host;
$this->_port = $port;
$this->_timeout = $timeout;
}
public function __destruct()
private function _fclose(
mixed $socket
)
{
if ($this->_socket)
if (true === is_resource($socket))
{
fclose(
$this->_socket
$socket
);
}
}
public static function getServersIPv6(
// 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 $region = "\xFF",
string $host = "0.0.0.0:0",
string $host = "0.0.0.0",
int $port = 0,
string $gamedir = "valve"
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
);
$master = "{$this->_host}:{$this->_port}";
// Is connected
if (!$this->_socket)
if (true === is_resource($socket))
{
stream_set_timeout(
$socket,
$this->_timeout
);
}
else
{
$this->_errors[] = sprintf(
_("Connection error for $master: %s"),
$message
);
$this->_fclose(
$socket
);
return null;
}
// Filter query
if (!fwrite($this->_socket, "1{$region}{$host}:{$port}\0\gamedir\t{$gamedir}\0"))
if (false === fwrite($socket, "1{$region->value}{$host}:{$port}\0\\gamedir\\{$game->value}\0"))
{
fclose(
$this->_socket
$this->_errors[] = _("Could not send socket query for $master");
$this->_fclose(
$socket
);
return null;
}
// Skip header
if (!fread($this->_socket, 6))
if (false === fread($socket, 6))
{
$this->_errors[] = _("Could not init packet header for $master");
$this->_fclose(
$socket
);
return null;
}
@ -70,56 +131,64 @@ class Master
for ($i = 0; $i < $limit; $i++)
{
// Get host
if (false === $host = fread($this->_socket, 16))
// Get host bytes
if (false === $host = fread($socket, $family->value))
{
return null;
$this->_errors[] = _("Invalid `host` fragment in packet at $i for $master");
break;
}
// Is end of packet
if (true === str_starts_with($host, 0))
// End of packet
if (true === str_ends_with(bin2hex($host), bin2hex("\0\0\0\0\0\0")))
{
break;
}
// Skip invalid host
// Get host string
if (false === $host = inet_ntop($host))
{
continue;
$this->_errors[] = _("Invalid `host` value in packet at $i for $master");
break;
}
// Decode first byte for port
if (false === $byte1 = fread($this->_socket, 1))
// Get port bytes
if (false === $p = fread($socket, 2))
{
return null;
$this->_errors[] = _("Invalid `port` fragment in packet at $i for $master");
break;
}
// Decode second byte for port
if (false === $byte2 = fread($this->_socket, 1))
// Get port value
if (false === $p = unpack('nport', $p))
{
return null;
$this->_errors[] = _("Invalid `port` value in packet at $i for $master");
break;
}
// 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)
)
// Validate result
if (false === filter_var($host, FILTER_VALIDATE_IP, $family == Family::IPv6 ? FILTER_FLAG_IPV6
: FILTER_FLAG_IPV4) || empty($p['port']))
{
continue;
$this->_errors[] = _("Invalid socket address in packet at $i for $master");
break;
}
$servers["[{$host}]:{$port}"] = // keep unique
$servers["{$host}{$p['port']}"] = // keep unique
[
'host' => $host,
'port' => $port
'port' => $p['port']
];
}
$this->_fclose(
$socket
);
return $servers;
}
public function getErrors(): array
{
return $this->_errors;
}
}