379 lines
7.1 KiB
Go
379 lines
7.1 KiB
Go
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()
|
|
}
|