first commit

This commit is contained in:
2026-04-04 20:01:57 +03:00
commit 383b3fe543
10 changed files with 1159 additions and 0 deletions

378
java/java.go Normal file
View File

@@ -0,0 +1,378 @@
package java
import (
"bytes"
"context"
"crypto/tls"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"strconv"
"strings"
"time"
"github.com/VexoraDevelopment/mcstatus/info"
)
type Options struct {
Timeout time.Duration
EnableSRV bool
Resolver *net.Resolver
UseTLS bool
TLSConfig *tls.Config
}
type Client struct {
opt Options
}
func New(opt Options) *Client {
if opt.Timeout <= 0 {
opt.Timeout = 2 * time.Second
}
if opt.Resolver == nil {
opt.Resolver = net.DefaultResolver
}
return &Client{opt: opt}
}
func (c *Client) Connect(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
}
}
}
return c.Status(host, p)
}
func (c *Client) Status(address string, port int) (*info.ServerInfo, error) {
conn, err := c.dial(address, port)
if err != nil {
return nil, err
}
defer conn.Close()
_ = conn.SetDeadline(time.Now().Add(c.opt.Timeout))
const protocolVersion = 47
if err := writePacket(conn, func(w *bytes.Buffer) {
writeVarInt(w, 0x00)
writeVarInt(w, protocolVersion)
writeString(w, address)
writeU16(w, uint16(port))
writeVarInt(w, 1)
}); err != nil {
return nil, err
}
if err := writePacket(conn, func(w *bytes.Buffer) {
writeVarInt(w, 0x00)
}); err != nil {
return nil, err
}
pkt, err := readPacket(conn)
if err != nil {
return nil, err
}
r := bytes.NewReader(pkt)
pid, err := readVarInt(r)
if err != nil {
return nil, err
}
if pid != 0x00 {
return nil, fmt.Errorf("unexpected status response packet id: %d", pid)
}
js, err := readString(r)
if err != nil {
return nil, err
}
latency, _ := measureLatency(conn)
raw := json.RawMessage(js)
parsed, err := parseJavaStatus(raw)
if err != nil {
return nil, err
}
ip := resolveIP(address)
srvInfo := &info.ServerInfo{
Edition: info.EditionJava,
Address: address,
IP: ip,
Port: port,
MOTD: parsed.MOTD,
Version: parsed.VersionName,
Software: parsed.VersionName,
Online: parsed.Online,
Slots: parsed.Max,
Latency: latency,
Raw: raw,
}
return srvInfo, nil
}
func (c *Client) resolveSRV(domain string) (string, int, error) {
ctx, cancel := context.WithTimeout(context.Background(), c.opt.Timeout)
defer cancel()
_, addrs, err := c.opt.Resolver.LookupSRV(ctx, "minecraft", "tcp", domain)
if err != nil || len(addrs) == 0 {
return "", 0, errors.New("no _minecraft._tcp SRV record")
}
target := strings.TrimSuffix(addrs[0].Target, ".")
return target, int(addrs[0].Port), nil
}
func (c *Client) dial(host string, port int) (net.Conn, error) {
addr := net.JoinHostPort(host, strconv.Itoa(port))
d := &net.Dialer{Timeout: c.opt.Timeout}
if !c.opt.UseTLS {
return d.Dial("tcp", addr)
}
cfg := c.opt.TLSConfig
if cfg == nil {
cfg = &tls.Config{ServerName: host}
}
return tls.DialWithDialer(d, "tcp", addr, cfg)
}
func writePacket(w io.Writer, build func(*bytes.Buffer)) error {
var payload bytes.Buffer
build(&payload)
var framed bytes.Buffer
writeVarInt(&framed, payload.Len())
_, _ = framed.Write(payload.Bytes())
_, err := w.Write(framed.Bytes())
return err
}
func readPacket(r io.Reader) ([]byte, error) {
length, err := readVarIntFromIO(r)
if err != nil {
return nil, err
}
if length < 0 || length > 1<<20 {
return nil, fmt.Errorf("invalid packet length: %d", length)
}
buf := make([]byte, length)
if _, err := io.ReadFull(r, buf); err != nil {
return nil, err
}
return buf, nil
}
func writeVarInt(w *bytes.Buffer, v int) {
uv := uint32(v)
for {
if (uv & ^uint32(0x7F)) == 0 {
_ = w.WriteByte(byte(uv))
return
}
_ = w.WriteByte(byte(uv&0x7F | 0x80))
uv >>= 7
}
}
func readVarInt(r *bytes.Reader) (int, error) {
var numRead int
var result int
for {
if numRead > 5 {
return 0, errors.New("varint too big")
}
b, err := r.ReadByte()
if err != nil {
return 0, err
}
value := int(b & 0x7F)
result |= value << (7 * numRead)
numRead++
if (b & 0x80) == 0 {
break
}
}
return result, nil
}
func readVarIntFromIO(r io.Reader) (int, error) {
var numRead int
var result int
var one [1]byte
for {
if numRead > 5 {
return 0, errors.New("varint too big")
}
if _, err := io.ReadFull(r, one[:]); err != nil {
return 0, err
}
b := one[0]
value := int(b & 0x7F)
result |= value << (7 * numRead)
numRead++
if (b & 0x80) == 0 {
break
}
}
return result, nil
}
func writeString(w *bytes.Buffer, s string) {
writeVarInt(w, len(s))
_, _ = w.WriteString(s)
}
func readString(r *bytes.Reader) (string, error) {
n, err := readVarInt(r)
if err != nil {
return "", err
}
if n < 0 || n > 1<<20 {
return "", fmt.Errorf("invalid string length: %d", n)
}
b := make([]byte, n)
if _, err := io.ReadFull(r, b); err != nil {
return "", err
}
return string(b), nil
}
func writeU16(w *bytes.Buffer, v uint16) {
var tmp [2]byte
binary.BigEndian.PutUint16(tmp[:], v)
_, _ = w.Write(tmp[:])
}
func measureLatency(conn net.Conn) (time.Duration, error) {
start := time.Now()
ts := time.Now().UnixMilli()
if err := writePacket(conn, func(w *bytes.Buffer) {
writeVarInt(w, 0x01)
var tmp [8]byte
binary.BigEndian.PutUint64(tmp[:], uint64(ts))
_, _ = w.Write(tmp[:])
}); err != nil {
return 0, err
}
pkt, err := readPacket(conn)
if err != nil {
return 0, err
}
r := bytes.NewReader(pkt)
pid, err := readVarInt(r)
if err != nil {
return 0, err
}
if pid != 0x01 {
return 0, fmt.Errorf("unexpected pong packet id: %d", pid)
}
return time.Since(start), nil
}
type javaStatusParsed struct {
VersionName string
Online int
Max int
MOTD string
}
func parseJavaStatus(raw json.RawMessage) (*javaStatusParsed, error) {
var doc map[string]any
if err := json.Unmarshal(raw, &doc); err != nil {
return nil, err
}
out := &javaStatusParsed{}
if v, ok := doc["version"].(map[string]any); ok {
if name, _ := v["name"].(string); name != "" {
out.VersionName = name
}
}
if p, ok := doc["players"].(map[string]any); ok {
out.Online = asInt(p["online"])
out.Max = asInt(p["max"])
}
out.MOTD = parseDescription(doc["description"])
return out, nil
}
func parseDescription(v any) string {
switch t := v.(type) {
case string:
return t
case map[string]any:
if s, _ := t["text"].(string); s != "" {
return s
}
if extra, ok := t["extra"].([]any); ok {
var b strings.Builder
for _, it := range extra {
if m, ok := it.(map[string]any); ok {
if s, _ := m["text"].(string); s != "" {
b.WriteString(s)
}
}
}
if b.Len() > 0 {
return b.String()
}
}
}
return ""
}
func asInt(v any) int {
switch t := v.(type) {
case float64:
return int(t)
case int:
return t
case json.Number:
n, _ := t.Int64()
return int(n)
default:
return 0
}
}
func resolveIP(host string) string {
if ip := net.ParseIP(host); ip != nil {
return ip.String()
}
ips, err := net.LookupIP(host)
if err != nil || len(ips) == 0 {
return ""
}
return ips[0].String()
}

192
java/query.go Normal file
View File

@@ -0,0 +1,192 @@
package java
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"net"
"strconv"
"strings"
"time"
"github.com/VexoraDevelopment/mcstatus/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
}