mince/src/ServerQuery.php
2022-07-03 22:07:00 +00:00

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.');
}
}