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() }