diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 89b0ded..1a2fe57 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -10,7 +10,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v1 with: - go-version: 1.18 + go-version: 1.19 id: go - name: Check out code into the Go module directory diff --git a/bot/mcbot.go b/bot/mcbot.go index 22dff86..2411d40 100644 --- a/bot/mcbot.go +++ b/bot/mcbot.go @@ -6,10 +6,7 @@ package bot import ( "context" - "encoding/base64" - "encoding/pem" "errors" - "io" "net" "strconv" @@ -79,22 +76,20 @@ func (c *Client) join(ctx context.Context, d *mcnet.Dialer, addr string) error { } // Login Start c.KeyPair, err = user.GetOrFetchKeyPair(c.Auth.AsTk) - HasSignature := err == nil + KeyPair := pk.OptionEncoder[user.KeyPairResp]{ + Has: err == nil, + Val: c.KeyPair, + } c.UUID, err = uuid.Parse(c.Auth.UUID) - HasPlayerUUID := err == nil + PlayerUUID := pk.Option[pk.UUID, *pk.UUID]{ + Has: err == nil, + Val: pk.UUID(c.UUID), + } err = c.Conn.WritePacket(pk.Marshal( packetid.LoginStart, pk.String(c.Auth.Name), - pk.Boolean(HasSignature), - pk.Opt{ - Has: HasSignature, - Field: keyPair(c.KeyPair), - }, - pk.Boolean(HasPlayerUUID), - pk.Opt{ - Has: HasPlayerUUID, - Field: pk.UUID(c.UUID), - }, + KeyPair, + PlayerUUID, )) if err != nil { return LoginErr{"login start", err} @@ -148,9 +143,10 @@ func (c *Client) join(ctx context.Context, d *mcnet.Dialer, addr string) error { return LoginErr{"Login Plugin", err} } - handler, ok := c.LoginPlugin[string(channel)] - if ok { - data, err = handler(data) + var PluginMessageData pk.Option[pk.PluginMessageData, *pk.PluginMessageData] + if handler, ok := c.LoginPlugin[string(channel)]; ok { + PluginMessageData.Has = true + PluginMessageData.Val, err = handler(data) if err != nil { return LoginErr{"Login Plugin", err} } @@ -158,8 +154,7 @@ func (c *Client) join(ctx context.Context, d *mcnet.Dialer, addr string) error { if err := c.Conn.WritePacket(pk.Marshal( packetid.LoginPluginResponse, - msgid, pk.Boolean(ok), - pk.Opt{Has: ok, Field: data}, + msgid, PluginMessageData, )); err != nil { return LoginErr{"login Plugin", err} } @@ -167,24 +162,6 @@ func (c *Client) join(ctx context.Context, d *mcnet.Dialer, addr string) error { } } -type keyPair user.KeyPairResp - -func (k keyPair) WriteTo(w io.Writer) (int64, error) { - block, _ := pem.Decode([]byte(k.KeyPair.PublicKey)) - if block == nil { - return 0, errors.New("pem decode error: no data is found") - } - signature, err := base64.StdEncoding.DecodeString(k.PublicKeySignature) - if err != nil { - return 0, err - } - return pk.Tuple{ - pk.Long(k.ExpiresAt.UnixMilli()), - pk.ByteArray(block.Bytes), - pk.ByteArray(signature), - }.WriteTo(w) -} - type LoginErr struct { Stage string Err error diff --git a/chat/message.go b/chat/message.go index 2195900..f8cab08 100644 --- a/chat/message.go +++ b/chat/message.go @@ -11,6 +11,7 @@ package chat import ( + "bytes" "encoding/json" "errors" "fmt" @@ -113,6 +114,7 @@ func (m Message) MarshalJSON() ([]byte, error) { // UnmarshalJSON decode json to Message func (m *Message) UnmarshalJSON(raw []byte) (err error) { + raw = bytes.TrimSpace(raw) if len(raw) == 0 { return io.EOF } diff --git a/go.mod b/go.mod index f1c4a5d..e3cc9ba 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/Tnze/go-mc -go 1.18 +go 1.19 require ( github.com/google/uuid v1.1.1 diff --git a/net/packet/types.go b/net/packet/types.go index 4d522f1..37eb68f 100644 --- a/net/packet/types.go +++ b/net/packet/types.go @@ -479,8 +479,8 @@ func (u *UUID) ReadFrom(r io.Reader) (n int64, err error) { return int64(nn), err } -func (p *PluginMessageData) WriteTo(w io.Writer) (n int64, err error) { - nn, err := w.Write(*p) +func (p PluginMessageData) WriteTo(w io.Writer) (n int64, err error) { + nn, err := w.Write(p) return int64(nn), err } diff --git a/net/packet/util.go b/net/packet/util.go index 2ef0d17..e0c7bc2 100644 --- a/net/packet/util.go +++ b/net/packet/util.go @@ -7,6 +7,12 @@ import ( "reflect" ) +var ( + _ Field = (*Option[VarInt, *VarInt])(nil) + _ Field = (*Ary[VarInt])(nil) + _ Field = Tuple(nil) +) + // Ary is used to send or receive the packet field like "Array of X" // which has a count must be known from the context. // @@ -19,16 +25,16 @@ import ( // So it's allowed to directly set an integer type Len, but not a pointer. // // Note that Ary DO read or write the Len. You aren't need to do so by your self. -type Ary[T VarInt | VarLong | Byte | UnsignedByte | Short | UnsignedShort | Int | Long] struct { +type Ary[LEN VarInt | VarLong | Byte | UnsignedByte | Short | UnsignedShort | Int | Long] struct { Ary interface{} // Slice or Pointer of Slice of FieldEncoder, FieldDecoder or both (Field) } -func (a Ary[T]) WriteTo(w io.Writer) (n int64, err error) { +func (a Ary[LEN]) WriteTo(w io.Writer) (n int64, err error) { array := reflect.ValueOf(a.Ary) for array.Kind() == reflect.Ptr { array = array.Elem() } - Len := T(array.Len()) + Len := LEN(array.Len()) if nn, err := any(&Len).(FieldEncoder).WriteTo(w); err != nil { return n, err } else { @@ -45,8 +51,8 @@ func (a Ary[T]) WriteTo(w io.Writer) (n int64, err error) { return n, nil } -func (a Ary[T]) ReadFrom(r io.Reader) (n int64, err error) { - var Len T +func (a Ary[LEN]) ReadFrom(r io.Reader) (n int64, err error) { + var Len LEN if nn, err := any(&Len).(FieldDecoder).ReadFrom(r); err != nil { return nn, err } else { @@ -80,6 +86,12 @@ func Array(ary any) Field { return Ary[VarInt]{Ary: ary} } +// Opt is a optional [Field] which sending/receiving or not is depending on its Has field. +// When calling `WriteTo()` or `ReadFrom()`, if Has is true, the Field's `WriteTo` or `ReadFrom()` is called. +// Otherwise, it does nothing and return 0 and nil. +// +// The different between [Opt] and [Option] is that [Opt] does NOT read or write the Has field for you. +// Which should be cared. type Opt struct { Has any // Pointer of bool, or `func() bool` Field any // FieldEncoder, FieldDecoder, `func() FieldEncoder`, `func() FieldDecoder` or `func() Field` @@ -133,7 +145,99 @@ func (o Opt) ReadFrom(r io.Reader) (int64, error) { return 0, nil } -type Tuple []interface{} // FieldEncoder, FieldDecoder or both (Field) +type fieldPointer[T any] interface { + *T + FieldDecoder +} + +// Option is a helper type for encoding/decoding these kind of packet: +// +// +-----------+------------+----------------------------------------- + +// | Name | Type | Notes | +// +-----------+------------+------------------------------------------+ +// | Has Value | Boolean | Whether or not the Value should be sent. | +// +-----------+------------+------------------------------------------+ +// | Value | Optional T | Only exist when Has Value is true. | +// +-----------+------------+------------------------------------------+ +// +// # Usage +// +// `Option[T]` implements [FieldEncoder] and `*Option[T]` implements [FieldDecoder]. +// That is, you can call `WriteTo()` and `ReadFrom()` methods on it. +// +// var optStr Option[String] +// n, err := optStr.ReadFrom(r) +// if err != nil { +// // ... +// } +// if optStr.Has { +// fmt.Println(optStr.Val) +// } +// +// # Notes +// +// Currently we have to repeat T in the type arguments. +// +// var opt Option[String, *String] +// +// Constraint type will inference makes it less awkward in the future. +// See: https://github.com/golang/go/issues/54469 +type Option[T FieldEncoder, P fieldPointer[T]] struct { + Has Boolean + Val T +} + +func (o Option[T, P]) WriteTo(w io.Writer) (n int64, err error) { + n1, err := o.Has.WriteTo(w) + if err != nil || !o.Has { + return n1, err + } + n2, err := o.Val.WriteTo(w) + return n1 + n2, err +} + +func (o *Option[T, P]) ReadFrom(r io.Reader) (n int64, err error) { + n1, err := o.Has.ReadFrom(r) + if err != nil || !o.Has { + return n1, err + } + n2, err := P(&o.Val).ReadFrom(r) + return n1 + n2, err +} + +// OptionDecoder is basiclly 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 { + Has Boolean + Val T +} + +func (o *OptionDecoder[T, P]) ReadFrom(r io.Reader) (n int64, err error) { + n1, err := o.Has.ReadFrom(r) + if err != nil || !o.Has { + return n1, err + } + n2, err := P(&o.Val).ReadFrom(r) + return n1 + n2, err +} + +// OptionEncoder is basiclly same with [Option], but support [FieldEncoder] only. +// This allowed wrapping a [FieldEncoder] type (which isn't a [FieldDecoder]) to an Option. +type OptionEncoder[T FieldEncoder] struct { + Has Boolean + Val T +} + +func (o OptionEncoder[T]) WriteTo(w io.Writer) (n int64, err error) { + n1, err := o.Has.WriteTo(w) + if err != nil || !o.Has { + return n1, err + } + n2, err := o.Val.WriteTo(w) + return n1 + n2, err +} + +type Tuple []any // FieldEncoder, FieldDecoder or both (Field) // WriteTo write Tuple to io.Writer, panic when any of filed don't implement FieldEncoder func (t Tuple) WriteTo(w io.Writer) (n int64, err error) { diff --git a/net/packet/util_test.go b/net/packet/util_test.go index dc147bf..101d3f1 100644 --- a/net/packet/util_test.go +++ b/net/packet/util_test.go @@ -138,16 +138,16 @@ func ExampleOpt_ReadFrom() { // WILL NOT BE READ, WILL NOT BE COVERED } +// As an example, we define this packet as this: +// +------+-----------------+----------------------------------+ +// | Name | Type | Notes | +// +------+-----------------+----------------------------------+ +// | Flag | Unsigned Byte | Odd if the following is present. | +// +------+-----------------+----------------------------------+ +// | User | Optional String | The player's name. | +// +------+-----------------+----------------------------------+ +// So we need a function to decide if the User field is present. func ExampleOpt_ReadFrom_func() { - // As an example, we define this packet as this: - // +------+-----------------+----------------------------------+ - // | Name | Type | Notes | - // +------+-----------------+----------------------------------+ - // | Flag | Unsigned Byte | Odd if the following is present. | - // +------+-----------------+----------------------------------+ - // | User | Optional String | The player's name. | - // +------+-----------------+----------------------------------+ - // So we need a function to decide if the User field is present. var flag pk.Byte var data pk.String p := pk.Packet{Data: []byte{ @@ -188,3 +188,38 @@ func ExampleTuple_ReadFrom() { panic(err) } } + +// As an example, we define this packet as this: +// +------+-----------------+-----------------------------------+ +// | Name | Type | Notes | +// +------+-----------------+-----------------------------------+ +// | Has | Boolean | True if the following is present. | +// +------+-----------------+-----------------------------------+ +// | User | Optional String | The player's name. | +// +------+-----------------+-----------------------------------+ +// So we need a function to decide if the User field is present. +func ExampleOption_ReadFrom_func() { + p1 := pk.Packet{Data: []byte{ + 0x01, // pk.Boolean(true) + 4, 'T', 'n', 'z', 'e', // pk.String("Tnze") + }} + p2 := pk.Packet{Data: []byte{ + 0x00, // pk.Boolean(false) + // empty + }} + + var User1, User2 pk.Option[pk.String, *pk.String] + if err := p1.Scan(&User1); err != nil { + panic(err) + } + if err := p2.Scan(&User2); err != nil { + panic(err) + } + + fmt.Println(User1.Has, User1.Val) + fmt.Println(User2.Has, User2.Val) + + // Output: + // true Tnze + // false +} diff --git a/server/auth/pubkey.go b/server/auth/pubkey.go index 0001b8c..15824fc 100644 --- a/server/auth/pubkey.go +++ b/server/auth/pubkey.go @@ -17,7 +17,7 @@ type PublicKey struct { Signature []byte } -func (p *PublicKey) WriteTo(w io.Writer) (n int64, err error) { +func (p PublicKey) WriteTo(w io.Writer) (n int64, err error) { pubKeyEncoded, err := x509.MarshalPKIXPublicKey(p.PubKey) if err != nil { return 0, err diff --git a/server/client.go b/server/client.go index 8ae1e9e..244d8bd 100644 --- a/server/client.go +++ b/server/client.go @@ -4,6 +4,7 @@ import ( "container/list" "strconv" "sync" + "sync/atomic" pk "github.com/Tnze/go-mc/net/packet" ) @@ -28,21 +29,27 @@ func (s WritePacketError) Unwrap() error { return s.Err } -type PacketQueue struct { - queue *list.List - closed bool - cond sync.Cond +type PacketQueue interface { + Push(packet pk.Packet) + Pull() (packet pk.Packet, ok bool) + Close() } -func NewPacketQueue() (p *PacketQueue) { - p = &PacketQueue{ +func NewPacketQueue() (p PacketQueue) { + p = &LinkedListPacketQueue{ queue: list.New(), cond: sync.Cond{L: new(sync.Mutex)}, } return p } -func (p *PacketQueue) Push(packet pk.Packet) { +type LinkedListPacketQueue struct { + queue *list.List + closed bool + cond sync.Cond +} + +func (p *LinkedListPacketQueue) Push(packet pk.Packet) { p.cond.L.Lock() if !p.closed { p.queue.PushBack(packet) @@ -51,7 +58,7 @@ func (p *PacketQueue) Push(packet pk.Packet) { p.cond.L.Unlock() } -func (p *PacketQueue) Pull() (packet pk.Packet, ok bool) { +func (p *LinkedListPacketQueue) Pull() (packet pk.Packet, ok bool) { p.cond.L.Lock() defer p.cond.L.Unlock() for p.queue.Front() == nil && !p.closed { @@ -65,9 +72,36 @@ func (p *PacketQueue) Pull() (packet pk.Packet, ok bool) { return } -func (p *PacketQueue) Close() { +func (p *LinkedListPacketQueue) Close() { p.cond.L.Lock() p.closed = true p.cond.Broadcast() p.cond.L.Unlock() } + +type ChannelPacketQueue struct { + c chan pk.Packet + closed atomic.Bool +} + +func (c ChannelPacketQueue) Push(packet pk.Packet) { + if c.closed.Load() { + return + } + select { + case c.c <- packet: + default: + c.closed.Store(true) + } +} + +func (c ChannelPacketQueue) Pull() (packet pk.Packet, ok bool) { + if !c.closed.Load() { + packet, ok = <-c.c + } + return +} + +func (c ChannelPacketQueue) Close() { + c.closed.Store(true) +} diff --git a/server/keepalive.go b/server/keepalive.go index 826f629..861c18b 100644 --- a/server/keepalive.go +++ b/server/keepalive.go @@ -118,7 +118,6 @@ func (k *KeepAlive) tickPlayer(c KeepAliveClient) { elem, ok := k.listIndex[c] if !ok { panic(errors.New("keepalive: fail to tick player: client not found")) - return } if elem.Prev() == nil { if !k.waitTimer.Stop() { @@ -156,7 +155,6 @@ func keepAliveSetTimer(l *list.List, timer *time.Timer, interval time.Duration) } } timer.Reset(interval) - return } type keepAliveItem struct { diff --git a/server/login.go b/server/login.go index a256a62..acd0cf3 100644 --- a/server/login.go +++ b/server/login.go @@ -64,21 +64,13 @@ func (d *MojangLoginHandler) AcceptLogin(conn *net.Conn, protocol int32) (name s } var ( - hasPubKey pk.Boolean - pubKey auth.PublicKey - hasUUID pk.Boolean - profileUUID pk.UUID // ignored + pubKey pk.Option[auth.PublicKey, *auth.PublicKey] + profileUUID pk.Option[pk.UUID, *pk.UUID] // ignored ) err = p.Scan( (*pk.String)(&name), // decode username as pk.String - &hasPubKey, pk.Opt{ - Has: &hasPubKey, - Field: &pubKey, - }, - &hasUUID, pk.Opt{ - Has: &hasUUID, - Field: &profileUUID, - }, + &pubKey, + &profileUUID, ) if err != nil { return @@ -86,12 +78,12 @@ func (d *MojangLoginHandler) AcceptLogin(conn *net.Conn, protocol int32) (name s // auth if d.OnlineMode { - if hasPubKey { - if !pubKey.Verify() { + if pubKey.Has { + if !pubKey.Val.Verify() { err = LoginFailErr{reason: chat.TranslateMsg("multiplayer.disconnect.invalid_public_key_signature")} return } - profilePubKey = &pubKey + profilePubKey = &pubKey.Val } else if d.EnforceSecureProfile { err = LoginFailErr{reason: chat.TranslateMsg("multiplayer.disconnect.missing_public_key")} return @@ -99,7 +91,7 @@ func (d *MojangLoginHandler) AcceptLogin(conn *net.Conn, protocol int32) (name s var resp *auth.Resp // Auth, Encrypt - resp, err = auth.Encrypt(conn, name, pubKey.PubKey) + resp, err = auth.Encrypt(conn, name, pubKey.Val.PubKey) if err != nil { return } diff --git a/yggdrasil/user/user.go b/yggdrasil/user/user.go index e86b204..b062ea2 100644 --- a/yggdrasil/user/user.go +++ b/yggdrasil/user/user.go @@ -1,10 +1,16 @@ package user import ( + "encoding/base64" "encoding/json" + "encoding/pem" + "errors" "fmt" + "io" "net/http" "time" + + pk "github.com/Tnze/go-mc/net/packet" ) var ServicesURL = "https://api.minecraftservices.com" @@ -21,6 +27,22 @@ type KeyPairResp struct { RefreshedAfter time.Time `json:"refreshedAfter"` } +func (k KeyPairResp) WriteTo(w io.Writer) (int64, error) { + block, _ := pem.Decode([]byte(k.KeyPair.PublicKey)) + if block == nil { + return 0, errors.New("pem decode error: no data is found") + } + signature, err := base64.StdEncoding.DecodeString(k.PublicKeySignature) + if err != nil { + return 0, err + } + return pk.Tuple{ + pk.Long(k.ExpiresAt.UnixMilli()), + pk.ByteArray(block.Bytes), + pk.ByteArray(signature), + }.WriteTo(w) +} + func GetOrFetchKeyPair(accessToken string) (KeyPairResp, error) { return fetchKeyPair(accessToken) // TODO: cache }