first commit
This commit is contained in:
378
java/java.go
Normal file
378
java/java.go
Normal 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()
|
||||
}
|
||||
Reference in New Issue
Block a user