-
-
Notifications
You must be signed in to change notification settings - Fork 4
/
ping_beta18.go
156 lines (131 loc) · 4.63 KB
/
ping_beta18.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
package minequery
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"strconv"
"strings"
)
var pingBeta18PingPacket = []byte{0xfe}
const (
pingBeta18ResponsePacketID byte = 0xff
pingBeta18ResponseFieldSeparator = "§"
)
// StatusBeta18 holds status response returned by Beta 1.8 to Release 1.4 (exclusively) Minecraft servers.
type StatusBeta18 struct {
MOTD string
OnlinePlayers int
MaxPlayers int
}
// String returns a user-friendly representation of a server status response.
// It contains presumed (Beta 1.8+) Minecraft Server version, online count and naturalized MOTD.
func (s *StatusBeta18) String() string {
return fmt.Sprintf("Minecraft Server (Beta 1.8+), %d/%d players online, MOTD: %s",
s.OnlinePlayers, s.MaxPlayers, naturalizeMOTD(s.MOTD))
}
// PingBeta18 pings Beta 1.8 to Release 1.4 (exclusively) Minecraft servers (Notchian servers of more late versions
// also respond to this ping packet.)
//
//goland:noinspection GoUnusedExportedFunction
func PingBeta18(host string, port int) (*StatusBeta18, error) {
return defaultPinger.PingBeta18(host, port)
}
// PingBeta18 pings Beta 1.8 to Release 1.4 (exclusively) Minecraft servers (Notchian servers of more late versions
// also respond to this ping packet.)
func (p *Pinger) PingBeta18(host string, port int) (*StatusBeta18, error) {
status, err := p.pingGeneric(p.pingBeta18, host, port)
if err != nil {
return nil, err
}
return status.(*StatusBeta18), nil
}
func (p *Pinger) pingBeta18(host string, port int) (interface{}, error) {
conn, err := p.openTCPConn(host, port)
if err != nil {
return nil, err
}
defer func() { _ = conn.Close() }()
// Send ping packet
if err = p.pingBeta18WritePingPacket(conn); err != nil {
return nil, fmt.Errorf("could not write ping packet: %w", err)
}
// Read status response (note: uses the same packet reading approach as 1.4)
payload, err := p.pingBeta18ReadResponsePacket(conn)
if err != nil {
return nil, fmt.Errorf("could not read response packet: %w", err)
}
// Parse response data from status packet
res, err := p.pingBeta18ParseResponsePayload(payload)
if err != nil {
return nil, fmt.Errorf("could not parse status from response packet: %w", err)
}
return res, nil
}
// Communication
func (p *Pinger) pingBeta18WritePingPacket(writer io.Writer) error {
// Write single-byte FE ping packet
_, err := writer.Write(pingBeta18PingPacket)
return err
}
func (p *Pinger) pingBeta18ReadResponsePacket(reader io.Reader) ([]byte, error) {
// Read first three bytes (packet ID as byte + packet length as short)
// and create a reader over this buffer for sequential reading.
b := make([]byte, 3)
bn, err := reader.Read(b)
if err != nil {
return nil, err
} else if bn < 3 {
return nil, io.EOF
}
br := bytes.NewReader(b)
// Read packet type, return error if it isn't FF kick packet
id, err := br.ReadByte()
if err != nil {
return nil, err
} else if id != pingBeta18ResponsePacketID {
return nil, fmt.Errorf("expected packet ID %#x, but instead got %#x", ping16ResponsePacketID, id)
}
// Read packet length, return error if it isn't readable as unsigned short
// Worth noting that this needs to be multiplied by two further on (for encoding reasons, most probably)
var length uint16
if err = binary.Read(br, binary.BigEndian, &length); err != nil {
return nil, err
}
// Read remainder of the status packet as raw bytes
// This is a UTF-16BE string separated by § (paragraph sign)
payload := bytes.NewBuffer(make([]byte, 0, length*2))
if _, err = io.CopyN(payload, reader, int64(length*2)); err != nil {
return nil, err
}
// Decode UTF-16BE string
decoded, err := utf16BEDecoder.Bytes(payload.Bytes())
if err != nil {
return nil, err
}
return decoded, nil
}
// Response processing
func (p *Pinger) pingBeta18ParseResponsePayload(payload []byte) (*StatusBeta18, error) {
// Split status string, parse and map to struct returning errors if conversions fail
fields := strings.Split(string(payload), pingBeta18ResponseFieldSeparator)
if len(fields) != 3 {
return nil, fmt.Errorf("%w: expected 3 status fields, got %d", ErrInvalidStatus, len(fields))
}
motd, onlineString, maxString := fields[0], fields[1], fields[2]
// Parse online players
online, err := strconv.ParseInt(onlineString, 10, 32)
if err != nil {
return nil, fmt.Errorf("%w: could not parse online players count: %s", ErrInvalidStatus, err)
}
// Parse max players
max, err := strconv.ParseInt(maxString, 10, 32)
if err != nil {
return nil, fmt.Errorf("%w: could not parse max players count: %s", ErrInvalidStatus, err)
}
return &StatusBeta18{
MOTD: motd,
OnlinePlayers: int(online),
MaxPlayers: int(max),
}, nil
}