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