From fa83b762bf2e52ffb219b29ca495919cb755c88e Mon Sep 17 00:00:00 2001 From: Tnze Date: Sun, 1 Jan 2023 13:17:07 +0800 Subject: [PATCH] Add chat session --- bot/msg/cache.go | 47 ++++++++++ bot/msg/cache_test.go | 84 ++++++++++++++++++ bot/msg/chat.go | 33 +++++-- bot/playerlist/playerlist.go | 10 ++- chat/sign/session.go | 50 +++++++++++ chat/sign/sign.go | 9 +- chat/sign/sign_test.go | 1 - net/packet/util.go | 8 ++ server/auth/auth.go | 12 +-- server/gameplay.go | 5 +- server/login.go | 6 +- yggdrasil/user/{auth.go => property.go} | 0 {server/auth => yggdrasil/user}/pubkey.go | 2 +- {server/auth => yggdrasil/user}/validator.go | 2 +- .../user}/yggdrasil_session_pubkey.der | Bin 15 files changed, 244 insertions(+), 25 deletions(-) create mode 100644 bot/msg/cache.go create mode 100644 bot/msg/cache_test.go create mode 100644 chat/sign/session.go delete mode 100644 chat/sign/sign_test.go rename yggdrasil/user/{auth.go => property.go} (100%) rename {server/auth => yggdrasil/user}/pubkey.go (99%) rename {server/auth => yggdrasil/user}/validator.go (99%) rename {server/auth => yggdrasil/user}/yggdrasil_session_pubkey.der (100%) diff --git a/bot/msg/cache.go b/bot/msg/cache.go new file mode 100644 index 0000000..80ed038 --- /dev/null +++ b/bot/msg/cache.go @@ -0,0 +1,47 @@ +package msg + +import ( + "github.com/Tnze/go-mc/chat/sign" +) + +type signatureCache struct { + signatures [128]*sign.Signature + signIndexes map[sign.Signature]int + cachedBuffer []*sign.Signature +} + +func newSignatureCache() signatureCache { + return signatureCache{ + signIndexes: make(map[sign.Signature]int), + } +} + +func (s *signatureCache) popOrInsert(self *sign.Signature, lastSeen []sign.PackedSignature) error { + var tmp *sign.Signature + s.cachedBuffer = s.cachedBuffer[:0] // clear buffer + if self != nil { + s.cachedBuffer = append(s.cachedBuffer, self) + } + for _, v := range lastSeen { + if v.Signature != nil { + s.cachedBuffer = append(s.cachedBuffer, v.Signature) + } else if v.ID >= 0 && int(v.ID) < len(s.signatures) { + s.cachedBuffer = append(s.cachedBuffer, s.signatures[v.ID]) + } else { + return InvalidChatPacket + } + } + for i := 0; i < len(s.cachedBuffer) && i < len(s.signatures); i++ { + v := s.cachedBuffer[i] + if i, ok := s.signIndexes[*v]; ok { + s.signatures[i] = nil + } + tmp, s.signatures[i] = s.signatures[i], v + s.signIndexes[*v] = i + if tmp != nil { + s.cachedBuffer = append(s.cachedBuffer, tmp) + delete(s.signIndexes, *tmp) + } + } + return nil +} diff --git a/bot/msg/cache_test.go b/bot/msg/cache_test.go new file mode 100644 index 0000000..a61bb7b --- /dev/null +++ b/bot/msg/cache_test.go @@ -0,0 +1,84 @@ +package msg + +import ( + "crypto/rand" + "testing" + + "github.com/Tnze/go-mc/chat/sign" +) + +func TestSignatureCache(t *testing.T) { + cache := newSignatureCache() + s1 := &sign.Signature{1} + s2 := &sign.Signature{2} + s3 := &sign.Signature{3} + s4 := &sign.Signature{4} + // t.Logf("%p, %p, %p, %p", s1, s2, s3, s4) + err := cache.popOrInsert(nil, []sign.PackedSignature{ + {Signature: s1}, + {Signature: s2}, + {Signature: s3}, + }) + if err != nil { + t.Fatal(err) + } + // cache: [s1, s2, s3, nil...] + if cache.signatures[0] != s1 || cache.signatures[1] != s2 || cache.signatures[2] != s3 { + t.Log(cache.signatures) + t.Fatal("insert s1~3 error") + } + err = cache.popOrInsert(s4, []sign.PackedSignature{{Signature: s3}}) + if err != nil { + t.Fatal(err) + } + // cache: [s4, s3, s1, s2, nil...] + if cache.signatures[0] != s4 { + t.Log(cache.signatures) + t.Fatal("insert s4 error") + } + if cache.signatures[1] != s3 { + t.Log(cache.signatures) + t.Fatal("pop s3 error") + } + if cache.signatures[2] != s1 || cache.signatures[3] != s2 { + t.Log(cache.signatures) + t.Fatal("s1~2 position error") + } +} + +func TestSignatureCache_2(t *testing.T) { + cache := newSignatureCache() + signs := make([]sign.PackedSignature, len(cache.signatures)+5) + for i := range signs { + newSign := new(sign.Signature) + _, _ = rand.Read(newSign[:]) + signs[i] = sign.PackedSignature{Signature: newSign} + } + err := cache.popOrInsert(nil, signs[:len(cache.signatures)]) + if !signatureEquals(cache.signatures[:], signs[:len(cache.signatures)]) { + t.Fatal("insert error") + } + if err != nil { + t.Fatal(err) + } + insert2 := signs[len(cache.signatures)-5:] + err = cache.popOrInsert(nil, insert2) + if err != nil { + t.Fatal(err) + } + if !signatureEquals(cache.signatures[:10], insert2) { + t.Fatal("insert and pop error") + } +} + +func signatureEquals(a []*sign.Signature, b []sign.PackedSignature) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i].Signature { + return false + } + } + return true +} diff --git a/bot/msg/chat.go b/bot/msg/chat.go index 5dd0ebd..644dec2 100644 --- a/bot/msg/chat.go +++ b/bot/msg/chat.go @@ -17,16 +17,25 @@ import ( ) type Manager struct { - c *bot.Client - p *basic.Player + c *bot.Client + p *basic.Player + pl *playerlist.PlayerList + + signatureCache } func New(c *bot.Client, p *basic.Player, pl *playerlist.PlayerList, events EventsHandler) *Manager { - attachPlayerMsg(c, p, pl, events.PlayerChatMessage) - return &Manager{c, p} + m := &Manager{ + c: c, + p: p, + pl: pl, + signatureCache: newSignatureCache(), + } + m.attachPlayerMsg(c, p, pl, events.PlayerChatMessage) + return m } -func attachPlayerMsg(c *bot.Client, p *basic.Player, _ *playerlist.PlayerList, handler func(msg chat.Message) error) { +func (m *Manager) attachPlayerMsg(c *bot.Client, p *basic.Player, pl *playerlist.PlayerList, handler func(msg chat.Message) error) { c.Events.AddListener( bot.PacketHandler{ Priority: 64, ID: packetid.ClientboundPlayerChat, @@ -35,7 +44,7 @@ func attachPlayerMsg(c *bot.Client, p *basic.Player, _ *playerlist.PlayerList, h sender pk.UUID index pk.VarInt signature pk.Option[sign.Signature, *sign.Signature] - body sign.MessageBody + body sign.PackedMessageBody unsignedContent pk.Option[chat.Message, *chat.Message] filter sign.FilterMask chatType chat.Type @@ -44,6 +53,18 @@ func attachPlayerMsg(c *bot.Client, p *basic.Player, _ *playerlist.PlayerList, h return err } + //senderInfo, ok := pl.PlayerInfos[uuid.UUID(sender)] + //if !ok { + // return bot.DisconnectErr(chat.TranslateMsg("multiplayer.disconnect.chat_validation_failed")) + //} + //senderInfo.ChatSession.Update() + //if err := senderInfo.ChatSession.Validate(); err != nil { + // return bot.DisconnectErr(chat.TranslateMsg("multiplayer.disconnect.chat_validation_failed")) + //} + // store signature into signatureCache + //if err := m.popOrInsert(signature.Pointer(), body.LastSeen); err != nil { + // return err + //} var content chat.Message if unsignedContent.Has { content = unsignedContent.Val diff --git a/bot/playerlist/playerlist.go b/bot/playerlist/playerlist.go index 298ee51..e7f6a91 100644 --- a/bot/playerlist/playerlist.go +++ b/bot/playerlist/playerlist.go @@ -2,12 +2,12 @@ package playerlist import ( "bytes" - "errors" "github.com/google/uuid" "github.com/Tnze/go-mc/bot" "github.com/Tnze/go-mc/chat" + "github.com/Tnze/go-mc/chat/sign" "github.com/Tnze/go-mc/data/packetid" pk "github.com/Tnze/go-mc/net/packet" "github.com/Tnze/go-mc/yggdrasil/user" @@ -74,7 +74,11 @@ func (pl *PlayerList) handlePlayerInfoUpdatePacket(p pk.Packet) error { } // initialize chat if action.Get(1) { - return errors.New("unsupported initialize chat yet") + var chatSession pk.Option[sign.Session, *sign.Session] + if _, err := chatSession.ReadFrom(r); err != nil { + return err + } + player.ChatSession = chatSession.Pointer() } // update gamemode if action.Get(2) { @@ -123,7 +127,7 @@ func (pl *PlayerList) handlePlayerInfoRemovePacket(p pk.Packet) error { type PlayerInfo struct { GameProfile - // chatSession + ChatSession *sign.Session Gamemode int32 Latency int32 Listed bool diff --git a/chat/sign/session.go b/chat/sign/session.go new file mode 100644 index 0000000..13c0e77 --- /dev/null +++ b/chat/sign/session.go @@ -0,0 +1,50 @@ +package sign + +import ( + "io" + + "github.com/google/uuid" + + "github.com/Tnze/go-mc/chat" + pk "github.com/Tnze/go-mc/net/packet" + "github.com/Tnze/go-mc/yggdrasil/user" +) + +type Message struct { + Link struct { + Index int + Sender uuid.UUID + Session uuid.UUID + } + Signature []byte + PackedMessageBody + Unsigned *chat.Message + FilterMask +} + +type Session struct { + sessionID uuid.UUID + publicKey user.PublicKey +} + +func (s Session) WriteTo(w io.Writer) (n int64, err error) { + n1, err := pk.UUID(s.sessionID).WriteTo(w) + if err != nil { + return n1, err + } + n2, err := s.publicKey.WriteTo(w) + return n1 + n2, err +} + +func (s *Session) ReadFrom(r io.Reader) (n int64, err error) { + n1, err := ((*pk.UUID)(&s.sessionID)).ReadFrom(r) + if err != nil { + return n1, err + } + n2, err := s.publicKey.ReadFrom(r) + return n1 + n2, err +} + +func (s *Session) Update(msg Message) bool { + panic("todo") +} diff --git a/chat/sign/sign.go b/chat/sign/sign.go index 3fcbfc3..f266568 100644 --- a/chat/sign/sign.go +++ b/chat/sign/sign.go @@ -4,18 +4,19 @@ import ( "io" "time" - pk "github.com/Tnze/go-mc/net/packet" "github.com/google/uuid" + + pk "github.com/Tnze/go-mc/net/packet" ) -type MessageBody struct { +type PackedMessageBody struct { PlainMsg string Timestamp time.Time Salt int64 LastSeen []PackedSignature } -func (m *MessageBody) WriteTo(w io.Writer) (n int64, err error) { +func (m *PackedMessageBody) WriteTo(w io.Writer) (n int64, err error) { return pk.Tuple{ pk.String(m.PlainMsg), pk.Long(m.Timestamp.UnixMilli()), @@ -24,7 +25,7 @@ func (m *MessageBody) WriteTo(w io.Writer) (n int64, err error) { }.WriteTo(w) } -func (m *MessageBody) ReadFrom(r io.Reader) (n int64, err error) { +func (m *PackedMessageBody) ReadFrom(r io.Reader) (n int64, err error) { var timestamp pk.Long n, err = pk.Tuple{ (*pk.String)(&m.PlainMsg), diff --git a/chat/sign/sign_test.go b/chat/sign/sign_test.go deleted file mode 100644 index b83d2df..0000000 --- a/chat/sign/sign_test.go +++ /dev/null @@ -1 +0,0 @@ -package sign diff --git a/net/packet/util.go b/net/packet/util.go index 536a99c..e6e2bbb 100644 --- a/net/packet/util.go +++ b/net/packet/util.go @@ -205,6 +205,14 @@ func (o *Option[T, P]) ReadFrom(r io.Reader) (n int64, err error) { return n1 + n2, err } +// Pointer returns the pointer of Val if Has is true, otherwise return nil. +func (o *Option[T, P]) Pointer() (p *T) { + if o.Has { + p = &o.Val + } + return +} + // OptionDecoder is basically same with [Option], but support [FieldDecoder] only. // This allowed wrapping a [FieldDecoder] type (which isn't a [FieldEncoder]) to an Option. type OptionDecoder[T any, P fieldPointer[T]] struct { diff --git a/server/auth/auth.go b/server/auth/auth.go index cfec992..74d89c8 100644 --- a/server/auth/auth.go +++ b/server/auth/auth.go @@ -24,6 +24,13 @@ import ( "github.com/Tnze/go-mc/yggdrasil/user" ) +// Deprecated: Moved to go-mc/yggdrasil/user, because go-mc/bot also needs them +type ( + Texture = user.Texture + Property = user.Property + PublicKey = user.PublicKey +) + const verifyTokenLen = 16 // Encrypt a connection, with authentication @@ -173,11 +180,6 @@ func twosComplement(p []byte) []byte { return p } -type ( - Texture = user.Texture - Property = user.Property -) - // Resp is the response of authentication type Resp struct { Name string diff --git a/server/gameplay.go b/server/gameplay.go index d445b4a..0923292 100644 --- a/server/gameplay.go +++ b/server/gameplay.go @@ -4,7 +4,8 @@ import ( _ "embed" "github.com/Tnze/go-mc/net" - "github.com/Tnze/go-mc/server/auth" + "github.com/Tnze/go-mc/yggdrasil/user" + "github.com/google/uuid" ) @@ -13,5 +14,5 @@ type GamePlay interface { // // Note: the connection will be closed after this function returned. // You don't need to close the connection, but to keep not returning while the player is playing. - AcceptPlayer(name string, id uuid.UUID, profilePubKey *auth.PublicKey, properties []auth.Property, protocol int32, conn *net.Conn) + AcceptPlayer(name string, id uuid.UUID, profilePubKey *user.PublicKey, properties []user.Property, protocol int32, conn *net.Conn) } diff --git a/server/login.go b/server/login.go index 498270c..877d367 100644 --- a/server/login.go +++ b/server/login.go @@ -13,13 +13,15 @@ import ( pk "github.com/Tnze/go-mc/net/packet" "github.com/Tnze/go-mc/offline" "github.com/Tnze/go-mc/server/auth" + "github.com/Tnze/go-mc/yggdrasil/user" + "github.com/google/uuid" ) // LoginHandler is used to handle player login process, that is, // from clientbound "LoginStart" packet to serverbound "LoginSuccess" packet. type LoginHandler interface { - AcceptLogin(conn *net.Conn, protocol int32) (name string, id uuid.UUID, profilePubKey *auth.PublicKey, properties []auth.Property, err error) + AcceptLogin(conn *net.Conn, protocol int32) (name string, id uuid.UUID, profilePubKey *user.PublicKey, properties []user.Property, err error) } // LoginChecker is the interface to check if a player is allowed to log in the server. @@ -87,7 +89,7 @@ func (d *MojangLoginHandler) getPrivateKey() (key *rsa.PrivateKey, err error) { */ // AcceptLogin implement LoginHandler for MojangLoginHandler -func (d *MojangLoginHandler) AcceptLogin(conn *net.Conn, protocol int32) (name string, id uuid.UUID, profilePubKey *auth.PublicKey, properties []auth.Property, err error) { +func (d *MojangLoginHandler) AcceptLogin(conn *net.Conn, protocol int32) (name string, id uuid.UUID, profilePubKey *user.PublicKey, properties []user.Property, err error) { // login start var p pk.Packet err = conn.ReadPacket(&p) diff --git a/yggdrasil/user/auth.go b/yggdrasil/user/property.go similarity index 100% rename from yggdrasil/user/auth.go rename to yggdrasil/user/property.go diff --git a/server/auth/pubkey.go b/yggdrasil/user/pubkey.go similarity index 99% rename from server/auth/pubkey.go rename to yggdrasil/user/pubkey.go index 15824fc..987007a 100644 --- a/server/auth/pubkey.go +++ b/yggdrasil/user/pubkey.go @@ -1,4 +1,4 @@ -package auth +package user import ( "crypto" diff --git a/server/auth/validator.go b/yggdrasil/user/validator.go similarity index 99% rename from server/auth/validator.go rename to yggdrasil/user/validator.go index 4a0dc23..f666b17 100644 --- a/server/auth/validator.go +++ b/yggdrasil/user/validator.go @@ -1,4 +1,4 @@ -package auth +package user import ( "crypto" diff --git a/server/auth/yggdrasil_session_pubkey.der b/yggdrasil/user/yggdrasil_session_pubkey.der similarity index 100% rename from server/auth/yggdrasil_session_pubkey.der rename to yggdrasil/user/yggdrasil_session_pubkey.der