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:
Geoff Bourne
2025-04-21 20:28:34 -05:00
committed by GitHub
parent a058d6e21d
commit cc590524c4
13 changed files with 585 additions and 66 deletions
+67
View File
@@ -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
}
+2
View File
@@ -0,0 +1,2 @@
10008206096c6f63616c686f737463dd02
16000469747a675cddfd26fc864981b52ec42bb10bfdef
+2
View File
@@ -0,0 +1,2 @@
10008206096c6f63616c686f737463dd01
0100
+18 -38
View File
@@ -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)
}
+73
View File
@@ -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
View File
@@ -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