Add connection webhook notifications (#392)
Also * Added decode of LoginStart message * Add metrics backend constants * Updated usage section * Documented MaxFrameLength
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
package mcproto
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const invalidPacketDataBytesMsg = "data should be byte slice from Packet.Data"
|
||||
|
||||
// DecodeHandshake takes the Packet.Data bytes and decodes a Handshake message from it
|
||||
func DecodeHandshake(data interface{}) (*Handshake, error) {
|
||||
|
||||
dataBytes, ok := data.([]byte)
|
||||
if !ok {
|
||||
return nil, errors.New(invalidPacketDataBytesMsg)
|
||||
}
|
||||
|
||||
handshake := &Handshake{}
|
||||
buffer := bytes.NewBuffer(dataBytes)
|
||||
var err error
|
||||
|
||||
handshake.ProtocolVersion, err = ReadVarInt(buffer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
handshake.ServerAddress, err = ReadString(buffer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
handshake.ServerPort, err = ReadUnsignedShort(buffer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nextState, err := ReadVarInt(buffer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
handshake.NextState = State(nextState)
|
||||
return handshake, nil
|
||||
}
|
||||
|
||||
// DecodeLoginStart takes the Packet.Data bytes and decodes a LoginStart message from it
|
||||
func DecodeLoginStart(data interface{}) (*LoginStart, error) {
|
||||
dataBytes, ok := data.([]byte)
|
||||
if !ok {
|
||||
return nil, errors.New(invalidPacketDataBytesMsg)
|
||||
}
|
||||
|
||||
loginStart := &LoginStart{}
|
||||
buffer := bytes.NewBuffer(dataBytes)
|
||||
var err error
|
||||
|
||||
loginStart.Name, err = ReadString(buffer)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to read username")
|
||||
}
|
||||
|
||||
loginStart.PlayerUuid, err = ReadUuid(buffer)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to read player uuid")
|
||||
}
|
||||
|
||||
return loginStart, nil
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
10008206096c6f63616c686f737463dd02
|
||||
16000469747a675cddfd26fc864981b52ec42bb10bfdef
|
||||
@@ -0,0 +1,2 @@
|
||||
10008206096c6f63616c686f737463dd01
|
||||
0100
|
||||
+18
-38
@@ -4,6 +4,7 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"github.com/google/uuid"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
@@ -15,22 +16,27 @@ import (
|
||||
"golang.org/x/text/transform"
|
||||
)
|
||||
|
||||
func ReadPacket(reader io.Reader, addr net.Addr, state State) (*Packet, error) {
|
||||
// MaxFrameLength is declared at https://minecraft.wiki/w/Java_Edition_protocol#Packet_format
|
||||
// to be 2^21 - 1
|
||||
const MaxFrameLength = 2097151
|
||||
|
||||
// ReadPacket reads a packet from the given reader based on the provided connection state.
|
||||
// Returns a pointer to the Packet and an error if reading fails.
|
||||
// Handles legacy server list ping packet when in the handshaking state.
|
||||
// The provided addr is used for logging purposes.
|
||||
func ReadPacket(reader *bufio.Reader, addr net.Addr, state State) (*Packet, error) {
|
||||
logrus.
|
||||
WithField("client", addr).
|
||||
Debug("Reading packet")
|
||||
|
||||
if state == StateHandshaking {
|
||||
bufReader := bufio.NewReader(reader)
|
||||
data, err := bufReader.Peek(1)
|
||||
data, err := reader.Peek(1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if data[0] == PacketIdLegacyServerListPing {
|
||||
return ReadLegacyServerListPing(bufReader, addr)
|
||||
} else {
|
||||
reader = bufReader
|
||||
return ReadLegacyServerListPing(reader, addr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,8 +167,7 @@ func ReadFrame(reader io.Reader, addr net.Addr) (*Frame, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Limit frame length to 2^21 - 1
|
||||
if frame.Length > 2097151 {
|
||||
if frame.Length > MaxFrameLength {
|
||||
return nil, errors.Errorf("frame length %d too large", frame.Length)
|
||||
}
|
||||
|
||||
@@ -280,36 +285,11 @@ func ReadUnsignedInt(reader io.Reader) (uint32, error) {
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func ReadHandshake(data interface{}) (*Handshake, error) {
|
||||
|
||||
dataBytes, ok := data.([]byte)
|
||||
if !ok {
|
||||
return nil, errors.New("data is not expected byte slice")
|
||||
}
|
||||
|
||||
handshake := &Handshake{}
|
||||
buffer := bytes.NewBuffer(dataBytes)
|
||||
var err error
|
||||
|
||||
handshake.ProtocolVersion, err = ReadVarInt(buffer)
|
||||
func ReadUuid(reader io.Reader) (uuid.UUID, error) {
|
||||
uuidBytes := make([]byte, 16)
|
||||
_, err := io.ReadFull(reader, uuidBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return uuid.UUID{}, err
|
||||
}
|
||||
|
||||
handshake.ServerAddress, err = ReadString(buffer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
handshake.ServerPort, err = ReadUnsignedShort(buffer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nextState, err := ReadVarInt(buffer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
handshake.NextState = nextState
|
||||
return handshake, nil
|
||||
return uuid.FromBytes(uuidBytes)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
package mcproto
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -35,3 +42,69 @@ func TestReadVarInt(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandshakeThenStatus(t *testing.T) {
|
||||
content, err := ReadHexDumpFile("handshake-status.hex")
|
||||
require.NoError(t, err)
|
||||
|
||||
reader := bufio.NewReader(bytes.NewReader(content))
|
||||
|
||||
handshakePacket, err := ReadPacket(reader, nil, StateHandshaking)
|
||||
require.NoError(t, err)
|
||||
|
||||
handshake, err := DecodeHandshake(handshakePacket.Data)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "localhost", handshake.ServerAddress)
|
||||
assert.Equal(t, uint16(25565), handshake.ServerPort)
|
||||
assert.Equal(t, 770 /*for 1.21.5*/, handshake.ProtocolVersion)
|
||||
assert.Equal(t, StateStatus, handshake.NextState)
|
||||
}
|
||||
|
||||
func TestHandshakeThenLoginStart(t *testing.T) {
|
||||
content, err := ReadHexDumpFile("handshake-login-start.hex")
|
||||
require.NoError(t, err)
|
||||
|
||||
reader := bufio.NewReader(bytes.NewReader(content))
|
||||
|
||||
handshakePacket, err := ReadPacket(reader, nil, StateHandshaking)
|
||||
require.NoError(t, err)
|
||||
|
||||
handshake, err := DecodeHandshake(handshakePacket.Data)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "localhost", handshake.ServerAddress)
|
||||
assert.Equal(t, uint16(25565), handshake.ServerPort)
|
||||
assert.Equal(t, 770 /*for 1.21.5*/, handshake.ProtocolVersion)
|
||||
assert.Equal(t, StateLogin, handshake.NextState)
|
||||
|
||||
loginStartPacket, err := ReadPacket(reader, nil, StateLogin)
|
||||
require.NoError(t, err)
|
||||
|
||||
loginStart, err := DecodeLoginStart(loginStartPacket.Data)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "itzg", loginStart.Name)
|
||||
assert.Equal(t, uuid.MustParse("5cddfd26-fc86-4981-b52e-c42bb10bfdef"), loginStart.PlayerUuid)
|
||||
}
|
||||
|
||||
func ReadHexDumpFile(filename string) ([]byte, error) {
|
||||
// Read the file content
|
||||
content, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read file: %w", err)
|
||||
}
|
||||
|
||||
// Convert content to string and clean it up
|
||||
hexString := string(content)
|
||||
|
||||
// Remove whitespace and newlines
|
||||
hexString = strings.Map(func(r rune) rune {
|
||||
if unicode.IsSpace(r) {
|
||||
return -1 // Remove spaces, tabs, newlines
|
||||
}
|
||||
return r
|
||||
}, hexString)
|
||||
|
||||
return hex.DecodeString(hexString)
|
||||
}
|
||||
|
||||
+18
-7
@@ -1,6 +1,9 @@
|
||||
package mcproto
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Frame struct {
|
||||
Length int
|
||||
@@ -9,8 +12,14 @@ type Frame struct {
|
||||
|
||||
type State int
|
||||
|
||||
/*
|
||||
Handshaking -> Status
|
||||
Handshaking -> Login -> ...
|
||||
*/
|
||||
const (
|
||||
StateHandshaking = iota
|
||||
StateHandshaking State = 0
|
||||
StateStatus State = 1
|
||||
StateLogin State = 2
|
||||
)
|
||||
|
||||
var trimLimit = 64
|
||||
@@ -31,7 +40,7 @@ func (f *Frame) String() string {
|
||||
type Packet struct {
|
||||
Length int
|
||||
PacketID int
|
||||
// Data is either a byte slice of raw content or a parsed message
|
||||
// Data is either a byte slice of raw content or a decoded message
|
||||
Data interface{}
|
||||
}
|
||||
|
||||
@@ -46,6 +55,7 @@ func (p *Packet) String() string {
|
||||
|
||||
const (
|
||||
PacketIdHandshake = 0x00
|
||||
PacketIdLogin = 0x00 // during StateLogin
|
||||
PacketIdLegacyServerListPing = 0xFE
|
||||
)
|
||||
|
||||
@@ -53,12 +63,13 @@ type Handshake struct {
|
||||
ProtocolVersion int
|
||||
ServerAddress string
|
||||
ServerPort uint16
|
||||
NextState int
|
||||
NextState State
|
||||
}
|
||||
|
||||
const (
|
||||
StateStatus State = 1
|
||||
)
|
||||
type LoginStart struct {
|
||||
Name string
|
||||
PlayerUuid uuid.UUID
|
||||
}
|
||||
|
||||
type LegacyServerListPing struct {
|
||||
ProtocolVersion int
|
||||
|
||||
Reference in New Issue
Block a user