diff --git a/go.mod b/go.mod index 3f314f0e..55d44380 100644 --- a/go.mod +++ b/go.mod @@ -9,18 +9,18 @@ require ( github.com/go-jose/go-jose/v3 v3.0.3 github.com/golang/snappy v0.0.4 github.com/google/uuid v1.6.0 - github.com/klauspost/compress v1.17.9 + github.com/klauspost/compress v1.17.11 github.com/muhammadmuzzammil1998/jsonc v1.0.0 github.com/pelletier/go-toml v1.9.5 - github.com/sandertv/go-raknet v1.14.1 - golang.org/x/net v0.26.0 - golang.org/x/oauth2 v0.21.0 - golang.org/x/text v0.16.0 + github.com/sandertv/go-raknet v1.14.2 + golang.org/x/net v0.30.0 + golang.org/x/oauth2 v0.23.0 + golang.org/x/text v0.19.0 ) require ( github.com/df-mc/atomic v1.10.0 // indirect - golang.org/x/crypto v0.24.0 // indirect + golang.org/x/crypto v0.28.0 // indirect golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 // indirect golang.org/x/image v0.17.0 // indirect ) diff --git a/go.sum b/go.sum index b6a2670b..286c1c2b 100644 --- a/go.sum +++ b/go.sum @@ -13,8 +13,8 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= -github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/muhammadmuzzammil1998/jsonc v1.0.0 h1:8o5gBQn4ZA3NBA9DlTujCj2a4w0tqWrPVjDwhzkgTIs= github.com/muhammadmuzzammil1998/jsonc v1.0.0/go.mod h1:saF2fIVw4banK0H4+/EuqfFLpRnoy5S+ECwTOCcRcSU= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= @@ -32,8 +32,8 @@ go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= @@ -46,10 +46,10 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= -golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= -golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -72,8 +72,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/minecraft/conn.go b/minecraft/conn.go index 4ced467a..1e5543fc 100644 --- a/minecraft/conn.go +++ b/minecraft/conn.go @@ -20,7 +20,7 @@ import ( "github.com/sandertv/gophertunnel/minecraft/resource" "github.com/sandertv/gophertunnel/minecraft/text" "io" - "log" + "log/slog" "net" "strings" "sync" @@ -54,7 +54,7 @@ type Conn struct { close chan struct{} conn net.Conn - log *log.Logger + log *slog.Logger authEnabled bool proto Protocol @@ -148,7 +148,7 @@ type Conn struct { // Minecraft packets to that net.Conn. // newConn accepts a private key which will be used to identify the connection. If a nil key is passed, the // key is generated. -func newConn(netConn net.Conn, key *ecdsa.PrivateKey, log *log.Logger, proto Protocol, flushRate time.Duration, limits bool) *Conn { +func newConn(netConn net.Conn, key *ecdsa.PrivateKey, log *slog.Logger, proto Protocol, flushRate time.Duration, limits bool) *Conn { conn := &Conn{ enc: packet.NewEncoder(netConn), dec: packet.NewDecoder(netConn), @@ -159,7 +159,7 @@ func newConn(netConn net.Conn, key *ecdsa.PrivateKey, log *log.Logger, proto Pro spawn: make(chan struct{}), conn: netConn, privateKey: key, - log: log, + log: log.With("raddr", netConn.RemoteAddr().String()), hdr: &packet.Header{}, proto: proto, readerLimits: limits, @@ -360,7 +360,7 @@ func (conn *Conn) ReadPacket() (pk packet.Packet, err error) { if data, ok := conn.takeDeferredPacket(); ok { pk, err := data.decode(conn) if err != nil { - conn.log.Println(err) + conn.log.Error("read packet: " + err.Error()) return conn.ReadPacket() } if len(pk) == 0 { @@ -380,7 +380,7 @@ func (conn *Conn) ReadPacket() (pk packet.Packet, err error) { case data := <-conn.packets: pk, err := data.decode(conn) if err != nil { - conn.log.Println(err) + conn.log.Error("read packet: " + err.Error()) return conn.ReadPacket() } if len(pk) == 0 { @@ -802,13 +802,12 @@ func (conn *Conn) handleClientToServerHandshake() error { } pk := &packet.ResourcePacksInfo{TexturePackRequired: conn.texturePacksRequired} for _, pack := range conn.resourcePacks { - if pack.DownloadURL() != "" { - pk.PackURLs = append(pk.PackURLs, protocol.PackURL{ - UUIDVersion: fmt.Sprintf("%s_%s", pack.UUID(), pack.Version()), - URL: pack.DownloadURL(), - }) + texturePack := protocol.TexturePackInfo{ + UUID: pack.UUID(), + Version: pack.Version(), + Size: uint64(pack.Len()), + DownloadURL: pack.DownloadURL(), } - texturePack := protocol.TexturePackInfo{UUID: pack.UUID(), Version: pack.Version(), Size: uint64(pack.Len())} if pack.Encrypted() { texturePack.ContentKey = pack.ContentKey() texturePack.ContentIdentity = pack.Manifest().Header.UUID @@ -891,7 +890,7 @@ func (conn *Conn) handleResourcePacksInfo(pk *packet.ResourcePacksInfo) error { for index, pack := range pk.TexturePacks { if _, ok := conn.packQueue.downloadingPacks[pack.UUID]; ok { - conn.log.Printf("handle ResourcePacksInfo: duplicate texture pack (UUID=%v)\n", pack.UUID) + conn.log.Warn("handle ResourcePacksInfo: duplicate texture pack", "UUID", pack.UUID) conn.packQueue.packAmount-- continue } @@ -935,9 +934,9 @@ func (conn *Conn) handleResourcePackStack(pk *packet.ResourcePackStack) error { for _, pack := range pk.TexturePacks { for i, behaviourPack := range pk.BehaviourPacks { if pack.UUID == behaviourPack.UUID { - // We had a behaviour pack with the same UUID as the texture pack, so we drop the texture + // We had a behaviour pack with the same UUID as the texture pack, so we drop the behaviour // pack and log it. - conn.log.Printf("handle ResourcePackStack: dropping behaviour pack (UUID=%v) due to a texture pack with the same UUID\n", pack.UUID) + conn.log.Warn("handle ResourcePackStack: dropping behaviour pack due to a texture pack with the same UUID", "UUID", pack.UUID) pk.BehaviourPacks = append(pk.BehaviourPacks[:i], pk.BehaviourPacks[i+1:]...) } } @@ -1104,12 +1103,12 @@ func (conn *Conn) handleResourcePackDataInfo(pk *packet.ResourcePackDataInfo) er if !ok { // We either already downloaded the pack or we got sent an invalid UUID, that did not match any pack // sent in the ResourcePacksInfo packet. - return fmt.Errorf("unknown pack (UUID=%v)", id) + return fmt.Errorf("handle ResourcePackDataInfo: unknown pack (UUID=%v)", id) } if pack.size != pk.Size { // Size mismatch: The ResourcePacksInfo packet had a size for the pack that did not match with the // size sent here. - conn.log.Printf("pack (UUID=%v) had a different size in ResourcePacksInfo than in ResourcePackDataInfo\n", id) + conn.log.Warn("handle ResourcePackDataInfo: pack had a different size in ResourcePacksInfo than in ResourcePackDataInfo", "UUID", id) pack.size = pk.Size } @@ -1145,13 +1144,13 @@ func (conn *Conn) handleResourcePackDataInfo(pk *packet.ResourcePackDataInfo) er defer conn.packMu.Unlock() if pack.buf.Len() != int(pack.size) { - conn.log.Printf("incorrect resource pack size (UUID=%v): expected %v, got %v\n", id, pack.size, pack.buf.Len()) + conn.log.Error(fmt.Sprintf("download resource pack: incorrect resource pack size: expected %v, got %v", pack.size, pack.buf.Len()), "UUID", id) return } // First parse the resource pack from the total byte buffer we obtained. newPack, err := resource.Read(pack.buf) if err != nil { - conn.log.Printf("invalid full resource pack data (UUID=%v): %v\n", id, err) + conn.log.Error("download resource pack: invalid full resource pack data: "+err.Error(), "UUID", id) return } conn.packQueue.packAmount-- diff --git a/minecraft/dial.go b/minecraft/dial.go index 854c7aff..f9d58702 100644 --- a/minecraft/dial.go +++ b/minecraft/dial.go @@ -15,14 +15,14 @@ import ( "github.com/go-jose/go-jose/v3/jwt" "github.com/google/uuid" "github.com/sandertv/gophertunnel/minecraft/auth" + "github.com/sandertv/gophertunnel/minecraft/internal" "github.com/sandertv/gophertunnel/minecraft/protocol" "github.com/sandertv/gophertunnel/minecraft/protocol/login" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" "golang.org/x/oauth2" - "log" + "log/slog" "math/rand" "net" - "os" "strconv" "strings" "time" @@ -31,9 +31,9 @@ import ( // Dialer allows specifying specific settings for connection to a Minecraft server. // The zero value of Dialer is used for the package level Dial function. type Dialer struct { - // ErrorLog is a log.Logger that errors that occur during packet handling of servers are written to. By - // default, ErrorLog is set to one equal to the global logger. - ErrorLog *log.Logger + // ErrorLog is a log.Logger that errors that occur during packet handling of + // servers are written to. By default, errors are not logged. + ErrorLog *slog.Logger // ClientData is the client data used to login to the server with. It includes fields such as the skin, // locale and UUIDs unique to the client. If empty, a default is sent produced using defaultClientData(). @@ -148,8 +148,18 @@ func (d Dialer) DialTimeout(network, address string, timeout time.Duration) (*Co // typically "raknet". A Conn is returned which may be used to receive packets from and send packets to. // If a connection is not established before the context passed is cancelled, DialContext returns an error. func (d Dialer) DialContext(ctx context.Context, network, address string) (conn *Conn, err error) { - key, _ := ecdsa.GenerateKey(elliptic.P384(), cryptorand.Reader) + if d.ErrorLog == nil { + d.ErrorLog = slog.New(internal.DiscardHandler{}) + } + d.ErrorLog = d.ErrorLog.With("src", "dialer") + if d.Protocol == nil { + d.Protocol = DefaultProtocol + } + if d.FlushRate == 0 { + d.FlushRate = time.Second / 20 + } + key, _ := ecdsa.GenerateKey(elliptic.P384(), cryptorand.Reader) var chainData string if d.TokenSource != nil { chainData, err = authChain(ctx, d.TokenSource, key) @@ -158,19 +168,10 @@ func (d Dialer) DialContext(ctx context.Context, network, address string) (conn } d.IdentityData = readChainIdentityData([]byte(chainData)) } - if d.ErrorLog == nil { - d.ErrorLog = log.New(os.Stderr, "", log.LstdFlags) - } - if d.Protocol == nil { - d.Protocol = DefaultProtocol - } - if d.FlushRate == 0 { - d.FlushRate = time.Second / 20 - } - n, ok := networkByID(network) + n, ok := networkByID(network, d.ErrorLog) if !ok { - return nil, fmt.Errorf("dial: no network under id %v", network) + return nil, &net.OpError{Op: "dial", Net: "minecraft", Err: fmt.Errorf("dial: no network under id %v", network)} } var pong []byte @@ -218,34 +219,35 @@ func (d Dialer) DialContext(ctx context.Context, network, address string) (conn conn.identityData = identityData } - l, c := make(chan struct{}), make(chan struct{}) - go listenConn(conn, d.ErrorLog, l, c) + readyForLogin, connected := make(chan struct{}), make(chan struct{}) + ctx, cancel := context.WithCancelCause(ctx) + go listenConn(conn, readyForLogin, connected, cancel) conn.expect(packet.IDNetworkSettings, packet.IDPlayStatus) if err := conn.WritePacket(&packet.RequestNetworkSettings{ClientProtocol: d.Protocol.ID()}); err != nil { - return nil, err + return nil, conn.wrap(fmt.Errorf("send request network settings: %w", err), "dial") } _ = conn.Flush() select { + case <-ctx.Done(): + return nil, conn.wrap(context.Cause(ctx), "dial") case <-conn.close: return nil, conn.closeErr("dial") - case <-ctx.Done(): - return nil, conn.wrap(ctx.Err(), "dial") - case <-l: + case <-readyForLogin: // We've received our network settings, so we can now send our login request. conn.expect(packet.IDServerToClientHandshake, packet.IDPlayStatus) if err := conn.WritePacket(&packet.Login{ConnectionRequest: request, ClientProtocol: d.Protocol.ID()}); err != nil { - return nil, err + return nil, conn.wrap(fmt.Errorf("send login: %w", err), "dial") } _ = conn.Flush() select { + case <-ctx.Done(): + return nil, conn.wrap(context.Cause(ctx), "dial") case <-conn.close: return nil, conn.closeErr("dial") - case <-ctx.Done(): - return nil, conn.wrap(ctx.Err(), "dial") - case <-c: + case <-connected: // We've connected successfully. We return the connection and no error. return conn, nil } @@ -278,35 +280,45 @@ func readChainIdentityData(chainData []byte) login.IdentityData { // listenConn listens on the connection until it is closed on another goroutine. The channel passed will // receive a value once the connection is logged in. -func listenConn(conn *Conn, logger *log.Logger, l, c chan struct{}) { +func listenConn(conn *Conn, readyForLogin, connected chan struct{}, cancel context.CancelCauseFunc) { defer func() { _ = conn.Close() }() + cancelContext := true for { // We finally arrived at the packet decoding loop. We constantly decode packets that arrive // and push them to the Conn so that they may be processed. packets, err := conn.dec.Decode() if err != nil { if !errors.Is(err, net.ErrClosed) { - logger.Printf("dialer conn: %v\n", err) + if cancelContext { + cancel(err) + } else { + conn.log.Error(err.Error()) + } } return } for _, data := range packets { loggedInBefore, readyToLoginBefore := conn.loggedIn, conn.readyToLogin if err := conn.receive(data); err != nil { - logger.Printf("dialer conn: %v", err) + if cancelContext { + cancel(err) + } else { + conn.log.Error(err.Error()) + } return } if !readyToLoginBefore && conn.readyToLogin { // This is the signal that the connection is ready to login, so we put a value in the channel so that // it may be detected. - l <- struct{}{} + readyForLogin <- struct{}{} } if !loggedInBefore && conn.loggedIn { // This is the signal that the connection was considered logged in, so we put a value in the channel so // that it may be detected. - c <- struct{}{} + cancelContext = false + connected <- struct{}{} } } } diff --git a/minecraft/err.go b/minecraft/err.go index acb0b977..9ab2fd7d 100644 --- a/minecraft/err.go +++ b/minecraft/err.go @@ -5,10 +5,7 @@ import ( "net" ) -var ( - errBufferTooSmall = errors.New("a message sent was larger than the buffer used to receive the message into") - errListenerClosed = errors.New("use of closed listener") -) +var errBufferTooSmall = errors.New("a message sent was larger than the buffer used to receive the message into") // wrap wraps the error passed into a net.OpError with the op as operation and returns it, or nil if the error // passed is nil. diff --git a/minecraft/internal/log.go b/minecraft/internal/log.go new file mode 100644 index 00000000..ce9b1213 --- /dev/null +++ b/minecraft/internal/log.go @@ -0,0 +1,15 @@ +package internal + +import ( + "context" + "log/slog" +) + +// DiscardHandler implements a slog.Handler that is always disabled. Each of its +// methods return immediately without any code running. +type DiscardHandler struct{} + +func (d DiscardHandler) Enabled(context.Context, slog.Level) bool { return false } +func (d DiscardHandler) Handle(context.Context, slog.Record) error { return nil } +func (d DiscardHandler) WithAttrs([]slog.Attr) slog.Handler { return d } +func (d DiscardHandler) WithGroup(string) slog.Handler { return d } diff --git a/minecraft/listener.go b/minecraft/listener.go index 6eeb528b..dead47aa 100644 --- a/minecraft/listener.go +++ b/minecraft/listener.go @@ -6,13 +6,13 @@ import ( "crypto/rand" "errors" "fmt" + "github.com/sandertv/gophertunnel/minecraft/internal" "github.com/sandertv/go-raknet" "github.com/sandertv/gophertunnel/minecraft/protocol" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" "github.com/sandertv/gophertunnel/minecraft/resource" - "log" + "log/slog" "net" - "os" "slices" "sync" "sync/atomic" @@ -21,9 +21,9 @@ import ( // ListenConfig holds settings that may be edited to change behaviour of a Listener. type ListenConfig struct { - // ErrorLog is a log.Logger that errors that occur during packet handling of clients are written to. By - // default, ErrorLog is set to one equal to the global logger. - ErrorLog *log.Logger + // ErrorLog is a log.Logger that errors that occur during packet handling of + // clients are written to. By default, errors are not logged. + ErrorLog *slog.Logger // AuthenticationDisabled specifies if authentication of players that join is disabled. If set to true, no // verification will be done to ensure that the player connecting is authenticated using their XBOX Live @@ -108,19 +108,10 @@ type Listener struct { // If the host in the address parameter is empty or a literal unspecified IP address, Listen listens on all // available unicast and anycast IP addresses of the local system. func (cfg ListenConfig) Listen(network string, address string) (*Listener, error) { - n, ok := networkByID(network) - if !ok { - return nil, fmt.Errorf("listen: no network under id %v", network) - } - - netListener, err := n.Listen(address) - if err != nil { - return nil, err - } - if cfg.ErrorLog == nil { - cfg.ErrorLog = log.New(os.Stderr, "", log.LstdFlags) + cfg.ErrorLog = slog.New(internal.DiscardHandler{}) } + cfg.ErrorLog = cfg.ErrorLog.With("src", "listener") if cfg.StatusProvider == nil { cfg.StatusProvider = NewStatusProvider("Minecraft Server", "Gophertunnel") } @@ -130,6 +121,16 @@ func (cfg ListenConfig) Listen(network string, address string) (*Listener, error if cfg.FlushRate == 0 { cfg.FlushRate = time.Second / 20 } + + n, ok := networkByID(network, cfg.ErrorLog) + if !ok { + return nil, fmt.Errorf("listen: no network under id %v", network) + } + + netListener, err := n.Listen(address) + if err != nil { + return nil, err + } key, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) listener := &Listener{ cfg: cfg, @@ -163,7 +164,7 @@ func Listen(network, address string) (*Listener, error) { func (listener *Listener) Accept() (net.Conn, error) { conn, ok := <-listener.incoming if !ok { - return nil, &net.OpError{Op: "accept", Net: "minecraft", Addr: listener.Addr(), Err: errListenerClosed} + return nil, &net.OpError{Op: "accept", Net: "minecraft", Addr: listener.Addr(), Err: net.ErrClosed} } return conn, nil } @@ -213,8 +214,7 @@ func (listener *Listener) updatePongData() { s := listener.status() listener.listener.PongData([]byte(fmt.Sprintf("MCPE;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;%v;", s.ServerName, protocol.CurrentProtocol, protocol.CurrentVersion, s.PlayerCount, s.MaxPlayers, - listener.listener.ID(), s.ServerSubName, "Creative", 1, listener.Addr().(*net.UDPAddr).Port, listener.Addr().(*net.UDPAddr).Port, - 0, + listener.listener.ID(), s.ServerSubName, "Creative", 1, listener.Addr().(*net.UDPAddr).Port, listener.Addr().(*net.UDPAddr).Port, 0, ))) } @@ -316,14 +316,14 @@ func (listener *Listener) handleConn(conn *Conn) { packets, err := conn.dec.Decode() if err != nil { if !errors.Is(err, net.ErrClosed) { - conn.log.Printf("listener conn: %v\n", err) + conn.log.Error(err.Error()) } return } for _, data := range packets { loggedInBefore := conn.loggedIn if err := conn.receive(data); err != nil { - conn.log.Printf("listener conn: %v", err) + conn.log.Error(err.Error()) return } if !loggedInBefore && conn.loggedIn { diff --git a/minecraft/network.go b/minecraft/network.go index c29d1363..09889cc6 100644 --- a/minecraft/network.go +++ b/minecraft/network.go @@ -2,6 +2,7 @@ package minecraft import ( "context" + "log/slog" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" "net" ) @@ -45,15 +46,18 @@ type NetworkListener interface { // networks holds a map of id => Network to be used for looking up the network by an ID. It is registered to when calling // RegisterNetwork. -var networks = map[string]Network{} +var networks = map[string]func(l *slog.Logger) Network{} // RegisterNetwork registers a network so that it can be used for Gophertunnel. -func RegisterNetwork(id string, n Network) { +func RegisterNetwork(id string, n func(l *slog.Logger) Network) { networks[id] = n } // networkByID returns the network with the ID passed. If no network is found, the second return value will be false. -func networkByID(id string) (Network, bool) { +func networkByID(id string, l *slog.Logger) (Network, bool) { n, ok := networks[id] - return n, ok + if ok { + return n(l), true + } + return nil, false } diff --git a/minecraft/protocol/camera.go b/minecraft/protocol/camera.go index 19233144..5c6838a8 100644 --- a/minecraft/protocol/camera.go +++ b/minecraft/protocol/camera.go @@ -75,6 +75,8 @@ type CameraInstructionSet struct { // ViewOffset is an offset based on a pivot point to the player, causing the camera to be shifted in a // certain direction. ViewOffset Optional[mgl32.Vec2] + // EntityOffset is an offset from the entity that the camera should be rendered at. + EntityOffset Optional[mgl32.Vec3] // Default determines whether the camera is a default camera or not. Default Optional[bool] } @@ -87,6 +89,7 @@ func (x *CameraInstructionSet) Marshal(r IO) { OptionalFunc(r, &x.Rotation, r.Vec2) OptionalFunc(r, &x.Facing, r.Vec3) OptionalFunc(r, &x.ViewOffset, r.Vec2) + OptionalFunc(r, &x.EntityOffset, r.Vec3) OptionalFunc(r, &x.Default, r.Bool) } @@ -156,6 +159,12 @@ type CameraPreset struct { RotationSpeed Optional[float32] // SnapToTarget determines whether the camera should snap to the target entity or not. SnapToTarget Optional[bool] + // HorizontalRotationLimit is the horizontal rotation limit of the camera. + HorizontalRotationLimit Optional[mgl32.Vec2] + // VerticalRotationLimit is the vertical rotation limit of the camera. + VerticalRotationLimit Optional[mgl32.Vec2] + // ContinueTargeting determines whether the camera should continue targeting the entity or not. + ContinueTargeting Optional[bool] // ViewOffset is only used in a follow_orbit camera and controls an offset based on a pivot point to the // player, causing it to be shifted in a certain direction. ViewOffset Optional[mgl32.Vec2] @@ -169,6 +178,9 @@ type CameraPreset struct { AudioListener Optional[byte] // PlayerEffects is currently unknown. PlayerEffects Optional[bool] + // AlignTargetAndCameraForward determines whether the camera should align the target and the camera forward + // or not. + AlignTargetAndCameraForward Optional[bool] } // Marshal encodes/decodes a CameraPreset. @@ -182,9 +194,13 @@ func (x *CameraPreset) Marshal(r IO) { OptionalFunc(r, &x.RotY, r.Float32) OptionalFunc(r, &x.RotationSpeed, r.Float32) OptionalFunc(r, &x.SnapToTarget, r.Bool) + OptionalFunc(r, &x.HorizontalRotationLimit, r.Vec2) + OptionalFunc(r, &x.VerticalRotationLimit, r.Vec2) + OptionalFunc(r, &x.ContinueTargeting, r.Bool) OptionalFunc(r, &x.ViewOffset, r.Vec2) OptionalFunc(r, &x.EntityOffset, r.Vec3) OptionalFunc(r, &x.Radius, r.Float32) OptionalFunc(r, &x.AudioListener, r.Uint8) OptionalFunc(r, &x.PlayerEffects, r.Bool) + OptionalFunc(r, &x.AlignTargetAndCameraForward, r.Bool) } diff --git a/minecraft/protocol/info.go b/minecraft/protocol/info.go index e1f3fc47..265103dc 100644 --- a/minecraft/protocol/info.go +++ b/minecraft/protocol/info.go @@ -2,7 +2,7 @@ package protocol const ( // CurrentProtocol is the current protocol version for the version below. - CurrentProtocol = 729 + CurrentProtocol = 748 // CurrentVersion is the current version of Minecraft as supported by the `packet` package. - CurrentVersion = "1.21.30" + CurrentVersion = "1.21.40" ) diff --git a/minecraft/protocol/login/data.go b/minecraft/protocol/login/data.go index 7d596825..6306a61b 100644 --- a/minecraft/protocol/login/data.go +++ b/minecraft/protocol/login/data.go @@ -193,6 +193,20 @@ type ClientData struct { // CompatibleWithClientSideChunkGen is a boolean indicating if the client's hardware is capable of using the client // side chunk generation system. CompatibleWithClientSideChunkGen bool + // MaxViewDistance is the highest render distance that the client's hardware can handle. + MaxViewDistance int + // MemoryTier is the tier of memory that the client's hardware has. This is a number between 0 and 5. The + // full calculation of this tier is currently unknown but the following is a rough estimate from a + // developer at Mojang: + // 0 - Undetermined + // 1 - Super Low, less than ~1.5GB of memory + // 2 - Low, less than ~2GB of memory + // 3 - Mid, less than ~4GB of memory + // 4 - High, less than ~8GB of memory + // 5 - Super High, more than ~8GB of memory + MemoryTier int + // PlatformType is the type of platform the client is running. + PlatformType int } // PersonaPiece represents a piece of a persona skin. All pieces are sent separately. diff --git a/minecraft/protocol/packet/id.go b/minecraft/protocol/packet/id.go index 92f98689..c036fa9f 100644 --- a/minecraft/protocol/packet/id.go +++ b/minecraft/protocol/packet/id.go @@ -218,4 +218,6 @@ const ( IDServerBoundDiagnostics IDCameraAimAssist IDContainerRegistryCleanup + IDMovementEffect + IDSetMovementAuthority ) diff --git a/minecraft/protocol/packet/inventory_content.go b/minecraft/protocol/packet/inventory_content.go index 09bef7a2..1a44bedd 100644 --- a/minecraft/protocol/packet/inventory_content.go +++ b/minecraft/protocol/packet/inventory_content.go @@ -16,8 +16,10 @@ type InventoryContent struct { Content []protocol.ItemInstance // Container is the protocol.FullContainerName that describes the container that the content is for. Container protocol.FullContainerName - // DynamicContainerSize is the size of the container, if the container is dynamic. - DynamicContainerSize uint32 + // StorageItem is the item that is acting as the storage container for the inventory. If the inventory is + // not a dynamic container then this field should be left empty. When set, only the item type is used by + // the client and none of the other stack info. + StorageItem protocol.ItemInstance } // ID ... @@ -29,5 +31,5 @@ func (pk *InventoryContent) Marshal(io protocol.IO) { io.Varuint32(&pk.WindowID) protocol.FuncSlice(io, &pk.Content, io.ItemInstance) protocol.Single(io, &pk.Container) - io.Varuint32(&pk.DynamicContainerSize) + io.ItemInstance(&pk.StorageItem) } diff --git a/minecraft/protocol/packet/inventory_slot.go b/minecraft/protocol/packet/inventory_slot.go index 421c86ae..5529fab6 100644 --- a/minecraft/protocol/packet/inventory_slot.go +++ b/minecraft/protocol/packet/inventory_slot.go @@ -16,8 +16,10 @@ type InventorySlot struct { Slot uint32 // Container is the protocol.FullContainerName that describes the container that the content is for. Container protocol.FullContainerName - // DynamicContainerSize is the size of the container, if the container is dynamic. - DynamicContainerSize uint32 + // StorageItem is the item that is acting as the storage container for the inventory. If the inventory is + // not a dynamic container then this field should be left empty. When set, only the item type is used by + // the client and none of the other stack info. + StorageItem protocol.ItemInstance // NewItem is the item to be put in the slot at Slot. It will overwrite any item that may currently // be present in that slot. NewItem protocol.ItemInstance @@ -32,6 +34,6 @@ func (pk *InventorySlot) Marshal(io protocol.IO) { io.Varuint32(&pk.WindowID) io.Varuint32(&pk.Slot) protocol.Single(io, &pk.Container) - io.Varuint32(&pk.DynamicContainerSize) + io.ItemInstance(&pk.StorageItem) io.ItemInstance(&pk.NewItem) } diff --git a/minecraft/protocol/packet/mob_effect.go b/minecraft/protocol/packet/mob_effect.go index b476c9e4..b52148ed 100644 --- a/minecraft/protocol/packet/mob_effect.go +++ b/minecraft/protocol/packet/mob_effect.go @@ -78,5 +78,5 @@ func (pk *MobEffect) Marshal(io protocol.IO) { io.Varint32(&pk.Amplifier) io.Bool(&pk.Particles) io.Varint32(&pk.Duration) - io.Uint64(&pk.Tick) + io.Varuint64(&pk.Tick) } diff --git a/minecraft/protocol/packet/movement_effect.go b/minecraft/protocol/packet/movement_effect.go new file mode 100644 index 00000000..17cb5583 --- /dev/null +++ b/minecraft/protocol/packet/movement_effect.go @@ -0,0 +1,36 @@ +package packet + +import ( + "github.com/sandertv/gophertunnel/minecraft/protocol" +) + +const ( + MovementEffectTypeGlideBoost = iota +) + +// MovementEffect is sent by the server to the client to update specific movement effects to allow the client +// to predict its movement. For example, fireworks used during gliding will send this packet to tell the +// client the exact duration of the boost. +type MovementEffect struct { + // EntityRuntimeID is the runtime ID of the entity. The runtime ID is unique for each world session, and + // entities are generally identified in packets using this runtime ID. + EntityRuntimeID uint64 + // Type is the type of movement effect being updated. It is one of the constants found above. + Type int32 + // Duration is the duration of the effect, measured in ticks. + Duration int32 + // Tick is the server tick at which the packet was sent. It is used in relation to CorrectPlayerMovePrediction. + Tick uint64 +} + +// ID ... +func (*MovementEffect) ID() uint32 { + return IDMovementEffect +} + +func (pk *MovementEffect) Marshal(io protocol.IO) { + io.Varuint64(&pk.EntityRuntimeID) + io.Varint32(&pk.Type) + io.Varint32(&pk.Duration) + io.Varuint64(&pk.Tick) +} diff --git a/minecraft/protocol/packet/player_auth_input.go b/minecraft/protocol/packet/player_auth_input.go index c4470fe5..79a6cb06 100644 --- a/minecraft/protocol/packet/player_auth_input.go +++ b/minecraft/protocol/packet/player_auth_input.go @@ -59,6 +59,10 @@ const ( InputFlagVerticalCollision InputFlagDownLeft InputFlagDownRight + InputFlagCameraRelativeMovementEnabled + InputFlagRotControlledByMoveDirection + InputFlagStartSpinAttack + InputFlagStopSpinAttack ) const ( @@ -113,9 +117,10 @@ type PlayerAuthInput struct { // InteractionModel is a constant representing the interaction model the player is using. It is one of the // constants that may be found above. InteractionModel uint32 - // GazeDirection is the direction in which the player is gazing, when the PlayMode is PlayModeReality: In - // other words, when the player is playing in virtual reality. - GazeDirection mgl32.Vec3 + // InteractPitch and interactYaw is the rotation the player is looking that they intend to use for + // interactions. This is only different to Pitch and Yaw in cases such as VR or when custom cameras + // being used. + InteractPitch, InteractYaw float32 // Tick is the server tick at which the packet was sent. It is used in relation to // CorrectPlayerMovePrediction. Tick uint64 @@ -135,6 +140,7 @@ type PlayerAuthInput struct { // AnalogueMoveVector is a Vec2 that specifies the direction in which the player moved, as a combination // of X/Z values which are created using an analogue input. AnalogueMoveVector mgl32.Vec2 + CameraOrientation mgl32.Vec3 } // ID ... @@ -152,9 +158,8 @@ func (pk *PlayerAuthInput) Marshal(io protocol.IO) { io.Varuint32(&pk.InputMode) io.Varuint32(&pk.PlayMode) io.Varuint32(&pk.InteractionModel) - if pk.PlayMode == PlayModeReality { - io.Vec3(&pk.GazeDirection) - } + io.Float32(&pk.InteractPitch) + io.Float32(&pk.InteractYaw) io.Varuint64(&pk.Tick) io.Vec3(&pk.Delta) @@ -176,4 +181,5 @@ func (pk *PlayerAuthInput) Marshal(io protocol.IO) { } io.Vec2(&pk.AnalogueMoveVector) + io.Vec3(&pk.CameraOrientation) } diff --git a/minecraft/protocol/packet/pool.go b/minecraft/protocol/packet/pool.go index 78620a2f..4efab6cd 100644 --- a/minecraft/protocol/packet/pool.go +++ b/minecraft/protocol/packet/pool.go @@ -258,6 +258,8 @@ func init() { IDServerBoundDiagnostics: func() Packet { return &ServerBoundDiagnostics{} }, IDCameraAimAssist: func() Packet { return &CameraAimAssist{} }, IDContainerRegistryCleanup: func() Packet { return &ContainerRegistryCleanup{} }, + IDMovementEffect: func() Packet { return &MovementEffect{} }, + IDSetMovementAuthority: func() Packet { return &SetMovementAuthority{} }, } for id, pk := range serverOriginating { RegisterPacketFromServer(id, pk) diff --git a/minecraft/protocol/packet/resource_packs_info.go b/minecraft/protocol/packet/resource_packs_info.go index effc41b2..531f8acf 100644 --- a/minecraft/protocol/packet/resource_packs_info.go +++ b/minecraft/protocol/packet/resource_packs_info.go @@ -21,9 +21,6 @@ type ResourcePacksInfo struct { // The order of these texture packs is not relevant in this packet. It is however important in the // ResourcePackStack packet. TexturePacks []protocol.TexturePackInfo - // PackURLs is a list of URLs that the client can use to download a resource pack instead of downloading - // it the usual way. - PackURLs []protocol.PackURL } // ID ... @@ -36,5 +33,4 @@ func (pk *ResourcePacksInfo) Marshal(io protocol.IO) { io.Bool(&pk.HasAddons) io.Bool(&pk.HasScripts) protocol.SliceUint16Length(io, &pk.TexturePacks) - protocol.Slice(io, &pk.PackURLs) } diff --git a/minecraft/protocol/packet/set_movement_authority.go b/minecraft/protocol/packet/set_movement_authority.go new file mode 100644 index 00000000..98c69fca --- /dev/null +++ b/minecraft/protocol/packet/set_movement_authority.go @@ -0,0 +1,24 @@ +package packet + +import ( + "github.com/sandertv/gophertunnel/minecraft/protocol" +) + +// SetMovementAuthority is sent by the server to the client to change its movement mode. +type SetMovementAuthority struct { + // MovementType specifies the way the server handles player movement. Available options are + // protocol.PlayerMovementModeClient, protocol.PlayerMovementModeServer and + // protocol.PlayerMovementModeServerWithRewind, where the server authoritative types result + // in the client sending PlayerAuthInput packets instead of MovePlayer packets and the rewind mode + // requires sending the tick of movement and several actions. + MovementType byte +} + +// ID ... +func (*SetMovementAuthority) ID() uint32 { + return IDSetMovementAuthority +} + +func (pk *SetMovementAuthority) Marshal(io protocol.IO) { + io.Uint8(&pk.MovementType) +} diff --git a/minecraft/protocol/player.go b/minecraft/protocol/player.go index 69633e7b..063977f8 100644 --- a/minecraft/protocol/player.go +++ b/minecraft/protocol/player.go @@ -42,6 +42,7 @@ const ( PlayerActionStartFlying PlayerActionStopFlying PlayerActionClientAckServerData + PlayerActionStartUsingItem ) const ( diff --git a/minecraft/protocol/resource_pack.go b/minecraft/protocol/resource_pack.go index 24581faa..0c6fdc34 100644 --- a/minecraft/protocol/resource_pack.go +++ b/minecraft/protocol/resource_pack.go @@ -28,6 +28,9 @@ type TexturePackInfo struct { AddonPack bool // RTXEnabled specifies if the texture pack uses the raytracing technology introduced in 1.16.200. RTXEnabled bool + // DownloadURL is a URL that the client can use to download the pack instead of the server sending it in + // chunks, which it will continue to do if this field is left empty. + DownloadURL string } // Marshal encodes/decodes a TexturePackInfo. @@ -41,6 +44,7 @@ func (x *TexturePackInfo) Marshal(r IO) { r.Bool(&x.HasScripts) r.Bool(&x.AddonPack) r.Bool(&x.RTXEnabled) + r.String(&x.DownloadURL) } // StackResourcePack represents a resource pack sent on the stack of the client. When sent, the client will diff --git a/minecraft/raknet.go b/minecraft/raknet.go index 066321ea..4dc5c08f 100644 --- a/minecraft/raknet.go +++ b/minecraft/raknet.go @@ -4,23 +4,32 @@ import ( "context" "github.com/sandertv/go-raknet" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" + "log/slog" "net" ) -// RakNet is an implementation of a RakNet v10 Network. -type RakNet struct{} +// RakNet is an implementation of a RakNet v11 Network. +type RakNet struct { + l *slog.Logger +} +// DialContext ... func (RakNet) DialContext(ctx context.Context, a string) (net.Conn, error) { return raknet.DialContext(ctx, a) } + +// PingContext ... func (RakNet) PingContext(ctx context.Context, a string) ([]byte, error) { return raknet.PingContext(ctx, a) } + +// Listen ... func (RakNet) Listen(address string) (NetworkListener, error) { return raknet.Listen(address) } +// Compression ... func (RakNet) Compression(net.Conn) packet.Compression { return packet.FlateCompression } // init registers the RakNet network. func init() { - RegisterNetwork("raknet", RakNet{}) + RegisterNetwork("raknet", func(l *slog.Logger) Network { return RakNet{l: l} }) }