368 lines
11 KiB
Go
368 lines
11 KiB
Go
package proto
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"reflect"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/admin-else/strom/mc/data"
|
|
"github.com/admin-else/strom/mc/event"
|
|
"github.com/admin-else/strom/mc/proto_base"
|
|
"github.com/admin-else/strom/mc/proto_generated"
|
|
)
|
|
|
|
const (
|
|
BufferNPackets = 100
|
|
)
|
|
|
|
var (
|
|
BadPacketTypeErr = errors.New("bad packet type")
|
|
BadPacketIdErr = errors.New("bad packet id")
|
|
PacketNotFullyDecodedErr = errors.New("packet not fully decoded")
|
|
WrongStateErr = errors.New("wrong state")
|
|
WrongDirectionErr = errors.New("wrong direction")
|
|
CantTranslateErr = errors.New("cant translate packet")
|
|
PacketDoesntExistInFutureVersionErr = errors.New("packet doesnt exist in future version")
|
|
NoHandlerRegisteredErr = errors.New("no handler registered")
|
|
)
|
|
|
|
// LoginTick is used as a subsite for the old event system because it is very useful in login contexts
|
|
type LoginTick struct{}
|
|
|
|
var LatestVersion = proto_generated.SupportedVersions[len(proto_generated.SupportedVersions)-1]
|
|
var EarliestVersion = proto_generated.SupportedVersions[0]
|
|
|
|
// UnCodablePacket represents a packet that could not be decoded.
|
|
type UnCodablePacket struct {
|
|
Err error
|
|
Data []byte
|
|
Info proto_base.PacketInfo
|
|
}
|
|
|
|
func (u *UnCodablePacket) Encode(w io.Writer) (err error) {
|
|
_, err = w.Write(u.Data)
|
|
return
|
|
}
|
|
|
|
func (u *UnCodablePacket) Decode(r io.ReadSeeker) (err error) {
|
|
err = errors.New("how did you even get here")
|
|
return
|
|
}
|
|
|
|
type Conn struct {
|
|
RawConn
|
|
*event.Loop
|
|
state proto_base.State
|
|
stateMutex sync.RWMutex
|
|
Actor proto_base.Actor
|
|
Version string
|
|
ProtocolVersion int32
|
|
DebugPrintPackets []string
|
|
|
|
asyncPackets bool
|
|
asyncPacketsMutex sync.RWMutex
|
|
AlwaysDecode bool
|
|
|
|
Log *slog.Logger
|
|
}
|
|
|
|
// StartConn starts the connection event loop, registering the receive routine and login tick handler.
|
|
func (c *Conn) StartConn() (err error) {
|
|
c.RegisterEventSource(c.ActivateReceiveRoutine())
|
|
c.RegisterCritical(c.OnLoginTick)
|
|
err = c.Loop.StartLoop()
|
|
c.Loop = event.NewLoop()
|
|
return
|
|
}
|
|
|
|
// OnLoginTick handles a login tick by receiving a packet and firing it as an event.
|
|
func (c *Conn) OnLoginTick(_ LoginTick) (err error) {
|
|
packet, err := c.Receive()
|
|
if err != nil {
|
|
return
|
|
}
|
|
err = c.Fire(packet)
|
|
return
|
|
}
|
|
|
|
// SetState sets the connection's protocol state.
|
|
func (c *Conn) SetState(state proto_base.State) {
|
|
c.stateMutex.Lock()
|
|
defer c.stateMutex.Unlock()
|
|
c.state = state
|
|
}
|
|
|
|
// State returns the current connection protocol state.
|
|
func (c *Conn) State() proto_base.State {
|
|
c.stateMutex.RLock()
|
|
defer c.stateMutex.RUnlock()
|
|
return c.state
|
|
}
|
|
|
|
// SetAsyncPackets enables or disables asynchronous packet handling in the receive routine.
|
|
func (c *Conn) SetAsyncPackets(asyncPackets bool) {
|
|
c.asyncPacketsMutex.Lock()
|
|
defer c.asyncPacketsMutex.Unlock()
|
|
c.asyncPackets = asyncPackets
|
|
}
|
|
|
|
// GetAsyncPackets returns whether asynchronous packet handling is enabled.
|
|
func (c *Conn) GetAsyncPackets() bool {
|
|
c.asyncPacketsMutex.RLock()
|
|
defer c.asyncPacketsMutex.RUnlock()
|
|
return c.asyncPackets
|
|
}
|
|
|
|
// SetVersion sets the connection's Minecraft version by version name and looks up the corresponding protocol version.
|
|
func (c *Conn) SetVersion(version string) (err error) {
|
|
versionData, err := data.LookUpVersionByName(version)
|
|
if err != nil {
|
|
return
|
|
}
|
|
c.ProtocolVersion = versionData.Version
|
|
c.Version = version
|
|
return
|
|
}
|
|
|
|
// SetProtocolVersion sets the connection's version by protocol version number and looks up the corresponding version name.
|
|
func (c *Conn) SetProtocolVersion(version int32) (err error) {
|
|
versionData, err := data.LookUpVersionByProtocolVersion(version)
|
|
if err != nil {
|
|
return
|
|
}
|
|
c.ProtocolVersion = versionData.Version
|
|
c.Version = versionData.MinecraftVersion
|
|
return
|
|
}
|
|
|
|
// ActivateReceiveRoutine activates the receive-routine.
|
|
// It ensures that the receive-routine is only started once and creates a buffered channel for packet handling.
|
|
func (c *Conn) ActivateReceiveRoutine() (chSending <-chan any) {
|
|
// This expects you to start a new routine when switching into play state
|
|
ch := make(chan any, BufferNPackets)
|
|
go func() {
|
|
ctx := c.Ctx // this prevents c.Ctx being reassigned
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
default:
|
|
if !c.GetAsyncPackets() {
|
|
ch <- LoginTick{}
|
|
} else {
|
|
packet, err := c.Receive()
|
|
if err != nil {
|
|
ch <- err
|
|
return
|
|
}
|
|
ch <- packet
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
chSending = ch
|
|
return
|
|
}
|
|
|
|
func (c *Conn) translatePacketVersion(packet proto_base.EncodeDecodeAble, packetInfo proto_base.PacketInfo) (convertedPacket proto_base.EncodeDecodeAble, packetInfoReal proto_base.PacketInfo, err error) {
|
|
packetInfoReal, found := LookupPacketInfoByNameProtocolVersionStateAndDirection(packetInfo.Name, c.ProtocolVersion, c.State(), packetInfo.Direction)
|
|
if !found {
|
|
err = fmt.Errorf("%w: %s from pv%d to pv%d in %s/%s not found", PacketDoesntExistInFutureVersionErr, packetInfo.Name, packetInfo.ProtocolVersion, c.ProtocolVersion, packetInfo.State, packetInfo.Direction)
|
|
return
|
|
}
|
|
|
|
dstType := reflect.TypeOf(packetInfoReal.Type)
|
|
srcType := reflect.TypeOf(packetInfo.Type)
|
|
if !SmartConvertibleTo(srcType, dstType) {
|
|
err = fmt.Errorf("%w: %s from pv%d to pv%d in %s/%s (src=%s dst=%s)", CantTranslateErr, packetInfo.Name, packetInfo.ProtocolVersion, c.ProtocolVersion, packetInfo.State, packetInfo.Direction, srcType, dstType)
|
|
return
|
|
}
|
|
// can't fail cause pacetinfo.type is proto_base.EncodeDecodeAble
|
|
convertedPacket = SmartConvert(reflect.ValueOf(packet), dstType).Interface().(proto_base.EncodeDecodeAble)
|
|
return
|
|
}
|
|
|
|
// SimplePacketToBytes converts a packet to bytes and prepends the packet length and packet id.
|
|
// Its usefull for generating static data like handshakes or for debugging.
|
|
func SimplePacketToBytes(packet proto_base.EncodeDecodeAble) (b []byte, err error) {
|
|
var packetBuff = bytes.NewBuffer(nil)
|
|
i, ok := LookupPacketInfoByType(packet)
|
|
if !ok {
|
|
err = BadPacketTypeErr
|
|
return
|
|
}
|
|
err = proto_base.EncodeVarInt(packetBuff, i.PacketId)
|
|
if err != nil {
|
|
return
|
|
}
|
|
err = packet.Encode(packetBuff)
|
|
if err != nil {
|
|
return
|
|
}
|
|
b = packetBuff.Bytes()
|
|
return
|
|
}
|
|
|
|
// SimplePacketToBytesLenPrefix is like SimplePacketToBytes but also prepends the length-prefix framing.
|
|
func SimplePacketToBytesLenPrefix(packet proto_base.EncodeDecodeAble) (b []byte, err error) {
|
|
b, err = SimplePacketToBytes(packet)
|
|
if err != nil {
|
|
return
|
|
}
|
|
var packetWithLenBuf = bytes.NewBuffer(nil)
|
|
err = proto_base.EncodeVarInt(packetWithLenBuf, int32(len(b)))
|
|
if err != nil {
|
|
return
|
|
}
|
|
packetWithLenBuf.Write(b)
|
|
b = packetWithLenBuf.Bytes()
|
|
return
|
|
}
|
|
|
|
// SimpleBytesToPacket converts packet bytes to usable packet data.
|
|
func SimpleBytesToPacket(packetBytes []byte, version int32, direction proto_base.Direction, state proto_base.State) (packet proto_base.EncodeDecodeAble, err error) {
|
|
b := bytes.NewReader(packetBytes)
|
|
id, err := proto_base.DecodeVarInt(b)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
i, ok := LookUpTypeByPacketInfo(direction, state, id, version)
|
|
if !ok {
|
|
packet = &UnCodablePacket{Err: BadPacketIdErr, Data: packetBytes, Info: i}
|
|
return
|
|
}
|
|
packet = reflect.New(reflect.TypeOf(i.Type).Elem()).Interface().(proto_base.EncodeDecodeAble)
|
|
err = packet.Decode(b)
|
|
if err != nil {
|
|
packet = &UnCodablePacket{Err: err, Data: packetBytes, Info: i}
|
|
err = nil
|
|
return
|
|
}
|
|
if b.Len() != 0 {
|
|
err = nil
|
|
packet = &UnCodablePacket{Err: PacketNotFullyDecodedErr, Data: packetBytes, Info: i}
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
func (c *Conn) sendRegisteredPacket(packet proto_base.EncodeDecodeAble) (err error) {
|
|
i, ok := LookupPacketInfoByTypeAndState(packet, c.State())
|
|
if !ok {
|
|
err = BadPacketTypeErr
|
|
return
|
|
}
|
|
if i.Direction != c.Actor.SendDirection() {
|
|
err = WrongDirectionErr
|
|
return
|
|
}
|
|
if i.State != c.State() {
|
|
err = WrongStateErr
|
|
return
|
|
}
|
|
if i.ProtocolVersion != c.ProtocolVersion {
|
|
packet, i, err = c.translatePacketVersion(packet, i)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
var packetBuff = bytes.NewBuffer(nil)
|
|
err = proto_base.EncodeVarInt(packetBuff, i.PacketId)
|
|
if err != nil {
|
|
return
|
|
}
|
|
err = packet.Encode(packetBuff)
|
|
if err != nil {
|
|
return
|
|
}
|
|
debugPrint := slog.Default().Enabled(context.Background(), slog.LevelDebug) &&
|
|
slices.ContainsFunc(c.DebugPrintPackets, func(s string) bool { return strings.Contains(i.Name, s) })
|
|
if debugPrint {
|
|
c.Log.Debug("send", "actor", c.Actor, "packet", fmt.Sprintf("%#v", packet))
|
|
}
|
|
return c.SendRaw(packetBuff.Bytes())
|
|
}
|
|
|
|
// Send encodes and sends a packet to the remote peer, translating protocol versions if needed.
|
|
func (c *Conn) Send(packet proto_base.EncodeDecodeAble) (err error) {
|
|
switch packet := packet.(type) {
|
|
case *UnCodablePacket:
|
|
err = c.SendRaw(packet.Data)
|
|
default:
|
|
return c.sendRegisteredPacket(packet)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Receive reads and decodes a raw packet from the remote peer.
|
|
func (c *Conn) Receive() (packet proto_base.EncodeDecodeAble, err error) {
|
|
if c.R == nil {
|
|
return
|
|
}
|
|
packetBytes, err := c.ReceiveRaw()
|
|
if err != nil {
|
|
return
|
|
}
|
|
b := bytes.NewReader(packetBytes)
|
|
id, err := proto_base.DecodeVarInt(b)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
i, ok := LookUpTypeByPacketInfo(c.Actor.ReceiveDirection(), c.State(), id, c.ProtocolVersion)
|
|
if !ok {
|
|
packet = &UnCodablePacket{Err: BadPacketIdErr, Data: packetBytes, Info: i}
|
|
return
|
|
}
|
|
packet = reflect.New(reflect.TypeOf(i.Type).Elem()).Interface().(proto_base.EncodeDecodeAble)
|
|
debugPrint := slog.Default().Enabled(context.Background(), slog.LevelDebug) &&
|
|
slices.ContainsFunc(c.DebugPrintPackets, func(s string) bool { return strings.Contains(i.Name, s) })
|
|
isListenerRegistered := c.IsTypeRegistered(reflect.TypeOf(packet))
|
|
if !isListenerRegistered && !debugPrint && !c.AlwaysDecode {
|
|
packet = &UnCodablePacket{Err: NoHandlerRegisteredErr, Data: packetBytes, Info: i}
|
|
return
|
|
}
|
|
err = packet.Decode(b)
|
|
if err != nil {
|
|
packet = &UnCodablePacket{Err: err, Data: packetBytes, Info: i}
|
|
err = nil
|
|
return
|
|
}
|
|
if b.Len() != 0 {
|
|
err = nil
|
|
packet = &UnCodablePacket{Err: PacketNotFullyDecodedErr, Data: packetBytes, Info: i}
|
|
return
|
|
}
|
|
if debugPrint {
|
|
c.Log.Debug("recv", "packet", fmt.Sprintf("%#v", packet), "isListenerRegistered", isListenerRegistered, "len", len(packetBytes))
|
|
}
|
|
return
|
|
}
|
|
|
|
// NewConn creates a new Conn with default settings and a background context.
|
|
func NewConn() (ret *Conn) {
|
|
return NewConnCtx(context.Background())
|
|
}
|
|
|
|
// NewConnCtx creates a new Conn with the given context, setting the handshaking state and latest version.
|
|
func NewConnCtx(ctx context.Context) (ret *Conn) {
|
|
ret = &Conn{}
|
|
ret.SetState(proto_base.Handshaking)
|
|
ret.SetCompressionThreshold(-1)
|
|
err := ret.SetVersion(LatestVersion)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
ret.Loop = event.NewLoopCtx(ctx)
|
|
ret.Log = slog.Default()
|
|
return
|
|
}
|