From 52863c2bdca3dbb1c1d71b9e4e59d8f86f26b04d Mon Sep 17 00:00:00 2001 From: Raqbit Date: Fri, 12 Mar 2021 12:29:47 +0100 Subject: [PATCH] Use mcproto for protocol bits, make public API more like net.Dial --- _example/context-ping/main.go | 81 +++++----- _example/default-ping/main.go | 7 +- _example/timed-example/main.go | 7 +- chat.go | 46 ------ encoding/types.go | 147 ------------------ encoding/types_test.go | 212 -------------------------- go.mod | 2 + go.sum | 38 +++++ packet/handshake.go | 49 ------ packet/packet.go | 93 ----------- packet/request.go | 16 -- packet/response.go | 27 ---- pinger.go | 206 +++++++++++-------------- pinger_test.go | 1 + status.go | 40 ----- status_test.go | 92 ----------- testdata/info.json | 20 --- testdata/info_description_string.json | 18 --- 18 files changed, 172 insertions(+), 930 deletions(-) delete mode 100644 chat.go delete mode 100644 encoding/types.go delete mode 100644 encoding/types_test.go create mode 100644 go.sum delete mode 100644 packet/handshake.go delete mode 100644 packet/packet.go delete mode 100644 packet/request.go delete mode 100644 packet/response.go create mode 100644 pinger_test.go delete mode 100644 status.go delete mode 100644 status_test.go delete mode 100644 testdata/info.json delete mode 100644 testdata/info_description_string.json diff --git a/_example/context-ping/main.go b/_example/context-ping/main.go index 567d92a..7a48571 100644 --- a/_example/context-ping/main.go +++ b/_example/context-ping/main.go @@ -1,42 +1,39 @@ -package main - -import ( - "context" - "fmt" - "log" - "os" - "os/signal" - "syscall" - "time" - - mcpinger "github.com/Raqbit/mc-pinger" -) - -func main() { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - sig := make(chan os.Signal) - signal.Notify(sig, os.Interrupt, syscall.SIGTERM) - go func() { - <-sig - // NOW cancel - fmt.Println("aborting due to interrupt...") - cancel() - }() - - // Create new Pinger with 10 seconds Timeout - pinger := mcpinger.NewContext(ctx, "mc.herobone.de", 25565) - // Get server info - info, err := pinger.Ping() - - if err != nil { - log.Println(err) - return - } - - // Print server info - fmt.Printf("Description: \"%s\"\n", info.Description.Text) - fmt.Printf("Online: %d/%d\n", info.Players.Online, info.Players.Max) - fmt.Printf("Version: %s\n", info.Version.Name) -} +package main + +import ( + "context" + "fmt" + mcpinger "github.com/Raqbit/mc-pinger" + "log" + "os" + "os/signal" + "syscall" +) + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + sig := make(chan os.Signal) + signal.Notify(sig, os.Interrupt, syscall.SIGTERM) + + go func() { + <-sig + // NOW cancel + fmt.Println("Aborting due to signal...") + cancel() + }() + + // Ping Minecraft server with context + info, err := mcpinger.PingContext(ctx, "mc.hypixel.net", "") + + if err != nil { + log.Println(err) + return + } + + // Print server info + fmt.Printf("Description: \"%s\"\n", info.Description.Text) + fmt.Printf("Online: %d/%d\n", info.Players.Online, info.Players.Max) + fmt.Printf("Version: %s\n", info.Version.Name) +} diff --git a/_example/default-ping/main.go b/_example/default-ping/main.go index 34ef988..33e09e2 100644 --- a/_example/default-ping/main.go +++ b/_example/default-ping/main.go @@ -8,11 +8,8 @@ import ( ) func main() { - // Create new Pinger - pinger := mcpinger.New("mc.hypixel.net", 25565) - - // Get server info - info, err := pinger.Ping() + // Ping Minecraft server + info, err := mcpinger.Ping("mc.hypixel.net", "") if err != nil { log.Println(err) diff --git a/_example/timed-example/main.go b/_example/timed-example/main.go index d53a196..35a60d3 100644 --- a/_example/timed-example/main.go +++ b/_example/timed-example/main.go @@ -9,11 +9,8 @@ import ( ) func main() { - // Create new Pinger with 10 seconds Timeout - pinger := mcpinger.NewTimed("mc.herobone.de", 25565, 10*time.Second) - - // Get server info - info, err := pinger.Ping() + // Ping Minecraft server with 10 seconds timeout + info, err := mcpinger.PingTimeout("mc.hypixel.net", "", 10*time.Second) if err != nil { log.Println(err) diff --git a/chat.go b/chat.go deleted file mode 100644 index b29ab1e..0000000 --- a/chat.go +++ /dev/null @@ -1,46 +0,0 @@ -package mcpinger - -import ( - "encoding/json" -) - -// RegularChatComponent is a Minecraft chat component -// See: https://wiki.vg/Chat#Current_system_.28JSON_Chat.29 -type RegularChatComponent struct { - Text string `json:"text"` // Text content - Bold bool `json:"bold"` // Component is emboldened - Italic bool `json:"italic"` // Component is italicized - Underlined bool `json:"underlined"` // Component is underlined - Strikethrough bool `json:"strikethrough"` // Component is struck out - Obfuscated bool `json:"obfuscated"` // Component randomly switches between characters of the same width - Color string `json:"color"` // Contains the color for the component - Extra []RegularChatComponent `json:"extra"` // RegularChatComponent siblings -} - -// ChatComponent wraps a RegularChatComponent for parsing both regular & string-only MOTD's -type ChatComponent struct { - RegularChatComponent -} - -// UnmarshalJSON unmarshals the JSON data -func (c *ChatComponent) UnmarshalJSON(data []byte) error { - var regular RegularChatComponent - - // The data starts with quotes which means it's a string, not an object - if data[0] == '"' { - var text string - if err := json.Unmarshal(data, &text); err != nil { - return err - } - - regular.Text = text - } else { - if err := json.Unmarshal(data, ®ular); err != nil { - return err - } - } - - c.RegularChatComponent = regular - - return nil -} diff --git a/encoding/types.go b/encoding/types.go deleted file mode 100644 index 9700926..0000000 --- a/encoding/types.go +++ /dev/null @@ -1,147 +0,0 @@ -package encoding - -import ( - "encoding/binary" - "errors" - "io" - "math" -) - -const ( - VarIntMaxByteSize = 5 -) - -var ( - // ErrVarIntTooLarge is returned when a read varint was too large - // (more than 5 bytes) - ErrVarIntTooLarge = errors.New("VarInt too large") -) - -// Minecraft Protocol UnsignedShort type -type UnsignedShort uint16 - -// WriteUnsignedShort writes the passed UnsignedShort to the writer -func WriteUnsignedShort(buff io.Writer, value UnsignedShort) error { - return binary.Write(buff, binary.BigEndian, uint16(value)) -} - -//ReadUnsignedShort reads an UnsignedShort from the reader -func ReadUnsignedShort(buff io.Reader) (UnsignedShort, error) { - var short uint16 - err := binary.Read(buff, binary.BigEndian, &short) - return UnsignedShort(short), err -} - -// Minecrat Protocol UnsignedByte type -type UnsignedByte byte - -func ReadUnsignedByte(r io.Reader) (UnsignedByte, error) { - var bytes [1]byte - _, err := r.Read(bytes[:1]) - return UnsignedByte(bytes[0]), err -} - -func WriteUnsignedByte(w io.Writer, value UnsignedByte) error { - var bytes [1]byte - bytes[0] = byte(value) - _, err := w.Write(bytes[:1]) - return err -} - -// Minecraft Protocol VarInt type -type VarInt int32 - -// WriteVarInt writes the passed VarInt encoded integer to the writer. -func WriteVarInt(w io.Writer, value VarInt) error { - for cont := true; cont; cont = value != 0 { - temp := byte(value & 0x7F) - - // Casting value to a uint to get a logical shift - value = VarInt(uint32(value) >> 7) - - if value != 0 { - temp |= 0x80 - } - - if err := WriteUnsignedByte(w, UnsignedByte(temp)); err != nil { - return err - } - } - - return nil -} - -// ReadVarInt reads a VarInt encoded integer from the reader. -func ReadVarInt(r io.Reader) (VarInt, error) { - var numRead uint - var result int32 - var read UnsignedByte - - for cont := true; cont; cont = (read & 0x80) != 0 { - var err error - read, err = ReadUnsignedByte(r) - - if err != nil { - return 0, err - } - - value := read & 0x7F - - result |= int32(value) << (7 * numRead) - - numRead++ - - if numRead > VarIntMaxByteSize { - return 0, ErrVarIntTooLarge - } - } - - return VarInt(result), nil -} - -type String string - -// WriteString writes a VarInt prefixed utf-8 string to the -// writer. -func WriteString(w io.Writer, str String) error { - - // Creating buffer from string - b := []byte(str) - - // Writing string length as varint to output buffer - err := WriteVarInt(w, VarInt(len(b))) - - if err != nil { - return err - } - - // Writing string to buffer - _, err = w.Write(b) - - return err -} - -// ReadString reads a VarInt prefixed utf-8 string to the -// reader. It uses io.ReadFull to ensure all bytes are read. -func ReadString(r io.Reader) (String, error) { - - // Reading string size encoded as VarInt - l, err := ReadVarInt(r) - - if err != nil { - return "", nil - } - - // Checking if string size is valid - if l < 0 || l > math.MaxInt16 { - return "", errors.New("string length out of bounds") - } - - // Creating string buffer with the specified size - stringBuff := make([]byte, int(l)) - - // Reading l amount of bytes from the buffer - _, err = io.ReadFull(r, stringBuff) - - return String(stringBuff), err -} diff --git a/encoding/types_test.go b/encoding/types_test.go deleted file mode 100644 index 4f46824..0000000 --- a/encoding/types_test.go +++ /dev/null @@ -1,212 +0,0 @@ -package encoding - -import ( - "bytes" - "io" - "math" - "testing" -) - -func TestWriteUnsignedShort(t *testing.T) { - tests := []struct { - Value UnsignedShort - Expected []byte - }{ - {Value: 0, Expected: []byte{0x00, 0x00}}, - {Value: 1, Expected: []byte{0x00, 0x01}}, - {Value: 2, Expected: []byte{0x00, 0x02}}, - {Value: 127, Expected: []byte{0x00, 0x7f}}, - {Value: 128, Expected: []byte{0x00, 0x80}}, - {Value: 255, Expected: []byte{0x00, 0xff}}, - {Value: math.MaxUint16, Expected: []byte{0xff, 0xff}}, - } - - var buff bytes.Buffer - _ = io.Writer(&buff) - - for _, test := range tests { - err := WriteUnsignedShort(&buff, test.Value) - - if err != nil { - t.Error(err) - } - - if bytes.Compare(test.Expected, buff.Bytes()) != 0 { - // Not equal - t.Errorf("Unable to convert %d: %v != %v", test.Value, buff.Bytes(), test.Expected) - } - - buff.Reset() - } -} - -func TestReadUnsignedShort(t *testing.T) { - tests := []struct { - Expected UnsignedShort - Value []byte - }{ - {Expected: 0, Value: []byte{0x00, 0x00}}, - {Expected: 1, Value: []byte{0x00, 0x01}}, - {Expected: 2, Value: []byte{0x00, 0x02}}, - {Expected: 127, Value: []byte{0x00, 0x7f}}, - {Expected: 128, Value: []byte{0x00, 0x80}}, - {Expected: 255, Value: []byte{0x00, 0xff}}, - {Expected: math.MaxUint16, Value: []byte{0xff, 0xff}}, - } - - var buff bytes.Buffer - _ = io.Writer(&buff) - - for _, test := range tests { - - buff.Write(test.Value) - - actual, err := ReadUnsignedShort(&buff) - - if err != nil { - t.Error(err) - } - - if actual != test.Expected { - // Not equal - t.Errorf("Unable to convert %v: %d != %d", test.Value, actual, test.Expected) - } - - buff.Reset() - } -} - - -func TestWriteVarInt(t *testing.T) { - tests := []struct { - Value VarInt - Expected []byte - }{ - {Value: 0, Expected: []byte{0x00}}, - {Value: 1, Expected: []byte{0x01}}, - {Value: 2, Expected: []byte{0x02}}, - {Value: 127, Expected: []byte{0x7f}}, - {Value: 128, Expected: []byte{0x80, 0x01}}, - {Value: 255, Expected: []byte{0xff, 0x01}}, - {Value: 2147483647, Expected: []byte{0xff, 0xff, 0xff, 0xff, 0x07}}, - {Value: -1, Expected: []byte{0xff, 0xff, 0xff, 0xff, 0x0f}}, - {Value: -2147483648, Expected: []byte{0x80, 0x80, 0x80, 0x80, 0x08}}, - } - - var buff bytes.Buffer - _ = io.Writer(&buff) - - for _, test := range tests { - err := WriteVarInt(&buff, test.Value) - - if err != nil { - t.Error(err) - } - - if bytes.Compare(test.Expected, buff.Bytes()) != 0 { - // Not equal - t.Errorf("Unable to convert %d: %v != %v", test.Value, buff.Bytes(), test.Expected) - } - - buff.Reset() - } -} - -func TestReadVarInt(t *testing.T) { - tests := []struct { - Expected VarInt - Value []byte - }{ - {Expected: 0, Value: []byte{0x00}}, - {Expected: 1, Value: []byte{0x01}}, - {Expected: 2, Value: []byte{0x02}}, - {Expected: 127, Value: []byte{0x7f}}, - {Expected: 128, Value: []byte{0x80, 0x01}}, - {Expected: 255, Value: []byte{0xff, 0x01}}, - {Expected: 2147483647, Value: []byte{0xff, 0xff, 0xff, 0xff, 0x07}}, - {Expected: -1, Value: []byte{0xff, 0xff, 0xff, 0xff, 0x0f}}, - {Expected: -2147483648, Value: []byte{0x80, 0x80, 0x80, 0x80, 0x08}}, - } - - var buff bytes.Buffer - _ = io.Writer(&buff) - - for _, test := range tests { - buff.Write(test.Value) - - actual, err := ReadVarInt(&buff) - - if err != nil { - t.Error(err) - } - - if actual != test.Expected { - // Not equal - t.Errorf("Unable to convert %v: %d != %d", test.Value, actual, test.Expected) - } - - buff.Reset() - } -} - -func TestWriteString(t *testing.T) { - tests := []struct { - Value string - Expected []byte - }{ - {Value: "john", Expected: []byte{0x04, 0x6a, 0x6f, 0x68, 0x6e}}, - {Value: " doe ", Expected: []byte{0x05, 0x20, 0x64, 0x6f, 0x65, 0x20}}, - {Value: "😂😂😂", Expected: []byte{0x0c, 0xf0, 0x9f, 0x98, 0x82, 0xf0, 0x9f, 0x98, 0x82, 0xf0, 0x9f, 0x98, 0x82}}, - {Value: "(╯°Д°)╯︵/(.â–¡ . \\)", Expected: []byte{0x1e, 0x28, 0xe2, 0x95, 0xaf, 0xc2, 0xb0, 0xd0, 0x94, 0xc2, 0xb0, 0xef, 0xbc, 0x89, 0xe2, 0x95, 0xaf, 0xef, 0xb8, 0xb5, 0x2f, 0x28, 0x2e, 0xe2, 0x96, 0xa1, 0x20, 0x2e, 0x20, 0x5c, 0x29}}, - } - - var buff bytes.Buffer - _ = io.Writer(&buff) - - for _, test := range tests { - err := WriteString(&buff, String(test.Value)) - - if err != nil { - t.Fatal(err) - } - - if bytes.Compare(test.Expected, buff.Bytes()) != 0 { - // Not equal - t.Errorf(`Unable to convert "%s": %v != %v`, test.Value, buff.Bytes(), test.Expected) - } - - buff.Reset() - } -} - -func TestReadString(t *testing.T) { - tests := []struct { - Expected String - Value []byte - }{ - {Expected: "john", Value: []byte{0x04, 0x6a, 0x6f, 0x68, 0x6e}}, - {Expected: " doe ", Value: []byte{0x05, 0x20, 0x64, 0x6f, 0x65, 0x20}}, - {Expected: "😂😂😂", Value: []byte{0x0c, 0xf0, 0x9f, 0x98, 0x82, 0xf0, 0x9f, 0x98, 0x82, 0xf0, 0x9f, 0x98, 0x82}}, - {Expected: "(╯°Д°)╯︵/(.â–¡ . \\)", Value: []byte{0x1e, 0x28, 0xe2, 0x95, 0xaf, 0xc2, 0xb0, 0xd0, 0x94, 0xc2, 0xb0, 0xef, 0xbc, 0x89, 0xe2, 0x95, 0xaf, 0xef, 0xb8, 0xb5, 0x2f, 0x28, 0x2e, 0xe2, 0x96, 0xa1, 0x20, 0x2e, 0x20, 0x5c, 0x29}}, - } - - var buff bytes.Buffer - _ = io.Writer(&buff) - - for _, test := range tests { - buff.Write(test.Value) - - actual, err := ReadString(&buff) - - if err != nil { - t.Error(err) - } - - if actual != test.Expected { - // Not equal - t.Errorf(`Unable to convert %v: "%s" != "%s"`, test.Value, actual, test.Expected) - } - - buff.Reset() - } -} diff --git a/go.mod b/go.mod index 5673f7f..e1e2d6d 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/Raqbit/mc-pinger go 1.12 + +require github.com/Raqbit/mcproto v0.0.0-20210320181836-253ae09a13b8 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5576853 --- /dev/null +++ b/go.sum @@ -0,0 +1,38 @@ +github.com/Raqbit/mcproto v0.0.0-20210320181836-253ae09a13b8 h1:8Nz1jZnBHqmK9h0rijTwr+2YZjLLpv0GOFUTxOcWjqQ= +github.com/Raqbit/mcproto v0.0.0-20210320181836-253ae09a13b8/go.mod h1:oWafTH2twEsmMTF07OV5cNMXrCmUD+yVDN93U+/kISA= +github.com/dave/jennifer v1.4.1/go.mod h1:7jEdnm+qBcxl8PC0zyp7vxcpSRnzXSt9r39tpTVGlwA= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/packet/handshake.go b/packet/handshake.go deleted file mode 100644 index 544ea12..0000000 --- a/packet/handshake.go +++ /dev/null @@ -1,49 +0,0 @@ -package packet - -import ( - "bytes" - enc "github.com/Raqbit/mc-pinger/encoding" - "io" -) - -type HandshakePacket struct { - ProtoVer enc.VarInt - ServerAddr enc.String - ServerPort enc.UnsignedShort - NextState enc.VarInt -} - -func (h HandshakePacket) Marshal() ([]byte, error) { - var buffer bytes.Buffer - err := h.write(&buffer) - return buffer.Bytes(), err -} - -func (HandshakePacket) ID() enc.VarInt { - return 0x00 -} - -func (h HandshakePacket) write(buffer io.Writer) error { - - // Write protocol version - if err := enc.WriteVarInt(buffer, h.ProtoVer); err != nil { - return err - } - - // Write server address - if err := enc.WriteString(buffer, h.ServerAddr); err != nil { - return err - } - - // Write server port - if err := enc.WriteUnsignedShort(buffer, h.ServerPort); err != nil { - return err - } - - // Write next connection state - if err := enc.WriteVarInt(buffer, h.NextState); err != nil { - return err - } - - return nil -} diff --git a/packet/packet.go b/packet/packet.go deleted file mode 100644 index 5c18160..0000000 --- a/packet/packet.go +++ /dev/null @@ -1,93 +0,0 @@ -package packet - -import ( - "bytes" - enc "github.com/Raqbit/mc-pinger/encoding" - "io" -) - -// Represents a Minecraft packet. -type Packet interface { - ID() enc.VarInt -} - -// Packet which is able to be encoded. -type EncodablePacket interface { - Packet - Marshal() ([]byte, error) -} - -// Packet which is able to be decoded. -type DecodablePacket interface { - Packet - Unmarshal(reader io.Reader) error -} - -// Write a packet to the given Writer. -func WritePacket(p EncodablePacket, w io.Writer) error { - // Marshal packet data - data, err := p.Marshal() - - if err != nil { - return err - } - - // Get the packet id in packed form - pId, err := getPacketIdBytes(p) - - if err != nil { - return err - } - - // Calculate packet length - length := enc.VarInt(len(pId) + len(data)) - - // Write packet length - if err = enc.WriteVarInt(w, length); err != nil { - return err - } - - // Write packet id - if _, err = w.Write(pId); err != nil { - return err - } - - // Write packet data - if _, err = w.Write(data); err != nil { - return err - } - - return nil -} - -// Get the packet ID of given packet in byte form. -func getPacketIdBytes(p Packet) ([]byte, error) { - packetId := p.ID() - - pIdBuff := new(bytes.Buffer) - - err := enc.WriteVarInt(pIdBuff, packetId) - - if err != nil { - return nil, err - } - - return pIdBuff.Bytes(), nil -} - -// Reads a packet header (length, version) from the given Reader. -func ReadPacketHeader(r io.Reader) (enc.VarInt, enc.VarInt, error) { - pLen, err := enc.ReadVarInt(r) - - if err != nil { - return 0, 0, err - } - - pId, err := enc.ReadVarInt(r) - - if err != nil { - return 0, 0, err - } - - return pLen, pId, nil -} diff --git a/packet/request.go b/packet/request.go deleted file mode 100644 index 955c49c..0000000 --- a/packet/request.go +++ /dev/null @@ -1,16 +0,0 @@ -package packet - -import ( - enc "github.com/Raqbit/mc-pinger/encoding" -) - -type RequestPacket struct{} - -func (RequestPacket) ID() enc.VarInt { - return 0x00 -} - -func (h RequestPacket) Marshal() ([]byte, error) { - // Packet does not have any content. - return make([]byte, 0), nil -} diff --git a/packet/response.go b/packet/response.go deleted file mode 100644 index e780798..0000000 --- a/packet/response.go +++ /dev/null @@ -1,27 +0,0 @@ -package packet - -import ( - enc "github.com/Raqbit/mc-pinger/encoding" - "io" -) - -type ResponsePacket struct { - Json enc.String -} - -func (ResponsePacket) ID() enc.VarInt { - return 0x00 -} - -func (rp *ResponsePacket) Unmarshal(reader io.Reader) error { - // Read JSON string - str, err := enc.ReadString(reader) - - if err != nil { - return err - } - - rp.Json = str - - return nil -} diff --git a/pinger.go b/pinger.go index dedc291..65408b8 100644 --- a/pinger.go +++ b/pinger.go @@ -1,184 +1,154 @@ package mcpinger import ( - "bufio" "context" - "errors" "fmt" + "github.com/Raqbit/mcproto/encoding" + "github.com/Raqbit/mcproto/packet" + "github.com/Raqbit/mcproto/types" "net" "strconv" "time" - enc "github.com/Raqbit/mc-pinger/encoding" - "github.com/Raqbit/mc-pinger/packet" + "github.com/Raqbit/mcproto" ) -const ( - UnknownProtoVersion = -1 - StatusState = 1 -) - -// Pinger allows you to retrieve server info. -type Pinger interface { - Ping() (*ServerInfo, error) +// A Pinger contains options for pinging a Minecraft server. +// +// The zero value for each field is equivalent to pinging +// without that option. Pinging with the zero value of Pinger +// is therefore equivalent to just calling the Ping function. +// +// It is safe to call Pinger's methods concurrently. +type Pinger struct { } -type mcPinger struct { - Host string - Port uint16 - Context context.Context - Timeout time.Duration +// Ping sends a server query to the specified address. +func (p Pinger) Ping(host, port string) (*packet.ServerInfo, error) { + return p.PingContext(context.Background(), host, port) } -// InvalidPacketError returned when the received packet type -// does not match the expected packet type. -type InvalidPacketError struct { - expected enc.VarInt - actual enc.VarInt -} +// PingTimeout acts like Ping but takes a timeout. +// +// If the timeout expires before the server info has been retrieved, an error is returned. +func (p Pinger) PingTimeout(host, port string, timeout time.Duration) (*packet.ServerInfo, error) { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() -func (i InvalidPacketError) Error() string { - return fmt.Sprintf("Received invalid packet. Expected #%d, got #%d", i.expected, i.actual) + return p.PingContext(ctx, host, port) } -func (p *mcPinger) Ping() (*ServerInfo, error) { - if p.Timeout > 0 && p.Context == nil { - ctx, cancel := context.WithTimeout(context.Background(), p.Timeout) - p.Context = ctx - defer cancel() +// PingContext acts like Ping but takes a context. +// +// The provided Context must be non-nil. If the context expires before +// the server info has been retrieved, an error is returned. +func (p Pinger) PingContext(ctx context.Context, host, port string) (*packet.ServerInfo, error) { + if ctx == nil { + panic("nil context") } - return p.ping() -} - -// Will connect to the Minecraft server, -// retrieve server status and return the server info. -func (p *mcPinger) ping() (*ServerInfo, error) { - if p.Context == nil { - panic("Context is nil!") - } - - address := net.JoinHostPort(p.Host, strconv.Itoa(int(p.Port))) - - var d net.Dialer - - conn, err := d.DialContext(p.Context, "tcp", address) + // Connect to Minecraft server + conn, address, err := mcproto.DialContext(ctx, host, port) if err != nil { - return nil, errors.New("could not connect to Minecraft server: " + err.Error()) + return nil, fmt.Errorf("could not connect to Minecraft server: %w", err) } - rd := bufio.NewReader(conn) - w := bufio.NewWriter(conn) - defer conn.Close() - err = p.sendHandshakePacket(w) - - if err != nil { + // Send handshake + if err = sendHandshakePacket(ctx, conn, address); err != nil { return nil, err } - err = p.sendRequestPacket(w) + // Switch to the state we requested + conn.SetState(types.ConnectionStateStatus) - if err != nil { + // Send server query + if err = sendServerQueryPacket(ctx, conn); err != nil { return nil, err } - err = w.Flush() + // Read server info response + res, err := readServerInfoPacket(ctx, conn) if err != nil { return nil, err } - res, err := p.readPacket(rd) - - if err != nil { - return nil, err - } + return &res.Response, err +} - info, err := parseServerInfo([]byte(res.Json)) +// Ping sends a server query to the specified address. +func Ping(host, port string) (*packet.ServerInfo, error) { + var p Pinger + return p.Ping(host, port) +} - return info, err +// PingContext acts like Ping but takes a context. +// +// The provided Context must be non-nil. If the context expires before +// the server info has been retrieved, an error is returned. +func PingContext(ctx context.Context, host, port string) (*packet.ServerInfo, error) { + var p Pinger + return p.PingContext(ctx, host, port) } -func (p *mcPinger) sendHandshakePacket(w *bufio.Writer) error { - handshakePkt := &packet.HandshakePacket{ - ProtoVer: UnknownProtoVersion, - ServerAddr: enc.String(p.Host), - ServerPort: enc.UnsignedShort(p.Port), - NextState: StatusState, - } +// PingTimeout acts like Ping but takes a timeout. +// +// If the timeout expires before the server info has been retrieved, an error is returned. +func PingTimeout(host, port string, timeout time.Duration) (*packet.ServerInfo, error) { + var p Pinger + return p.PingTimeout(host, port, timeout) +} - err := packet.WritePacket(handshakePkt, w) +func sendHandshakePacket(ctx context.Context, conn mcproto.Connection, address string) error { + host, port, err := net.SplitHostPort(address) if err != nil { - return errors.New("could not pack: " + err.Error()) + return fmt.Errorf("could not split host & port: %w", err) } - return nil -} + serverPort, err := strconv.Atoi(port) -func (p *mcPinger) sendRequestPacket(w *bufio.Writer) error { - requestPkt := &packet.RequestPacket{} + if err != nil { + return fmt.Errorf("could not parse port: %w", err) + } - err := packet.WritePacket(requestPkt, w) + handshakePkt := &packet.HandshakePacket{ + ProtoVer: -1, + ServerAddr: encoding.String(host), + ServerPort: encoding.UnsignedShort(uint16(serverPort)), + NextState: types.ConnectionStateStatus, + } - if err != nil { - return errors.New("could not pack: " + err.Error()) + if err = conn.WritePacket(ctx, handshakePkt); err != nil { + return fmt.Errorf("could not send handshake packet: %w", err) } return nil } -func (p *mcPinger) readPacket(rd *bufio.Reader) (*packet.ResponsePacket, error) { - - rp := &packet.ResponsePacket{} - - _, packetID, err := packet.ReadPacketHeader(rd) - - if packetID != rp.ID() { - return nil, InvalidPacketError{expected: rp.ID(), actual: packetID} +func sendServerQueryPacket(ctx context.Context, w mcproto.Connection) error { + if err := w.WritePacket(ctx, &packet.ServerQueryPacket{}); err != nil { + return fmt.Errorf("could not send server query request packet: %w", err) } - if err != nil { - return nil, err - } + return nil +} - err = rp.Unmarshal(rd) +func readServerInfoPacket(ctx context.Context, conn mcproto.Connection) (*packet.ServerInfoPacket, error) { + pkt, err := conn.ReadPacket(ctx) if err != nil { - return nil, err + return nil, fmt.Errorf("could not read server info packet: %w", err) } - return rp, nil -} + resp, ok := pkt.(*packet.ServerInfoPacket) -// New Creates a new Pinger with specified host & port -// to connect to a minecraft server -func New(host string, port uint16) Pinger { - return &mcPinger{ - Host: host, - Port: port, - Context: context.Background(), + if !ok { + return nil, fmt.Errorf("returned packet was not expected server info") } -} -// NewTimed Creates a new Pinger with specified host & port -// to connect to a minecraft server with Timeout -func NewTimed(host string, port uint16, timeout time.Duration) Pinger { - return &mcPinger{ - Host: host, - Port: port, - Timeout: timeout, - } -} - -// NewContext Creates a new Pinger with the given Context -func NewContext(ctx context.Context, host string, port uint16) Pinger { - return &mcPinger{ - Host: host, - Port: port, - Context: ctx, - } + return resp, nil } diff --git a/pinger_test.go b/pinger_test.go new file mode 100644 index 0000000..53efbde --- /dev/null +++ b/pinger_test.go @@ -0,0 +1 @@ +package mcpinger diff --git a/status.go b/status.go deleted file mode 100644 index e3c03e2..0000000 --- a/status.go +++ /dev/null @@ -1,40 +0,0 @@ -package mcpinger - -import ( - "encoding/json" -) - -// Server info version -type Version struct { - Name string `json:"name"` // Version name - Protocol int32 `json:"protocol"` // Version protocol number -} - -// Server info player -type Player struct { - Name string `json:"name"` // Player name - ID string `json:"id"` // Player UUID -} - -// Server info players -type Players struct { - Max int32 `json:"max"` // Max amount of players allowed - Online int32 `json:"online"` // Amount of players online - Sample []Player // Sample of online players -} - -// Server ping response -// https://wiki.vg/Server_List_Ping#Response -type ServerInfo struct { - Version Version `json:"version"` // Server version info - Players Players `json:"players"` // Server player info - Description ChatComponent `json:"description"` // Server description - Favicon string `json:"favicon"` // Server favicon -} - -// Parses the provided json byte array into a ServerInfo struct -func parseServerInfo(infoJson []byte) (*ServerInfo, error) { - info := new(ServerInfo) - err := json.Unmarshal(infoJson, info) - return info, err -} diff --git a/status_test.go b/status_test.go deleted file mode 100644 index 243229b..0000000 --- a/status_test.go +++ /dev/null @@ -1,92 +0,0 @@ -package mcpinger - -import ( - "io/ioutil" - "path/filepath" - "testing" -) - -const ( - TestDataDir = "testdata" // Name of testdata directory - VersionName = "1.13.2" - Protocol = 404 - Description = "Hello world" - MaxPlayers = 100 - OnlinePlayers = 5 - PlayerSampleCount = 1 - PlayerSampleName = "Raqbit" - PlayerSampleUuid = "09bc745b-3679-4152-b96b-3f9c59c42059" - Favicon = "data:image/png;base64," -) - -func TestParseServerInfo(t *testing.T) { - files := []string{ - "info.json", - "info_description_string.json", - } - - for _, f := range files { - infoJson := GetTestFileContents(t, f) - - info, err := parseServerInfo(infoJson) - - if err != nil { - t.Fatal(err) - } - - if info.Version.Name != VersionName { - parseError(t, f, "version name") - } - - if info.Version.Protocol != Protocol { - parseError(t, f, "protocol version") - } - - if info.Description.Text != Description { - parseError(t, f, "description") - - } - - if info.Players.Max != MaxPlayers { - parseError(t, f, "max players") - - } - - if info.Players.Online != OnlinePlayers { - parseError(t, f, "online players") - } - - if len(info.Players.Sample) != PlayerSampleCount { - parseError(t, f, "player sample") - } else { - if info.Players.Sample[0].Name != PlayerSampleName { - parseError(t, f, "player sample name") - } - - if info.Players.Sample[0].ID != PlayerSampleUuid { - parseError(t, f, "player sample uuid") - } - - } - - if info.Favicon != Favicon { - parseError(t, f, "favicon") - } - } -} - -func parseError(t *testing.T, file string, name string) { - t.Errorf("%s: Did not parse %s correctly", file, name) -} - -func GetTestFileContents(t *testing.T, name string) []byte { - path := filepath.Join(TestDataDir, name) - - data, err := ioutil.ReadFile(path) - - if err != nil { - t.Fatal(err) - } - - return data -} diff --git a/testdata/info.json b/testdata/info.json deleted file mode 100644 index fc6ff8d..0000000 --- a/testdata/info.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "version": { - "name": "1.13.2", - "protocol": 404 - }, - "players": { - "max": 100, - "online": 5, - "sample": [ - { - "name": "Raqbit", - "id": "09bc745b-3679-4152-b96b-3f9c59c42059" - } - ] - }, - "description": { - "text": "Hello world" - }, - "favicon": "data:image/png;base64," -} diff --git a/testdata/info_description_string.json b/testdata/info_description_string.json deleted file mode 100644 index af0ef5d..0000000 --- a/testdata/info_description_string.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "version": { - "name": "1.13.2", - "protocol": 404 - }, - "players": { - "max": 100, - "online": 5, - "sample": [ - { - "name": "Raqbit", - "id": "09bc745b-3679-4152-b96b-3f9c59c42059" - } - ] - }, - "description": "Hello world", - "favicon": "data:image/png;base64," -}