mcstatus/query.go

281 lines
6.4 KiB
Go

package main
import (
"bufio"
"bytes"
"encoding/binary"
"encoding/json"
"fmt"
"io"
"math/rand"
"net"
"strconv"
"strings"
"time"
)
type ServerStatus struct {
Online bool
Players []string
OnlineCount int
MaxPlayers int
}
type slpVersion struct {
Name string `json:"name"`
Protocol int `json:"protocol"`
}
type slpSample struct {
Name string `json:"name"`
ID string `json:"id"`
}
type slpPlayers struct {
Max int `json:"max"`
Online int `json:"online"`
Sample []slpSample `json:"sample"`
}
type slpResponse struct {
Version slpVersion `json:"version"`
Players slpPlayers `json:"players"`
Description any `json:"description"`
}
func queryServer(host string, port int) (*ServerStatus, error) {
status, err := querySLP(host, port)
if err != nil {
return nil, err
}
// If SLP returned no player names, try GS4 query
if len(status.Players) == 0 && status.OnlineCount > 0 {
players, gs4Err := queryGS4(host, port)
if gs4Err == nil && len(players) > 0 {
status.Players = players
}
}
return status, nil
}
func querySLP(host string, port int) (*ServerStatus, error) {
addr := fmt.Sprintf("%s:%d", host, port)
conn, err := net.DialTimeout("tcp", addr, 10*time.Second)
if err != nil {
return nil, fmt.Errorf("connect: %w", err)
}
defer conn.Close()
if err := conn.SetDeadline(time.Now().Add(10 * time.Second)); err != nil {
return nil, fmt.Errorf("set deadline: %w", err)
}
// Handshake
{
var payload strings.Builder
writeVarInt(&payload, 767) // protocol version
writeString(&payload, host) // server address
_ = binary.Write(&payload, binary.BigEndian, uint16(port)) // server port
writeVarInt(&payload, 1) // next state: status
packet := buildPacket(0, payload.String())
if _, err := conn.Write([]byte(packet)); err != nil {
return nil, fmt.Errorf("handshake write: %w", err)
}
}
// Status request
{
packet := buildPacket(0, "")
if _, err := conn.Write([]byte(packet)); err != nil {
return nil, fmt.Errorf("request write: %w", err)
}
}
// Read response
reader := bufio.NewReader(conn)
data, err := readPacket(reader)
if err != nil {
return nil, fmt.Errorf("read response: %w", err)
}
r := strings.NewReader(string(data))
packetID, err := readVarInt(r)
if err != nil {
return nil, fmt.Errorf("read packet id: %w", err)
}
if packetID != 0 {
return nil, fmt.Errorf("unexpected packet id: %d", packetID)
}
jsonStr, err := readString(r)
if err != nil {
return nil, fmt.Errorf("read json: %w", err)
}
var resp slpResponse
if err := json.Unmarshal([]byte(jsonStr), &resp); err != nil {
return nil, fmt.Errorf("parse json: %w", err)
}
status := &ServerStatus{
Online: true,
OnlineCount: resp.Players.Online,
MaxPlayers: resp.Players.Max,
}
for _, s := range resp.Players.Sample {
status.Players = append(status.Players, s.Name)
}
return status, nil
}
func queryGS4(host string, port int) ([]string, error) {
addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", host, port))
if err != nil {
return nil, err
}
conn, err := net.DialUDP("udp", nil, addr)
if err != nil {
return nil, err
}
defer conn.Close()
sessionID := int32(rand.Int31n(0x7FFFFFFF))
// Handshake: FE FD 09 <session_id>
conn.SetDeadline(time.Now().Add(10 * time.Second))
hs := []byte{0xFE, 0xFD, 0x09}
hs = binary.BigEndian.AppendUint32(hs, uint32(sessionID))
conn.Write(hs)
resp := make([]byte, 64)
n, err := conn.Read(resp)
if err != nil {
return nil, fmt.Errorf("gs4 handshake: %w", err)
}
data := resp[:n]
tokenStr := strings.TrimRight(string(data[5:]), "\x00")
challengeToken, err := strconv.Atoi(tokenStr)
if err != nil {
return nil, fmt.Errorf("parse challenge: %w", err)
}
// Full stat query: FE FD 00 <session_id> <challenge_token> 00 00 00 00
conn.SetDeadline(time.Now().Add(10 * time.Second))
q := []byte{0xFE, 0xFD, 0x00}
q = binary.BigEndian.AppendUint32(q, uint32(sessionID))
q = binary.BigEndian.AppendUint32(q, uint32(challengeToken))
q = append(q, 0x00, 0x00, 0x00, 0x00)
conn.Write(q)
resp = make([]byte, 65536)
n, err = conn.Read(resp)
if err != nil {
return nil, fmt.Errorf("gs4 query: %w", err)
}
data = resp[:n]
// Skip: type(1) + sessionID(4) + padding(11) = 16 bytes
content := data[16:]
sepIdx := bytes.Index(content, []byte{0x00, 0x00, 0x01})
if sepIdx < 0 {
return nil, fmt.Errorf("gs4: player separator not found")
}
playerData := content[sepIdx+3:]
playerData = bytes.TrimPrefix(playerData, []byte("player_\x00\x00"))
playerData = bytes.TrimRight(playerData, "\x00")
players := bytes.Split(playerData, []byte{0x00})
var result []string
for _, p := range players {
if len(p) > 0 {
result = append(result, string(p))
}
}
return result, nil
}
func buildPacket(id int, payload string) string {
var inner strings.Builder
writeVarInt(&inner, id)
inner.WriteString(payload)
var buf strings.Builder
writeVarInt(&buf, inner.Len())
buf.WriteString(inner.String())
return buf.String()
}
func readPacket(r *bufio.Reader) ([]byte, error) {
length, err := readVarInt(r)
if err != nil {
return nil, fmt.Errorf("read length: %w", err)
}
if length < 0 || length > 65536 {
return nil, fmt.Errorf("invalid packet length: %d", length)
}
buf := make([]byte, length)
if _, err := io.ReadFull(r, buf); err != nil {
return nil, fmt.Errorf("read payload: %w", err)
}
return buf, nil
}
func readVarInt(r interface{ ReadByte() (byte, error) }) (int, error) {
result := 0
shift := 0
for {
b, err := r.ReadByte()
if err != nil {
return 0, err
}
result |= int(b&0x7F) << shift
shift += 7
if shift > 35 {
return 0, fmt.Errorf("varint too long")
}
if b&0x80 == 0 {
break
}
}
return result, nil
}
func writeVarInt(w interface{ WriteByte(byte) error }, value int) {
for value > 0x7F {
w.WriteByte(byte(value&0x7F) | 0x80)
value >>= 7
}
w.WriteByte(byte(value & 0x7F))
}
func readString(r interface{ ReadByte() (byte, error) }) (string, error) {
length, err := readVarInt(r)
if err != nil {
return "", err
}
if length < 0 || length > 65536 {
return "", fmt.Errorf("invalid string length: %d", length)
}
buf := make([]byte, length)
if _, err := io.ReadFull(r.(io.Reader), buf); err != nil {
return "", fmt.Errorf("read string: %w", err)
}
return string(buf), nil
}
func writeString(w interface{ WriteByte(byte) error }, s string) {
writeVarInt(w, len(s))
for i := 0; i < len(s); i++ {
w.WriteByte(s[i])
}
}