193 lines
4.3 KiB
Go
193 lines
4.3 KiB
Go
package java
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/VexoraDevelopment/gamestatus/info"
|
|
)
|
|
|
|
func (c *Client) QueryFull(address string, port int) (*info.ServerInfo, error) {
|
|
if port == 0 {
|
|
port = 25565
|
|
}
|
|
|
|
host := address
|
|
p := port
|
|
if c.opt.EnableSRV && net.ParseIP(host) == nil {
|
|
if rh, rp, err := c.resolveSRV(host); err == nil && rh != "" {
|
|
host = rh
|
|
if rp != 0 {
|
|
p = rp
|
|
}
|
|
}
|
|
}
|
|
|
|
conn, _, err := c.dialUDP(host, p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
|
|
start := time.Now()
|
|
|
|
chalPayload, err := c.writeQueryPacket(conn, 0x09, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
chalStr := strings.TrimRight(string(chalPayload), "\x00\r\n\t ")
|
|
chalInt, err := strconv.Atoi(chalStr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("java query challenge parse failed: %w (payload=%q)", err, chalStr)
|
|
}
|
|
|
|
chal := make([]byte, 4)
|
|
binary.BigEndian.PutUint32(chal, uint32(chalInt))
|
|
|
|
appendData := make([]byte, 0, 8)
|
|
appendData = append(appendData, chal...)
|
|
appendData = append(appendData, 0x00, 0x00, 0x00, 0x00)
|
|
|
|
data, err := c.writeQueryPacket(conn, 0x00, appendData)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(data) < 11 {
|
|
return nil, errors.New("java query full stat response too short")
|
|
}
|
|
payload := data[11:]
|
|
|
|
marker := []byte{0x00, 0x00, 0x01, 'p', 'l', 'a', 'y', 'e', 'r', '_', 0x00, 0x00}
|
|
parts := bytes.SplitN(payload, marker, 2)
|
|
if len(parts) != 2 {
|
|
return nil, errors.New("java query parse failed: marker not found")
|
|
}
|
|
kvPart := parts[0]
|
|
playersPart := parts[1]
|
|
if len(playersPart) >= 2 {
|
|
playersPart = playersPart[:len(playersPart)-2]
|
|
}
|
|
|
|
raw := map[string]string{}
|
|
kvs := bytes.Split(kvPart, []byte{0x00})
|
|
for i := 0; i+1 < len(kvs); i += 2 {
|
|
k := string(kvs[i])
|
|
v := string(kvs[i+1])
|
|
if k == "" {
|
|
continue
|
|
}
|
|
raw[k] = v
|
|
}
|
|
|
|
var players []string
|
|
if len(playersPart) > 0 {
|
|
pp := bytes.Split(playersPart, []byte{0x00})
|
|
for _, b := range pp {
|
|
s := string(b)
|
|
if s != "" {
|
|
players = append(players, s)
|
|
}
|
|
}
|
|
}
|
|
|
|
srvInfo := &info.ServerInfo{
|
|
Edition: info.EditionJava,
|
|
Address: address,
|
|
IP: resolveIP(address),
|
|
Port: intFromMap(raw, "hostport", port),
|
|
MOTD: raw["hostname"],
|
|
Version: raw["version"],
|
|
GameMode: raw["gametype"],
|
|
Map: raw["map"],
|
|
Game: raw["game_id"],
|
|
Online: intFromMap(raw, "numplayers", 0),
|
|
Slots: intFromMap(raw, "maxplayers", 0),
|
|
Players: players,
|
|
Query: true,
|
|
RawQuery: raw,
|
|
Latency: time.Since(start),
|
|
}
|
|
|
|
if pl := raw["plugins"]; pl != "" {
|
|
split := strings.SplitN(pl, ": ", 2)
|
|
srvInfo.Software = split[0]
|
|
if len(split) == 2 {
|
|
p2 := strings.Split(split[1], "; ")
|
|
clean := make([]string, 0, len(p2))
|
|
for _, x := range p2 {
|
|
x = strings.TrimSpace(x)
|
|
if x != "" {
|
|
clean = append(clean, x)
|
|
}
|
|
}
|
|
if len(clean) > 0 {
|
|
srvInfo.Plugins = clean
|
|
}
|
|
}
|
|
} else if eng := raw["server_engine"]; eng != "" {
|
|
srvInfo.Software = eng
|
|
}
|
|
|
|
return srvInfo, nil
|
|
}
|
|
|
|
func (c *Client) dialUDP(host string, port int) (*net.UDPConn, *net.UDPAddr, error) {
|
|
raddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", host, port))
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
conn, err := net.DialUDP("udp", nil, raddr)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if err := conn.SetDeadline(time.Now().Add(c.opt.Timeout)); err != nil {
|
|
_ = conn.Close()
|
|
return nil, nil, err
|
|
}
|
|
return conn, raddr, nil
|
|
}
|
|
|
|
func (c *Client) writeQueryPacket(conn *net.UDPConn, typ byte, appendData []byte) ([]byte, error) {
|
|
header := []byte{0xFE, 0xFD, typ, 0x01, 0x02, 0x03, 0x04}
|
|
packet := make([]byte, 0, len(header)+len(appendData))
|
|
packet = append(packet, header...)
|
|
packet = append(packet, appendData...)
|
|
|
|
if err := conn.SetDeadline(time.Now().Add(c.opt.Timeout)); err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := conn.Write(packet); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
buf := make([]byte, 4096)
|
|
n, err := conn.Read(buf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp := buf[:n]
|
|
if len(resp) < 5 || resp[0] != typ {
|
|
return nil, errors.New("java query response validation failed")
|
|
}
|
|
return resp[5:], nil
|
|
}
|
|
|
|
func intFromMap(m map[string]string, key string, def int) int {
|
|
v := strings.TrimSpace(m[key])
|
|
if v == "" {
|
|
return def
|
|
}
|
|
n, err := strconv.Atoi(v)
|
|
if err != nil {
|
|
return def
|
|
}
|
|
return n
|
|
}
|