// Package protocol implements the small slice of the Minecraft Java protocol // Spinner needs: the handshake, the status exchange and the login disconnect. // Everything after the handshake is passed through untouched. package protocol import ( "bytes" "encoding/json" "errors" "fmt" "io" "net" "strconv" "time" ) const ( maxHandshake = 1024 maxStatus = 1 << 20 ) // ErrLegacy is returned for pre-1.7 server list pings, which Spinner does not route. var ErrLegacy = errors.New("legacy ping") type Handshake struct { Protocol int32 Host string Port uint16 NextState int32 // 1 status, 2 login, 3 transfer } func (h Handshake) IsStatus() bool { return h.NextState == 1 } func readVarInt(r io.Reader, raw *[]byte) (int32, error) { var v uint32 var b [1]byte for i := 0; i < 5; i++ { if _, err := io.ReadFull(r, b[:]); err != nil { return 0, err } if raw != nil { *raw = append(*raw, b[0]) } v |= uint32(b[0]&0x7f) << (7 * i) if b[0]&0x80 == 0 { return int32(v), nil } } return 0, errors.New("varint too long") } func AppendVarInt(buf []byte, v int32) []byte { u := uint32(v) for u >= 0x80 { buf = append(buf, byte(u)|0x80) u >>= 7 } return append(buf, byte(u)) } func appendString(buf []byte, s string) []byte { buf = AppendVarInt(buf, int32(len(s))) return append(buf, s...) } // Frame prefixes a packet body (id + payload) with its length. func Frame(body []byte) []byte { return append(AppendVarInt(make([]byte, 0, len(body)+5), int32(len(body))), body...) } // ReadHandshake reads one handshake packet and returns it along with the exact // bytes consumed, so they can be replayed to the backend unchanged. func ReadHandshake(r io.Reader) (Handshake, []byte, error) { var raw []byte n, err := readVarInt(r, &raw) if err != nil { return Handshake{}, raw, err } if len(raw) > 0 && raw[0] == 0xFE { return Handshake{}, raw, ErrLegacy } if n <= 0 || n > maxHandshake { return Handshake{}, raw, fmt.Errorf("invalid handshake length %d", n) } body := make([]byte, n) if _, err := io.ReadFull(r, body); err != nil { return Handshake{}, raw, err } raw = append(raw, body...) br := bytes.NewReader(body) id, err := readVarInt(br, nil) if err != nil || id != 0 { return Handshake{}, raw, errors.New("expected handshake packet") } var h Handshake if h.Protocol, err = readVarInt(br, nil); err != nil { return Handshake{}, raw, err } hl, err := readVarInt(br, nil) if err != nil || hl < 0 || int(hl) > br.Len()-3 { return Handshake{}, raw, errors.New("invalid host length") } host := make([]byte, hl) io.ReadFull(br, host) h.Host = string(host) var port [2]byte io.ReadFull(br, port[:]) h.Port = uint16(port[0])<<8 | uint16(port[1]) if h.NextState, err = readVarInt(br, nil); err != nil { return Handshake{}, raw, err } return h, raw, nil } func BuildHandshake(protocol int32, host string, port uint16, next int32) []byte { body := AppendVarInt(nil, 0) body = AppendVarInt(body, protocol) body = appendString(body, host) body = append(body, byte(port>>8), byte(port)) body = AppendVarInt(body, next) return Frame(body) } // ReadPacket reads one length-prefixed packet and returns its id and payload. func ReadPacket(r io.Reader, max int32) (int32, []byte, error) { n, err := readVarInt(r, nil) if err != nil { return 0, nil, err } if n <= 0 || n > max { return 0, nil, fmt.Errorf("invalid packet length %d", n) } body := make([]byte, n) if _, err := io.ReadFull(r, body); err != nil { return 0, nil, err } br := bytes.NewReader(body) id, err := readVarInt(br, nil) if err != nil { return 0, nil, err } return id, body[len(body)-br.Len():], nil } func stringPacket(id int32, s string) []byte { return Frame(appendString(AppendVarInt(nil, id), s)) } // StatusJSON builds a server list response. func StatusJSON(clientProtocol int32, version, motd string, online, max int) string { b, _ := json.Marshal(map[string]any{ "version": map[string]any{"name": version, "protocol": clientProtocol}, "players": map[string]any{"max": max, "online": online}, "description": map[string]any{"text": motd}, }) return string(b) } // LoginDisconnect is the packet a client shows as "Disconnected: ". func LoginDisconnect(message string) []byte { b, _ := json.Marshal(map[string]string{"text": message}) return stringPacket(0, string(b)) } // ServeStatus plays the server side of the status exchange with a canned // response: request, response, then an echoed ping if the client sends one. func ServeStatus(rw io.ReadWriter, statusJSON string) error { if id, _, err := ReadPacket(rw, 16); err != nil || id != 0 { return errors.New("bad status request") } if _, err := rw.Write(stringPacket(0, statusJSON)); err != nil { return err } id, payload, err := ReadPacket(rw, 32) if err != nil || id != 1 { return nil // client closed after the response } _, err = rw.Write(Frame(append(AppendVarInt(nil, 1), payload...))) return err } // QueryStatus performs a status ping against a backend and returns the raw JSON. func QueryStatus(conn net.Conn, host string, port uint16, timeout time.Duration) (string, error) { conn.SetDeadline(time.Now().Add(timeout)) if _, err := conn.Write(BuildHandshake(767, host, port, 1)); err != nil { return "", err } if _, err := conn.Write(Frame([]byte{0})); err != nil { return "", err } id, payload, err := ReadPacket(conn, maxStatus) if err != nil { return "", err } if id != 0 { return "", fmt.Errorf("unexpected packet id %d", id) } br := bytes.NewReader(payload) n, err := readVarInt(br, nil) if err != nil || n < 0 || int(n) > br.Len() { return "", errors.New("bad status string") } return string(payload[len(payload)-br.Len() : len(payload)-br.Len()+int(n)]), nil } // SplitHostPort parses a backend "host:port" address. func SplitHostPort(addr string) (string, uint16) { h, p, err := net.SplitHostPort(addr) if err != nil { return addr, 25565 } n, _ := strconv.Atoi(p) return h, uint16(n) }