167 lines
5.5 KiB
PHP
167 lines
5.5 KiB
PHP
|
<?php
|
||
|
namespace Mince;
|
||
|
|
||
|
use stdClass;
|
||
|
use RuntimeException;
|
||
|
use Socket;
|
||
|
use Index\AString;
|
||
|
use Index\Net\EndPoint;
|
||
|
use Index\Net\IPAddress;
|
||
|
use Index\Net\IPEndPoint;
|
||
|
use Index\Net\DnsEndPoint;
|
||
|
|
||
|
class ServerQuery { // rewrite this to use https://wiki.vg/Server_List_Ping query is kinda useless
|
||
|
public const PORT = 25565;
|
||
|
|
||
|
private string $addr;
|
||
|
private int $port;
|
||
|
private Socket $socket;
|
||
|
private int $sessionId;
|
||
|
private int $challengeToken = 0;
|
||
|
|
||
|
public function __construct(IPAddress $addr, int $port) {
|
||
|
$this->addr = (string)$addr;
|
||
|
$this->port = $port;
|
||
|
$this->sessionId = random_int(0, 0x7FFFFFFF) & 0x0F0F0F0F;
|
||
|
$this->socket = socket_create($addr->isV6() ? AF_INET6 : AF_INET, SOCK_DGRAM, SOL_UDP);
|
||
|
$this->handshake();
|
||
|
}
|
||
|
|
||
|
public function __destruct() {
|
||
|
socket_close($this->socket);
|
||
|
}
|
||
|
|
||
|
public function handshake(): void {
|
||
|
$response = $this->send(9);
|
||
|
$length = strlen($response);
|
||
|
$token = '';
|
||
|
|
||
|
for($i = 0; $i < $length; ++$i) {
|
||
|
$char = $response[$i];
|
||
|
if($char === "\0")
|
||
|
break;
|
||
|
$token .= $char;
|
||
|
}
|
||
|
|
||
|
$this->challengeToken = intval($token);
|
||
|
}
|
||
|
|
||
|
public function stats(): object {
|
||
|
$response = $this->send(0, pack('N', $this->challengeToken));
|
||
|
|
||
|
$offset = 0;
|
||
|
$data = new stdClass;
|
||
|
|
||
|
$data->motd = self::readString($response, $offset);
|
||
|
$data->gametype = self::readString($response, $offset);
|
||
|
$data->map = self::readString($response, $offset);
|
||
|
$data->numplayers = self::readString($response, $offset);
|
||
|
$data->maxplayers = self::readString($response, $offset);
|
||
|
$data->hostport = unpack('v', substr($response, $offset, 2))[1];
|
||
|
$offset += 2;
|
||
|
$data->hostip = self::readString($response, $offset);
|
||
|
|
||
|
return $data;
|
||
|
}
|
||
|
|
||
|
private static function readString(string $source, int &$offset): string {
|
||
|
$length = strlen($source);
|
||
|
$string = '';
|
||
|
|
||
|
for(; $offset < $length; ++$offset) {
|
||
|
$char = $source[$offset];
|
||
|
if($char === "\0")
|
||
|
break;
|
||
|
$string .= $char;
|
||
|
}
|
||
|
|
||
|
++$offset;
|
||
|
|
||
|
return $string;
|
||
|
}
|
||
|
|
||
|
private function send(int $type, string $payload = ''): string {
|
||
|
$payload = "\xFE\xFD" . pack('CN', $type, $this->sessionId) . $payload;
|
||
|
socket_sendto($this->socket, $payload, strlen($payload), 0, $this->addr, $this->port);
|
||
|
socket_recv($this->socket, $response, 1024, MSG_WAITALL);
|
||
|
|
||
|
$data = unpack('Ctype/Nsession', $response);
|
||
|
|
||
|
if($data['type'] !== $type)
|
||
|
throw new RuntimeException('Type does not match.');
|
||
|
if($data['session'] !== $this->sessionId)
|
||
|
throw new RuntimeException('Session id does not match.');
|
||
|
|
||
|
return substr($response, 5);
|
||
|
}
|
||
|
|
||
|
public static function create(AString|string $endPoint): ServerQuery {
|
||
|
$endPoint = AString::cast($endPoint);
|
||
|
$firstChar = $endPoint[0];
|
||
|
|
||
|
if($firstChar === '[') { // IPv6
|
||
|
if($endPoint->contains(']:'))
|
||
|
$endPoint = IPEndPoint::parse($endPoint);
|
||
|
else
|
||
|
$endPoint = new IPEndPoint(IPAddress::parse($endPoint->trim('[]')), self::PORT);
|
||
|
|
||
|
return new ServerQuery($endPoint->getAddress(), $endPoint->getPort());
|
||
|
} elseif(is_numeric($firstChar)) { // IPv4
|
||
|
if($endPoint->contains(':'))
|
||
|
$endPoint = IPEndPoint::parse($endPoint);
|
||
|
else
|
||
|
$endPoint = new IPEndPoint(IPAddress::parse($endPoint), self::PORT);
|
||
|
|
||
|
return new ServerQuery($endPoint->getAddress(), $endPoint->getPort());
|
||
|
} else { // DNS
|
||
|
if($endPoint->contains(':'))
|
||
|
$endPoint = DnsEndPoint::parse($endPoint);
|
||
|
else {
|
||
|
$endPoint = new DnsEndPoint($endPoint, self::PORT);
|
||
|
|
||
|
$records = dns_get_record('_minecraft._tcp.' . (string)$endPoint->getHost(), DNS_SRV);
|
||
|
|
||
|
if(!empty($records)) {
|
||
|
usort($records, function($a, $b) {
|
||
|
$priority = $a['pri'] <=> $b['pri'];
|
||
|
if($priority !== 0)
|
||
|
return $priority;
|
||
|
$weight = $a['weight'] <=> $b['weight'];
|
||
|
if($weight !== 0)
|
||
|
return $priority;
|
||
|
return 0;
|
||
|
});
|
||
|
|
||
|
foreach($records as $record) {
|
||
|
try {
|
||
|
return ServerQuery::create($record['target'] . ':' . $record['port']);
|
||
|
} catch(Exception $ex) {}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$records = dns_get_record((string)$endPoint->getHost(), DNS_A);
|
||
|
|
||
|
if(!empty($records)) {
|
||
|
foreach($records as $record) {
|
||
|
try {
|
||
|
return new ServerQuery(IPAddress::parse($record['ip']), $endPoint->getPort());
|
||
|
} catch(Exception $ex) {}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$records = dns_get_record((string)$endPoint->getHost(), DNS_AAAA);
|
||
|
|
||
|
if(!empty($records)) {
|
||
|
foreach($records as $record) {
|
||
|
try {
|
||
|
return new ServerQuery(IPAddress::parse($record['ipv6']), $endPoint->getPort());
|
||
|
} catch(Exception $ex) {}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
throw new RuntimeException('Failed to connect.');
|
||
|
}
|
||
|
}
|