From 8eda8378fd1f485e6ad1c205e5faaae0ac070e7e Mon Sep 17 00:00:00 2001 From: BorisP1234 Date: Tue, 18 Feb 2025 16:05:26 +0100 Subject: [PATCH] feat: add cookies --- .../proto/packet/cookie/cookie_request.go | 22 +++++ .../proto/packet/cookie/cookie_response.go | 32 +++++++ .../java/proto/packet/cookie/cookie_store.go | 32 +++++++ pkg/edition/java/proto/state/register.go | 24 +++++ pkg/edition/java/proto/util/reader.go | 2 +- pkg/edition/java/proxy/events.go | 26 ++++++ pkg/edition/java/proxy/player.go | 88 +++++++++++++++++++ pkg/edition/java/proxy/session_client_auth.go | 31 ++++++- .../java/proxy/session_client_config.go | 27 ++++++ .../proxy/session_client_initial_login.go | 3 +- pkg/edition/java/proxy/session_client_play.go | 25 ++++++ 11 files changed, 308 insertions(+), 4 deletions(-) create mode 100644 pkg/edition/java/proto/packet/cookie/cookie_request.go create mode 100644 pkg/edition/java/proto/packet/cookie/cookie_response.go create mode 100644 pkg/edition/java/proto/packet/cookie/cookie_store.go diff --git a/pkg/edition/java/proto/packet/cookie/cookie_request.go b/pkg/edition/java/proto/packet/cookie/cookie_request.go new file mode 100644 index 00000000..a755f36c --- /dev/null +++ b/pkg/edition/java/proto/packet/cookie/cookie_request.go @@ -0,0 +1,22 @@ +package cookie + +import ( + "io" + + "go.minekube.com/common/minecraft/key" + "go.minekube.com/gate/pkg/edition/java/proto/util" + "go.minekube.com/gate/pkg/gate/proto" +) + +type CookieRequest struct { + Key key.Key +} + +func (c *CookieRequest) Encode(ctx *proto.PacketContext, wr io.Writer) error { + return util.WriteKey(wr, c.Key) +} + +func (c *CookieRequest) Decode(ctx *proto.PacketContext, rd io.Reader) (err error) { + c.Key, err = util.ReadKey(rd) + return err +} diff --git a/pkg/edition/java/proto/packet/cookie/cookie_response.go b/pkg/edition/java/proto/packet/cookie/cookie_response.go new file mode 100644 index 00000000..0d20e295 --- /dev/null +++ b/pkg/edition/java/proto/packet/cookie/cookie_response.go @@ -0,0 +1,32 @@ +package cookie + +import ( + "io" + + "go.minekube.com/common/minecraft/key" + "go.minekube.com/gate/pkg/edition/java/proto/util" + "go.minekube.com/gate/pkg/gate/proto" +) + +type CookieResponse struct { + Key key.Key + Payload []byte +} + +func (c *CookieResponse) Encode(ctx *proto.PacketContext, wr io.Writer) error { + if err := util.WriteKey(wr, c.Key); err != nil { + return err + } + + return util.WriteBytes(wr, c.Payload) +} + +func (c *CookieResponse) Decode(ctx *proto.PacketContext, rd io.Reader) (err error) { + c.Key, err = util.ReadKey(rd) + if err != nil { + return err + } + + c.Payload, err = util.ReadRawBytes(rd) + return err +} diff --git a/pkg/edition/java/proto/packet/cookie/cookie_store.go b/pkg/edition/java/proto/packet/cookie/cookie_store.go new file mode 100644 index 00000000..9c90104d --- /dev/null +++ b/pkg/edition/java/proto/packet/cookie/cookie_store.go @@ -0,0 +1,32 @@ +package cookie + +import ( + "io" + + "go.minekube.com/common/minecraft/key" + "go.minekube.com/gate/pkg/edition/java/proto/util" + "go.minekube.com/gate/pkg/gate/proto" +) + +type CookieStore struct { + Key key.Key + Payload []byte +} + +func (c *CookieStore) Encode(ctx *proto.PacketContext, wr io.Writer) error { + if err := util.WriteKey(wr, c.Key); err != nil { + return err + } + + return util.WriteBytes(wr, c.Payload) +} + +func (c *CookieStore) Decode(ctx *proto.PacketContext, rd io.Reader) (err error) { + c.Key, err = util.ReadKey(rd) + if err != nil { + return err + } + + c.Payload, err = util.ReadBytes(rd) + return err +} diff --git a/pkg/edition/java/proto/state/register.go b/pkg/edition/java/proto/state/register.go index c3fd0d36..845997fc 100644 --- a/pkg/edition/java/proto/state/register.go +++ b/pkg/edition/java/proto/state/register.go @@ -5,6 +5,7 @@ import ( "go.minekube.com/gate/pkg/edition/java/proto/packet/bossbar" "go.minekube.com/gate/pkg/edition/java/proto/packet/chat" "go.minekube.com/gate/pkg/edition/java/proto/packet/config" + "go.minekube.com/gate/pkg/edition/java/proto/packet/cookie" "go.minekube.com/gate/pkg/edition/java/proto/packet/plugin" "go.minekube.com/gate/pkg/edition/java/proto/packet/tablist/legacytablist" "go.minekube.com/gate/pkg/edition/java/proto/packet/tablist/playerinfo" @@ -61,6 +62,9 @@ func init() { Config.ServerBound.Register(&config.KnownPacks{}, m(0x07, version.Minecraft_1_20_5), ) + Config.ServerBound.Register(&cookie.CookieResponse{}, + m(0x01, version.Minecraft_1_20_5), + ) Config.ClientBound.Register(&plugin.Message{}, m(0x00, version.Minecraft_1_20_2), @@ -117,6 +121,12 @@ func init() { Config.ClientBound.Register(&p.ServerLinks{}, m(0x10, version.Minecraft_1_21), ) + Config.ClientBound.Register(&cookie.CookieRequest{}, + m(0x00, version.Minecraft_1_20_5), + ) + Config.ClientBound.Register(&cookie.CookieStore{}, + m(0x0A, version.Minecraft_1_20_5), + ) Login.ServerBound.Register(&p.ServerLogin{}, m(0x00, version.Minecraft_1_7_2)) @@ -126,6 +136,9 @@ func init() { m(0x02, version.Minecraft_1_13)) Login.ServerBound.Register(&p.LoginAcknowledged{}, m(0x03, version.Minecraft_1_20_2)) + Login.ServerBound.Register(&cookie.CookieResponse{}, + m(0x04, version.Minecraft_1_20_5), + ) Login.ClientBound.Register(&p.Disconnect{}, m(0x00, version.Minecraft_1_7_2)) @@ -137,6 +150,8 @@ func init() { m(0x03, version.Minecraft_1_8)) Login.ClientBound.Register(&p.LoginPluginMessage{}, m(0x04, version.Minecraft_1_13)) + Login.ClientBound.Register(&cookie.CookieRequest{}, + m(0x05, version.Minecraft_1_20_5)) Play.ServerBound.Fallback = false Play.ClientBound.Fallback = false @@ -259,6 +274,9 @@ func init() { m(0x0C, version.Minecraft_1_20_5), m(0x0E, version.Minecraft_1_21_2), ) + Play.ServerBound.Register(&cookie.CookieResponse{}, + m(0x13, version.Minecraft_1_20_5), + ) Play.ClientBound.Register(&p.KeepAlive{}, m(0x00, version.Minecraft_1_7_2), @@ -570,4 +588,10 @@ func init() { Play.ClientBound.Register(&p.ServerLinks{}, m(0x7B, version.Minecraft_1_21), ) + Play.ClientBound.Register(&cookie.CookieRequest{}, + m(0x16, version.Minecraft_1_20_5), + ) + Play.ClientBound.Register(&cookie.CookieStore{}, + m(0x72, version.Minecraft_1_20_5), + ) } diff --git a/pkg/edition/java/proto/util/reader.go b/pkg/edition/java/proto/util/reader.go index f938a5da..be60984a 100644 --- a/pkg/edition/java/proto/util/reader.go +++ b/pkg/edition/java/proto/util/reader.go @@ -5,12 +5,12 @@ import ( "encoding/binary" "errors" "fmt" - "go.minekube.com/common/minecraft/key" "io" "math" "strings" "time" + "go.minekube.com/common/minecraft/key" "go.minekube.com/gate/pkg/edition/java/profile" "go.minekube.com/gate/pkg/util/uuid" ) diff --git a/pkg/edition/java/proxy/events.go b/pkg/edition/java/proxy/events.go index 2aab227d..632f6f04 100644 --- a/pkg/edition/java/proxy/events.go +++ b/pkg/edition/java/proxy/events.go @@ -8,6 +8,7 @@ import ( "go.minekube.com/brigodier" "go.minekube.com/common/minecraft/component" + "go.minekube.com/common/minecraft/key" "go.minekube.com/gate/pkg/command" "go.minekube.com/gate/pkg/edition/java/forge/modinfo" @@ -1163,3 +1164,28 @@ func (r *ReadyEvent) Addr() string { return r.addr } // Subscribe to this event to gracefully stop any subtasks, // such as plugin dependencies. type ShutdownEvent struct{} + +// PlayerCookieResponseEvent is fired when a player sends the cookie requested from the server. +type PlayerCookieResponseEvent struct { + player Player + key key.Key + payload []byte +} + +func newPlayerCookieResponseEvent(player Player, key key.Key, payload []byte) *PlayerCookieResponseEvent { + return &PlayerCookieResponseEvent{ + player: player, + key: key, + payload: payload, + } +} + +// Player returns the player from whom the cookie has been received. +func (c *PlayerCookieResponseEvent) Player() Player { return c.player } + +// Key returns the provider of the responded cookie. +// For example: minecraft:cookie +func (c *PlayerCookieResponseEvent) Key() key.Key { return c.key } + +// Payload returns the payload of the responded cookie. +func (c *PlayerCookieResponseEvent) Payload() []byte { return c.payload } diff --git a/pkg/edition/java/proxy/player.go b/pkg/edition/java/proxy/player.go index 31507ea1..2d460f41 100644 --- a/pkg/edition/java/proxy/player.go +++ b/pkg/edition/java/proxy/player.go @@ -13,6 +13,7 @@ import ( "github.com/robinbraemer/event" cfgpacket "go.minekube.com/gate/pkg/edition/java/proto/packet/config" + "go.minekube.com/gate/pkg/edition/java/proto/packet/cookie" "go.minekube.com/gate/pkg/edition/java/proto/state" "go.minekube.com/gate/pkg/edition/java/proto/state/states" "go.minekube.com/gate/pkg/edition/java/proxy/internal/resourcepack" @@ -23,6 +24,7 @@ import ( "github.com/go-logr/logr" "go.minekube.com/common/minecraft/component" "go.minekube.com/common/minecraft/component/codec/legacy" + "go.minekube.com/common/minecraft/key" "go.uber.org/atomic" "go.minekube.com/gate/pkg/edition/java/config" @@ -108,6 +110,15 @@ type Player interface { // TODO convert to struct(?) bc this is a lot of methods // // Deprecated: Use PendingResourcePacks instead. PendingResourcePack() *ResourcePackInfo + + StoreCookie(key key.Key, payload []byte) error + + // Sends request for a cookie which the player will respond to in the PlayerCookieResponseEvent. + RequestCookie(key key.Key) error + + // Sends request for a cookie which the player will respond to in this function. + // It times out after 5 seconds if the player doesn't send any packet back. + RequestCookieWithResult(key key.Key) ([]byte, error) } type connectedPlayer struct { @@ -143,6 +154,8 @@ type connectedPlayer struct { serversToTry []string // names of servers to try if we got disconnected from previous tryIndex int + + CookieRequestTracker *cookieRequestTracker } var _ Player = (*connectedPlayer)(nil) @@ -170,6 +183,9 @@ func newConnectedPlayer( ping: ping, permFunc: func(string) permission.TriState { return permission.Undefined }, playerKey: playerKey, + CookieRequestTracker: &cookieRequestTracker{ + pending: make(map[string]chan []byte), + }, } p.resourcePackHandler = resourcepack.NewHandler(p, p.eventMgr) p.bundleHandler = &resourcepack.BundleDelimiterHandler{Player: p} @@ -735,3 +751,75 @@ func (p *connectedPlayer) BackendInFlight() proto.PacketWriter { } return nil } + +func (p *connectedPlayer) StoreCookie(key key.Key, payload []byte) error { + if strings.TrimSpace(key.String()) == "" { + return errors.New("empty key") + } + + if len(payload) > 5*1024 { + return errors.New("payload size exceeds 5 kiB") + } + + if p.Protocol().Lower(version.Minecraft_1_20_5) { + return fmt.Errorf("%w: but player is on %s", ErrTransferUnsupportedClientProtocol, p.Protocol()) + } + + if p.State() != state.Play && p.State() != state.Config { + return errors.New("CookieStore packet can only be send in the Play and Configuration State") + } + + return p.WritePacket(&cookie.CookieStore{ + Key: key, + Payload: payload, + }) +} + +func (p *connectedPlayer) RequestCookie(key key.Key) error { + if strings.TrimSpace(key.String()) == "" { + return errors.New("empty key") + } + + if p.Protocol().Lower(version.Minecraft_1_20_5) { + return fmt.Errorf("%w: but player is on %s", ErrTransferUnsupportedClientProtocol, p.Protocol()) + } + + return p.WritePacket(&cookie.CookieRequest{ + Key: key, + }) +} + +type cookieRequestTracker struct { + mu sync.Mutex + pending map[string]chan []byte +} + +func (p *connectedPlayer) RequestCookieWithResult(key key.Key) ([]byte, error) { + if strings.TrimSpace(key.String()) == "" { + return nil, errors.New("empty key") + } + + if p.Protocol().Lower(version.Minecraft_1_20_5) { + return nil, fmt.Errorf("%w: but player is on %s", ErrTransferUnsupportedClientProtocol, p.Protocol()) + } + + responseChan := make(chan []byte, 1) + requestID := fmt.Sprintf("%s:%s", p.ID(), key) + + p.CookieRequestTracker.mu.Lock() + p.CookieRequestTracker.pending[requestID] = responseChan + defer delete(p.CookieRequestTracker.pending, requestID) + p.CookieRequestTracker.mu.Unlock() + + err := p.WritePacket(&cookie.CookieRequest{Key: key}) + if err != nil { + return nil, err + } + + select { + case response := <-responseChan: + return response, nil + case <-time.After(5 * time.Second): + return nil, errors.New("timeout waiting for response") + } +} diff --git a/pkg/edition/java/proxy/session_client_auth.go b/pkg/edition/java/proxy/session_client_auth.go index 827ccf57..b0c2581d 100644 --- a/pkg/edition/java/proxy/session_client_auth.go +++ b/pkg/edition/java/proxy/session_client_auth.go @@ -1,6 +1,9 @@ package proxy import ( + "fmt" + "sync/atomic" + "github.com/go-logr/logr" "github.com/robinbraemer/event" "go.minekube.com/common/minecraft/component" @@ -8,12 +11,12 @@ import ( "go.minekube.com/gate/pkg/edition/java/netmc" "go.minekube.com/gate/pkg/edition/java/profile" "go.minekube.com/gate/pkg/edition/java/proto/packet" + "go.minekube.com/gate/pkg/edition/java/proto/packet/cookie" "go.minekube.com/gate/pkg/edition/java/proto/state" "go.minekube.com/gate/pkg/edition/java/proto/version" "go.minekube.com/gate/pkg/edition/java/proxy/crypto" "go.minekube.com/gate/pkg/gate/proto" "go.minekube.com/gate/pkg/util/uuid" - "sync/atomic" ) type authSessionHandler struct { @@ -228,9 +231,11 @@ func (a *authSessionHandler) connectToInitialServer(player *connectedPlayer) { func (a *authSessionHandler) Deactivated() {} func (a *authSessionHandler) HandlePacket(pc *proto.PacketContext) { - switch pc.Packet.(type) { + switch t := pc.Packet.(type) { case *packet.LoginAcknowledged: a.handleLoginAcknowledged() + case *cookie.CookieResponse: + a.handleCookieResponse(t) default: a.log.Info("unexpected packet during auth session", "packet", pc.Packet, @@ -265,3 +270,25 @@ func (a *authSessionHandler) handleLoginAcknowledged() bool { } return true } + +func (a *authSessionHandler) handleCookieResponse(p *cookie.CookieResponse) { + // The payload in the cookie response packet has two bytes before the actual payload, which show the length. + // So the packet should use the util.ReadBytesLen(rd, 5120) but unfortunately i couldn't get that to work, as it makes the payload empty. + // TODO: fix this instead of this shortcut + if len(p.Payload) > 2 { + p.Payload = p.Payload[2:] + } + + tracker := a.connectedPlayer.CookieRequestTracker + requestID := fmt.Sprintf("%s:%s", a.connectedPlayer.ID(), p.Key) + tracker.mu.Lock() + responseChan, ok := tracker.pending[requestID] + + // check if the player.RequestCookieWithResult() is waiting for this packet, otherwise fire the event. + if ok { + responseChan <- p.Payload + } else { + event.FireParallel(a.eventMgr, newPlayerCookieResponseEvent(a.connectedPlayer, p.Key, p.Payload)) + } + tracker.mu.Unlock() +} diff --git a/pkg/edition/java/proxy/session_client_config.go b/pkg/edition/java/proxy/session_client_config.go index cab12e70..4dd40fc3 100644 --- a/pkg/edition/java/proxy/session_client_config.go +++ b/pkg/edition/java/proxy/session_client_config.go @@ -2,10 +2,13 @@ package proxy import ( "bytes" + "fmt" + "github.com/go-logr/logr" "github.com/robinbraemer/event" "go.minekube.com/gate/pkg/edition/java/proto/packet" "go.minekube.com/gate/pkg/edition/java/proto/packet/config" + "go.minekube.com/gate/pkg/edition/java/proto/packet/cookie" "go.minekube.com/gate/pkg/edition/java/proto/packet/plugin" "go.minekube.com/gate/pkg/edition/java/proto/state" "go.minekube.com/gate/pkg/edition/java/proto/util" @@ -67,6 +70,8 @@ func (h *clientConfigSessionHandler) HandlePacket(pc *proto.PacketContext) { } case *config.KnownPacks: h.handleKnownPacks(p, pc) + case *cookie.CookieResponse: + h.handleCookieResponse(p) default: forwardToServer(pc, h.player) } @@ -141,3 +146,25 @@ func (h *clientConfigSessionHandler) handleKnownPacks(p *config.KnownPacks, pc * func (h *clientConfigSessionHandler) event() event.Manager { return h.player.proxy.Event() } + +func (h *clientConfigSessionHandler) handleCookieResponse(p *cookie.CookieResponse) { + // The payload in the cookie response packet has two bytes before the actual payload, which show the length. + // So the packet should use the util.ReadBytesLen(rd, 5120) but unfortunately i couldn't get that to work, as it makes the payload empty. + // TODO: fix this instead of this shortcut + if len(p.Payload) > 2 { + p.Payload = p.Payload[2:] + } + + tracker := h.player.CookieRequestTracker + requestID := fmt.Sprintf("%s:%s", h.player.ID(), p.Key) + tracker.mu.Lock() + responseChan, ok := tracker.pending[requestID] + + // check if the player.RequestCookieWithResult() is waiting for this packet, otherwise fire the event. + if ok { + responseChan <- p.Payload + } else { + event.FireParallel(h.event(), newPlayerCookieResponseEvent(h.player, p.Key, p.Payload)) + } + tracker.mu.Unlock() +} diff --git a/pkg/edition/java/proxy/session_client_initial_login.go b/pkg/edition/java/proxy/session_client_initial_login.go index d37e0b1a..5021f091 100644 --- a/pkg/edition/java/proxy/session_client_initial_login.go +++ b/pkg/edition/java/proxy/session_client_initial_login.go @@ -5,10 +5,11 @@ import ( "context" "crypto/rand" "errors" - "go.minekube.com/gate/pkg/edition/java/proto/state" "regexp" "time" + "go.minekube.com/gate/pkg/edition/java/proto/state" + "github.com/go-logr/logr" "go.minekube.com/common/minecraft/color" "go.minekube.com/common/minecraft/component" diff --git a/pkg/edition/java/proxy/session_client_play.go b/pkg/edition/java/proxy/session_client_play.go index 9dbb07e8..a7bb6a3c 100644 --- a/pkg/edition/java/proxy/session_client_play.go +++ b/pkg/edition/java/proxy/session_client_play.go @@ -10,6 +10,7 @@ import ( "time" "go.minekube.com/gate/pkg/edition/java/proto/packet/config" + "go.minekube.com/gate/pkg/edition/java/proto/packet/cookie" "go.minekube.com/gate/pkg/edition/java/proxy/tablist" "go.minekube.com/gate/pkg/internal/future" @@ -113,6 +114,8 @@ func (c *clientPlaySessionHandler) HandlePacket(pc *proto.PacketContext) { c.handleFinishedUpdate(p) case *chat.ChatAcknowledgement: c.handleChatAcknowledgement(p) + case *cookie.CookieResponse: + c.handleCookieResponse(p) default: c.forwardToServer(pc) } @@ -827,6 +830,28 @@ func (c *clientPlaySessionHandler) handleChatAcknowledgement(p *chat.ChatAcknowl c.player.chatQueue.HandleAcknowledgement(p.Offset) } +func (c *clientPlaySessionHandler) handleCookieResponse(p *cookie.CookieResponse) { + // The payload in the cookie response packet has two bytes before the actual payload, which show the length. + // So the packet should use the util.ReadBytesLen(rd, 5120) but unfortunately i couldn't get that to work, as it makes the payload empty. + // TODO: fix this instead of this shortcut + if len(p.Payload) > 2 { + p.Payload = p.Payload[2:] + } + + tracker := c.player.CookieRequestTracker + requestID := fmt.Sprintf("%s:%s", c.player.ID(), p.Key) + tracker.mu.Lock() + responseChan, ok := tracker.pending[requestID] + + // check if the player.RequestCookieWithResult() is waiting for this packet, otherwise fire the event. + if ok { + responseChan <- p.Payload + } else { + event.FireParallel(c.player.eventMgr, newPlayerCookieResponseEvent(c.player, p.Key, p.Payload)) + } + tracker.mu.Unlock() +} + // doSwitch handles switching stages for swapping between servers. func (c *clientPlaySessionHandler) doSwitch() *future.Future[any] { c.log.V(1).WithName("doSwitch").Info("switching servers")