-
Notifications
You must be signed in to change notification settings - Fork 1
/
bedrockping.go
196 lines (165 loc) · 4.79 KB
/
bedrockping.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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
// Package bedrockping is a simple library to ping Minecraft Bedrock/MCPE servers.
package bedrockping
import (
"bufio"
"bytes"
"context"
"encoding/binary"
"fmt"
"io"
"net"
"strconv"
"strings"
"time"
)
const (
// DefaultPort is the default Minecraft Bedrock/MCPE server port.
DefaultPort = 19132
)
// Response data returned from ReadUnconnectedPong.
type Response struct {
Timestamp uint64 `json:"timestamp"`
ServerID uint64 `json:"serverId"`
GameID string `json:"gameId"`
ServerName string `json:"serverName"`
ProtocolVersion int `json:"protocolVersion"`
MCPEVersion string `json:"mcpeVersion"`
PlayerCount int `json:"playerCount"`
MaxPlayers int `json:"maxPlayers"`
Extra []string `json:"extra"`
}
var offlineMessageDataID = []byte{
0x00, 0xff, 0xff, 0x00, 0xfe, 0xfe, 0xfe, 0xfe,
0xfd, 0xfd, 0xfd, 0xfd, 0x12, 0x34, 0x56, 0x78,
}
// WriteUnconnectedPingPacket writes the 'Unconnected Ping (0x01)' as a single packet to a connection.
func WriteUnconnectedPingPacket(conn net.Conn, timestamp uint64) error {
buf := new(bytes.Buffer)
if err := WriteUnconnectedPing(buf, timestamp); err != nil {
return err
}
_, err := conn.Write(buf.Bytes())
return err
}
// WriteUnconnectedPing writes the 'Unconnected Ping (0x01)' packet to a writer.
// Details on the packet structure can be found:
// https://github.com/NiclasOlofsson/MiNET/blob/5bcfbfd94cff943f31208eb8614b3ff16269fdc7/src/MiNET/MiNET/Net/MCPE%20Protocol.cs#L1003
func WriteUnconnectedPing(writer io.Writer, timestamp uint64) error {
if err := binary.Write(writer, binary.BigEndian, byte(0x01)); err != nil {
return err
}
if err := binary.Write(writer, binary.BigEndian, timestamp); err != nil {
return err
}
if _, err := writer.Write(offlineMessageDataID); err != nil {
return err
}
return nil
}
// ReadUTFString reads a UTF-8 string with a uint16 length header.
func ReadUTFString(reader io.Reader) (string, error) {
var strLen uint16
if err := binary.Read(reader, binary.BigEndian, &strLen); err != nil {
return "", err
}
strBytes := make([]byte, strLen)
if _, err := reader.Read(strBytes); err != nil {
return "", err
}
return string(strBytes), nil
}
// ReadUnconnectedPong reads the 'Unconnected Pong (0x1C)' packet from a connection into a Response struct.
// Details on the packet structure can be found:
// https://github.com/NiclasOlofsson/MiNET/blob/5bcfbfd94cff943f31208eb8614b3ff16269fdc7/src/MiNET/MiNET/Net/MCPE%20Protocol.cs#L1154
func ReadUnconnectedPong(reader *bufio.Reader, resp *Response) error {
id, err := reader.ReadByte()
if err != nil {
return err
}
if id != 0x1c {
return fmt.Errorf("unexpected packet id: %d", id)
}
if err = binary.Read(reader, binary.BigEndian, &resp.Timestamp); err != nil {
return err
}
if err = binary.Read(reader, binary.BigEndian, &resp.ServerID); err != nil {
return err
}
temp := make([]byte, 16)
if _, err = reader.Read(temp); err != nil {
return err
}
if !bytes.Equal(offlineMessageDataID, temp) {
return fmt.Errorf("invalid offline message data id: %x", temp)
}
payload, err := ReadUTFString(reader)
if err != nil {
return err
}
split := strings.Split(payload, ";")
if len(split) < 6 {
return fmt.Errorf("invalid payload: %s", payload)
}
if len(split) > 6 {
resp.Extra = split[6:]
}
resp.GameID = split[0]
resp.ServerName = split[1]
resp.ProtocolVersion, err = strconv.Atoi(split[2])
if err != nil {
return err
}
resp.MCPEVersion = split[3]
resp.PlayerCount, err = strconv.Atoi(split[4])
if err != nil {
return err
}
resp.MaxPlayers, err = strconv.Atoi(split[5])
if err != nil {
return err
}
return nil
}
// Query makes a query to the specified address via the Minecraft Bedrock protocol,
// if successful it returns a Response containing data from the pong packet.
// resend is the interval that the ping packet is sent in case there is packet loss.
func Query(address string, timeout time.Duration, resend time.Duration) (Response, error) {
var resp Response
deadline := time.Now().Add(timeout)
conn, err := net.DialTimeout("udp", address, timeout)
if err != nil {
return resp, err
}
defer conn.Close()
if err = conn.SetDeadline(deadline); err != nil {
return resp, err
}
ctx, cancel := context.WithDeadline(context.TODO(), deadline)
defer cancel()
var errs chan error
// Repeat sending ping packet in case there is packet loss
ticker := time.NewTicker(resend)
go func() {
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
if err := WriteUnconnectedPingPacket(conn, 0); err != nil {
errs <- err
return
}
}
}
}()
reader := bufio.NewReader(conn)
if err = ReadUnconnectedPong(reader, &resp); err != nil {
return resp, err
}
select {
case err := <-errs:
return resp, err
default:
return resp, nil
}
}