This commit is contained in:
Tnze
2022-12-06 22:39:45 +08:00
12 changed files with 249 additions and 85 deletions

View File

@ -10,7 +10,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v1 uses: actions/setup-go@v1
with: with:
go-version: 1.18 go-version: 1.19
id: go id: go
- name: Check out code into the Go module directory - name: Check out code into the Go module directory

View File

@ -6,10 +6,7 @@ package bot
import ( import (
"context" "context"
"encoding/base64"
"encoding/pem"
"errors" "errors"
"io"
"net" "net"
"strconv" "strconv"
@ -79,22 +76,20 @@ func (c *Client) join(ctx context.Context, d *mcnet.Dialer, addr string) error {
} }
// Login Start // Login Start
c.KeyPair, err = user.GetOrFetchKeyPair(c.Auth.AsTk) 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) 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( err = c.Conn.WritePacket(pk.Marshal(
packetid.LoginStart, packetid.LoginStart,
pk.String(c.Auth.Name), pk.String(c.Auth.Name),
pk.Boolean(HasSignature), KeyPair,
pk.Opt{ PlayerUUID,
Has: HasSignature,
Field: keyPair(c.KeyPair),
},
pk.Boolean(HasPlayerUUID),
pk.Opt{
Has: HasPlayerUUID,
Field: pk.UUID(c.UUID),
},
)) ))
if err != nil { if err != nil {
return LoginErr{"login start", err} 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} return LoginErr{"Login Plugin", err}
} }
handler, ok := c.LoginPlugin[string(channel)] var PluginMessageData pk.Option[pk.PluginMessageData, *pk.PluginMessageData]
if ok { if handler, ok := c.LoginPlugin[string(channel)]; ok {
data, err = handler(data) PluginMessageData.Has = true
PluginMessageData.Val, err = handler(data)
if err != nil { if err != nil {
return LoginErr{"Login Plugin", err} 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( if err := c.Conn.WritePacket(pk.Marshal(
packetid.LoginPluginResponse, packetid.LoginPluginResponse,
msgid, pk.Boolean(ok), msgid, PluginMessageData,
pk.Opt{Has: ok, Field: data},
)); err != nil { )); err != nil {
return LoginErr{"login Plugin", err} 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 { type LoginErr struct {
Stage string Stage string
Err error Err error

View File

@ -11,6 +11,7 @@
package chat package chat
import ( import (
"bytes"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@ -113,6 +114,7 @@ func (m Message) MarshalJSON() ([]byte, error) {
// UnmarshalJSON decode json to Message // UnmarshalJSON decode json to Message
func (m *Message) UnmarshalJSON(raw []byte) (err error) { func (m *Message) UnmarshalJSON(raw []byte) (err error) {
raw = bytes.TrimSpace(raw)
if len(raw) == 0 { if len(raw) == 0 {
return io.EOF return io.EOF
} }

2
go.mod
View File

@ -1,6 +1,6 @@
module github.com/Tnze/go-mc module github.com/Tnze/go-mc
go 1.18 go 1.19
require ( require (
github.com/google/uuid v1.1.1 github.com/google/uuid v1.1.1

View File

@ -479,8 +479,8 @@ func (u *UUID) ReadFrom(r io.Reader) (n int64, err error) {
return int64(nn), err return int64(nn), err
} }
func (p *PluginMessageData) WriteTo(w io.Writer) (n int64, err error) { func (p PluginMessageData) WriteTo(w io.Writer) (n int64, err error) {
nn, err := w.Write(*p) nn, err := w.Write(p)
return int64(nn), err return int64(nn), err
} }

View File

@ -7,6 +7,12 @@ import (
"reflect" "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" // Ary is used to send or receive the packet field like "Array of X"
// which has a count must be known from the context. // 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. // 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. // 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) 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) array := reflect.ValueOf(a.Ary)
for array.Kind() == reflect.Ptr { for array.Kind() == reflect.Ptr {
array = array.Elem() array = array.Elem()
} }
Len := T(array.Len()) Len := LEN(array.Len())
if nn, err := any(&Len).(FieldEncoder).WriteTo(w); err != nil { if nn, err := any(&Len).(FieldEncoder).WriteTo(w); err != nil {
return n, err return n, err
} else { } else {
@ -45,8 +51,8 @@ func (a Ary[T]) WriteTo(w io.Writer) (n int64, err error) {
return n, nil return n, nil
} }
func (a Ary[T]) ReadFrom(r io.Reader) (n int64, err error) { func (a Ary[LEN]) ReadFrom(r io.Reader) (n int64, err error) {
var Len T var Len LEN
if nn, err := any(&Len).(FieldDecoder).ReadFrom(r); err != nil { if nn, err := any(&Len).(FieldDecoder).ReadFrom(r); err != nil {
return nn, err return nn, err
} else { } else {
@ -80,6 +86,12 @@ func Array(ary any) Field {
return Ary[VarInt]{Ary: ary} 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 { type Opt struct {
Has any // Pointer of bool, or `func() bool` Has any // Pointer of bool, or `func() bool`
Field any // FieldEncoder, FieldDecoder, `func() FieldEncoder`, `func() FieldDecoder` or `func() Field` 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 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 // 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) { func (t Tuple) WriteTo(w io.Writer) (n int64, err error) {

View File

@ -138,16 +138,16 @@ func ExampleOpt_ReadFrom() {
// WILL NOT BE READ, WILL NOT BE COVERED // 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() { 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 flag pk.Byte
var data pk.String var data pk.String
p := pk.Packet{Data: []byte{ p := pk.Packet{Data: []byte{
@ -188,3 +188,38 @@ func ExampleTuple_ReadFrom() {
panic(err) 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
}

View File

@ -17,7 +17,7 @@ type PublicKey struct {
Signature []byte 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) pubKeyEncoded, err := x509.MarshalPKIXPublicKey(p.PubKey)
if err != nil { if err != nil {
return 0, err return 0, err

View File

@ -4,6 +4,7 @@ import (
"container/list" "container/list"
"strconv" "strconv"
"sync" "sync"
"sync/atomic"
pk "github.com/Tnze/go-mc/net/packet" pk "github.com/Tnze/go-mc/net/packet"
) )
@ -28,21 +29,27 @@ func (s WritePacketError) Unwrap() error {
return s.Err return s.Err
} }
type PacketQueue struct { type PacketQueue interface {
queue *list.List Push(packet pk.Packet)
closed bool Pull() (packet pk.Packet, ok bool)
cond sync.Cond Close()
} }
func NewPacketQueue() (p *PacketQueue) { func NewPacketQueue() (p PacketQueue) {
p = &PacketQueue{ p = &LinkedListPacketQueue{
queue: list.New(), queue: list.New(),
cond: sync.Cond{L: new(sync.Mutex)}, cond: sync.Cond{L: new(sync.Mutex)},
} }
return p 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() p.cond.L.Lock()
if !p.closed { if !p.closed {
p.queue.PushBack(packet) p.queue.PushBack(packet)
@ -51,7 +58,7 @@ func (p *PacketQueue) Push(packet pk.Packet) {
p.cond.L.Unlock() 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() p.cond.L.Lock()
defer p.cond.L.Unlock() defer p.cond.L.Unlock()
for p.queue.Front() == nil && !p.closed { for p.queue.Front() == nil && !p.closed {
@ -65,9 +72,36 @@ func (p *PacketQueue) Pull() (packet pk.Packet, ok bool) {
return return
} }
func (p *PacketQueue) Close() { func (p *LinkedListPacketQueue) Close() {
p.cond.L.Lock() p.cond.L.Lock()
p.closed = true p.closed = true
p.cond.Broadcast() p.cond.Broadcast()
p.cond.L.Unlock() 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)
}

View File

@ -118,7 +118,6 @@ func (k *KeepAlive) tickPlayer(c KeepAliveClient) {
elem, ok := k.listIndex[c] elem, ok := k.listIndex[c]
if !ok { if !ok {
panic(errors.New("keepalive: fail to tick player: client not found")) panic(errors.New("keepalive: fail to tick player: client not found"))
return
} }
if elem.Prev() == nil { if elem.Prev() == nil {
if !k.waitTimer.Stop() { if !k.waitTimer.Stop() {
@ -156,7 +155,6 @@ func keepAliveSetTimer(l *list.List, timer *time.Timer, interval time.Duration)
} }
} }
timer.Reset(interval) timer.Reset(interval)
return
} }
type keepAliveItem struct { type keepAliveItem struct {

View File

@ -64,21 +64,13 @@ func (d *MojangLoginHandler) AcceptLogin(conn *net.Conn, protocol int32) (name s
} }
var ( var (
hasPubKey pk.Boolean pubKey pk.Option[auth.PublicKey, *auth.PublicKey]
pubKey auth.PublicKey profileUUID pk.Option[pk.UUID, *pk.UUID] // ignored
hasUUID pk.Boolean
profileUUID pk.UUID // ignored
) )
err = p.Scan( err = p.Scan(
(*pk.String)(&name), // decode username as pk.String (*pk.String)(&name), // decode username as pk.String
&hasPubKey, pk.Opt{ &pubKey,
Has: &hasPubKey, &profileUUID,
Field: &pubKey,
},
&hasUUID, pk.Opt{
Has: &hasUUID,
Field: &profileUUID,
},
) )
if err != nil { if err != nil {
return return
@ -86,12 +78,12 @@ func (d *MojangLoginHandler) AcceptLogin(conn *net.Conn, protocol int32) (name s
// auth // auth
if d.OnlineMode { if d.OnlineMode {
if hasPubKey { if pubKey.Has {
if !pubKey.Verify() { if !pubKey.Val.Verify() {
err = LoginFailErr{reason: chat.TranslateMsg("multiplayer.disconnect.invalid_public_key_signature")} err = LoginFailErr{reason: chat.TranslateMsg("multiplayer.disconnect.invalid_public_key_signature")}
return return
} }
profilePubKey = &pubKey profilePubKey = &pubKey.Val
} else if d.EnforceSecureProfile { } else if d.EnforceSecureProfile {
err = LoginFailErr{reason: chat.TranslateMsg("multiplayer.disconnect.missing_public_key")} err = LoginFailErr{reason: chat.TranslateMsg("multiplayer.disconnect.missing_public_key")}
return return
@ -99,7 +91,7 @@ func (d *MojangLoginHandler) AcceptLogin(conn *net.Conn, protocol int32) (name s
var resp *auth.Resp var resp *auth.Resp
// Auth, Encrypt // Auth, Encrypt
resp, err = auth.Encrypt(conn, name, pubKey.PubKey) resp, err = auth.Encrypt(conn, name, pubKey.Val.PubKey)
if err != nil { if err != nil {
return return
} }

View File

@ -1,10 +1,16 @@
package user package user
import ( import (
"encoding/base64"
"encoding/json" "encoding/json"
"encoding/pem"
"errors"
"fmt" "fmt"
"io"
"net/http" "net/http"
"time" "time"
pk "github.com/Tnze/go-mc/net/packet"
) )
var ServicesURL = "https://api.minecraftservices.com" var ServicesURL = "https://api.minecraftservices.com"
@ -21,6 +27,22 @@ type KeyPairResp struct {
RefreshedAfter time.Time `json:"refreshedAfter"` 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) { func GetOrFetchKeyPair(accessToken string) (KeyPairResp, error) {
return fetchKeyPair(accessToken) // TODO: cache return fetchKeyPair(accessToken) // TODO: cache
} }