diff --git a/go.mod b/go.mod index fe3ce02f..3f314f0e 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,10 @@ require ( ) require ( - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/df-mc/atomic v1.10.0 // indirect golang.org/x/crypto v0.24.0 // indirect + golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 // indirect golang.org/x/image v0.17.0 // indirect ) + +replace github.com/sandertv/go-raknet => github.com/tedacmc/tedac-raknet v0.0.4 diff --git a/go.sum b/go.sum index 359f81c3..b6a2670b 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/df-mc/atomic v1.10.0 h1:0ZuxBKwR/hxcFGorKiHIp+hY7hgY+XBTzhCYD2NqSEg= +github.com/df-mc/atomic v1.10.0/go.mod h1:Gw9rf+rPIbydMjA329Jn4yjd/O2c/qusw3iNp4tFGSc= github.com/go-gl/mathgl v1.1.0 h1:0lzZ+rntPX3/oGrDzYGdowSLC2ky8Osirvf5uAwfIEA= github.com/go-gl/mathgl v1.1.0/go.mod h1:yhpkQzEiH9yPyxDUGzkmgScbaBVlhC06qodikEM0ZwQ= github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= @@ -19,19 +21,21 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sandertv/go-raknet v1.14.0 h1:2vtO1m1DFLFszeCcV7mVZfVgkDcAbSxcjM2BlrVrEGs= -github.com/sandertv/go-raknet v1.14.0/go.mod h1:/yysjwfCXm2+2OY8mBazLzcxJ3irnylKCyG3FLgUPVU= -github.com/sandertv/go-raknet v1.14.1 h1:V2Gslo+0x4jfj+p0PM48mWxmMbYkxSlgeKy//y3ZrzI= -github.com/sandertv/go-raknet v1.14.1/go.mod h1:/yysjwfCXm2+2OY8mBazLzcxJ3irnylKCyG3FLgUPVU= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tedacmc/tedac-raknet v0.0.4 h1:GcRfp38iXARo/Wb+nfrCPHrYpqgAGi7XzapwunIA/LQ= +github.com/tedacmc/tedac-raknet v0.0.4/go.mod h1:vT0+qrD5NHYW9OElUncfIRT0brTgJhxyaozMZyjL2Zc= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +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/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= golang.org/x/image v0.17.0 h1:nTRVVdajgB8zCMZVsViyzhnMKPwYeroEERRC64JuLco= golang.org/x/image v0.17.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= diff --git a/minecraft/auth/live.go b/minecraft/auth/live.go index 6b445eb9..e1ef4b59 100644 --- a/minecraft/auth/live.go +++ b/minecraft/auth/live.go @@ -3,14 +3,13 @@ package auth import ( "encoding/json" "fmt" + "golang.org/x/oauth2" + "golang.org/x/oauth2/microsoft" "io" "net/http" "net/url" "os" "time" - - "golang.org/x/oauth2" - "golang.org/x/oauth2/microsoft" ) // TokenSource holds an oauth2.TokenSource which uses device auth to get a code. The user authenticates using @@ -74,7 +73,7 @@ func RequestLiveToken() (*oauth2.Token, error) { // be printed to the io.Writer passed with a user code which the user must use to submit. // Once fully authenticated, an oauth2 token is returned which may be used to login to XBOX Live. func RequestLiveTokenWriter(w io.Writer) (*oauth2.Token, error) { - d, err := startDeviceAuth() + d, err := StartDeviceAuth() if err != nil { return nil, err } @@ -83,7 +82,7 @@ func RequestLiveTokenWriter(w io.Writer) (*oauth2.Token, error) { defer ticker.Stop() for range ticker.C { - t, err := pollDeviceAuth(d.DeviceCode) + t, err := PollDeviceAuth(d.DeviceCode) if err != nil { return nil, fmt.Errorf("error polling for device auth: %w", err) } @@ -97,9 +96,9 @@ func RequestLiveTokenWriter(w io.Writer) (*oauth2.Token, error) { panic("unreachable") } -// startDeviceAuth starts the device auth, retrieving a login URI for the user and a code the user needs to +// StartDeviceAuth starts the device auth, retrieving a login URI for the user and a code the user needs to // enter. -func startDeviceAuth() (*deviceAuthConnect, error) { +func StartDeviceAuth() (*DeviceAuthConnect, error) { resp, err := http.PostForm("https://login.live.com/oauth20_connect.srf", url.Values{ "client_id": {"0000000048183522"}, "scope": {"service::user.auth.xboxlive.com::MBI_SSL"}, @@ -114,13 +113,13 @@ func startDeviceAuth() (*deviceAuthConnect, error) { if resp.StatusCode != 200 { return nil, fmt.Errorf("POST https://login.live.com/oauth20_connect.srf: %v", resp.Status) } - data := new(deviceAuthConnect) + data := new(DeviceAuthConnect) return data, json.NewDecoder(resp.Body).Decode(data) } -// pollDeviceAuth polls the token endpoint for the device code. A token is returned if the user authenticated +// PollDeviceAuth polls the token endpoint for the device code. A token is returned if the user authenticated // successfully. If the user has not yet authenticated, err is nil but the token is nil too. -func pollDeviceAuth(deviceCode string) (t *oauth2.Token, err error) { +func PollDeviceAuth(deviceCode string) (t *oauth2.Token, err error) { resp, err := http.PostForm(microsoft.LiveConnectEndpoint.TokenURL, url.Values{ "client_id": {"0000000048183522"}, "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, @@ -177,7 +176,7 @@ func refreshToken(t *oauth2.Token) (*oauth2.Token, error) { }, nil } -type deviceAuthConnect struct { +type DeviceAuthConnect struct { UserCode string `json:"user_code"` DeviceCode string `json:"device_code"` VerificationURI string `json:"verification_uri"` diff --git a/minecraft/auth/xbox.go b/minecraft/auth/xbox.go index 157de725..ede6de12 100644 --- a/minecraft/auth/xbox.go +++ b/minecraft/auth/xbox.go @@ -12,11 +12,10 @@ import ( "encoding/binary" "encoding/json" "fmt" - "net/http" - "time" - "github.com/google/uuid" "golang.org/x/oauth2" + "net/http" + "time" ) // XBLToken holds info on the authorization token used for authenticating with XBOX Live. @@ -220,4 +219,3 @@ func parseXboxErrorCode(code string) string { return fmt.Sprintf("unknown error code: %v", code) } } - diff --git a/minecraft/conn.go b/minecraft/conn.go index dec8b8d4..51d8d970 100644 --- a/minecraft/conn.go +++ b/minecraft/conn.go @@ -173,7 +173,7 @@ func newConn(netConn net.Conn, key *ecdsa.PrivateKey, log *log.Logger, proto Pro } _, _ = rand.Read(conn.salt) - conn.expectedIDs.Store([]uint32{packet.IDRequestNetworkSettings}) + conn.expectedIDs.Store([]uint32{packet.IDLogin, packet.IDRequestNetworkSettings}) if flushRate <= 0 { return conn @@ -458,7 +458,7 @@ func (conn *Conn) Flush() error { if len(conn.bufferedSend) > 0 { if err := conn.enc.Encode(conn.bufferedSend); err != nil && !errors.Is(err, net.ErrClosed) { // Should never happen. - panic(fmt.Errorf("error encoding packet batch: %w", err)) + return fmt.Errorf("error encoding packet batch: %v", err) } // First manually clear out conn.bufferedSend so that re-using the slice after resetting its length to // 0 doesn't result in an 'invisible' memory leak. @@ -494,6 +494,11 @@ func (conn *Conn) RemoteAddr() net.Addr { return conn.conn.RemoteAddr() } +// Protocol returns the protocol used by the connection. +func (conn *Conn) Protocol() Protocol { + return conn.proto +} + // SetDeadline sets the read and write deadline of the connection. It is equivalent to calling SetReadDeadline // and SetWriteDeadline at the same time. func (conn *Conn) SetDeadline(t time.Time) error { @@ -712,8 +717,13 @@ func (conn *Conn) handleRequestNetworkSettings(pk *packet.RequestNetworkSettings return fmt.Errorf("send NetworkSettings: %w", err) } _ = conn.Flush() - conn.enc.EnableCompression(conn.compression) - conn.dec.EnableCompression() + compression := conn.compression + if pk.ClientProtocol >= 649 { // 1.20.60 + // TODO: I hate this hack as much as the next person, but I don't see another other way out. + compression = packet.NewOnTheFlyCompression(compression) + } + conn.enc.EnableCompression(compression) + conn.dec.EnableCompression(compression) return nil } @@ -723,8 +733,13 @@ func (conn *Conn) handleNetworkSettings(pk *packet.NetworkSettings) error { if !ok { return fmt.Errorf("unknown compression algorithm %v", pk.CompressionAlgorithm) } - conn.enc.EnableCompression(alg) - conn.dec.EnableCompression() + compression := alg + if conn.proto.ID() >= 649 { // 1.20.60 + // TODO: I hate this hack as much as the next person, but I don't see another other way out. + compression = packet.NewOnTheFlyCompression(compression) + } + conn.enc.EnableCompression(compression) + conn.dec.EnableCompression(compression) conn.readyToLogin = true return nil } @@ -732,6 +747,25 @@ func (conn *Conn) handleNetworkSettings(pk *packet.NetworkSettings) error { // handleLogin handles an incoming login packet. It verifies and decodes the login request found in the packet // and returns an error if it couldn't be done successfully. func (conn *Conn) handleLogin(pk *packet.Login) error { + found := false + for _, pro := range conn.acceptedProto { + if pro.ID() == pk.ClientProtocol { + conn.proto = pro + conn.pool = pro.Packets(true) + found = true + break + } + } + if !found { + status := packet.PlayStatusLoginFailedClient + if pk.ClientProtocol > protocol.CurrentProtocol { + // The server is outdated in this case, so we have to change the status we send. + status = packet.PlayStatusLoginFailedServer + } + _ = conn.WritePacket(&packet.PlayStatus{Status: status}) + return fmt.Errorf("%v connected with an incompatible protocol: expected protocol = %v, client protocol = %v", conn.identityData.DisplayName, protocol.CurrentProtocol, pk.ClientProtocol) + } + // The next expected packet is a response from the client to the handshake. conn.expect(packet.IDClientToServerHandshake) var ( @@ -835,8 +869,8 @@ func (conn *Conn) handleServerToClientHandshake(pk *packet.ServerToClientHandsha keyBytes := sha256.Sum256(append(salt, sharedSecret...)) // Finally we enable encryption for the enc and dec using the secret pubKey bytes we produced. - conn.enc.EnableEncryption(keyBytes) - conn.dec.EnableEncryption(keyBytes) + conn.enc.EnableEncryption(conn.proto.Encryption(keyBytes)) + conn.dec.EnableEncryption(conn.proto.Encryption(keyBytes)) // We write a ClientToServerHandshake packet (which has no payload) as a response. _ = conn.WritePacket(&packet.ClientToServerHandshake{}) @@ -1420,9 +1454,8 @@ func (conn *Conn) enableEncryption(clientPublicKey *ecdsa.PublicKey) error { keyBytes := sha256.Sum256(append(conn.salt, sharedSecret...)) // Finally we enable encryption for the encoder and decoder using the secret key bytes we produced. - conn.enc.EnableEncryption(keyBytes) - conn.dec.EnableEncryption(keyBytes) - + conn.enc.EnableEncryption(conn.proto.Encryption(keyBytes)) + conn.dec.EnableEncryption(conn.proto.Encryption(keyBytes)) return nil } @@ -1437,5 +1470,5 @@ func (conn *Conn) closeErr(op string) error { if msg := *conn.disconnectMessage.Load(); msg != "" { return conn.wrap(DisconnectError(msg), op) } - return conn.wrap(errClosed, op) + return conn.wrap(net.ErrClosed, op) } diff --git a/minecraft/err.go b/minecraft/err.go index 04eb2ae1..acb0b977 100644 --- a/minecraft/err.go +++ b/minecraft/err.go @@ -6,8 +6,6 @@ import ( ) var ( - // TODO: Change this to net.ErrClosed in 1.16. - errClosed = errors.New("use of closed network connection") errBufferTooSmall = errors.New("a message sent was larger than the buffer used to receive the message into") errListenerClosed = errors.New("use of closed listener") ) diff --git a/minecraft/listener.go b/minecraft/listener.go index 001860bb..6eeb528b 100644 --- a/minecraft/listener.go +++ b/minecraft/listener.go @@ -6,6 +6,7 @@ import ( "crypto/rand" "errors" "fmt" + "github.com/sandertv/go-raknet" "github.com/sandertv/gophertunnel/minecraft/protocol" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" "github.com/sandertv/gophertunnel/minecraft/resource" @@ -140,7 +141,7 @@ func (cfg ListenConfig) Listen(network string, address string) (*Listener, error } // Actually start listening. - go listener.listen() + go listener.listen(n) return listener, nil } @@ -219,7 +220,7 @@ func (listener *Listener) updatePongData() { // listen starts listening for incoming connections and packets. When a player is fully connected, it submits // it to the accepted connections channel so that a call to Accept can pick it up. -func (listener *Listener) listen() { +func (listener *Listener) listen(n Network) { listener.updatePongData() go func() { ticker := time.NewTicker(time.Second * 4) @@ -245,13 +246,13 @@ func (listener *Listener) listen() { // close too. return } - listener.createConn(netConn) + listener.createConn(n, netConn) } } // createConn creates a connection for the net.Conn passed and adds it to the listener, so that it may be // accepted once its login sequence is complete. -func (listener *Listener) createConn(netConn net.Conn) { +func (listener *Listener) createConn(n Network, netConn net.Conn) { listener.packsMu.RLock() packs := slices.Clone(listener.packs) listener.packsMu.RUnlock() @@ -259,6 +260,8 @@ func (listener *Listener) createConn(netConn net.Conn) { conn := newConn(netConn, listener.key, listener.cfg.ErrorLog, proto{}, listener.cfg.FlushRate, true) conn.acceptedProto = append(listener.cfg.AcceptedProtocols, proto{}) conn.compression = listener.cfg.Compression + // Temporarily set the protocol to the latest: We don't know the actual protocol until we read the Login packet. + conn.proto = proto{} conn.pool = conn.proto.Packets(true) conn.packetFunc = listener.cfg.PacketFunc @@ -270,6 +273,14 @@ func (listener *Listener) createConn(netConn net.Conn) { conn.disconnectOnUnknownPacket = !listener.cfg.AllowUnknownPackets conn.disconnectOnInvalidPacket = !listener.cfg.AllowInvalidPackets + // Enable compression based on the protocol. + // 10 was the last RakNet protocol version, that reading Login packet at the first packet + // before RequestNetworkSettings packet getting added on version 11. + if netConn.(*raknet.Conn).ProtocolVersion() <= 10 { + conn.enc.EnableCompression(n.Compression(netConn)) + conn.dec.EnableCompression(n.Compression(netConn)) + } + if listener.playerCount.Load() == int32(listener.cfg.MaximumPlayers) && listener.cfg.MaximumPlayers != 0 { // The server was full. We kick the player immediately and close the connection. _ = conn.WritePacket(&packet.PlayStatus{Status: packet.PlayStatusLoginFailedServerFull}) diff --git a/minecraft/network.go b/minecraft/network.go index e0c8f91c..c29d1363 100644 --- a/minecraft/network.go +++ b/minecraft/network.go @@ -2,6 +2,7 @@ package minecraft import ( "context" + "github.com/sandertv/gophertunnel/minecraft/protocol/packet" "net" ) @@ -24,6 +25,9 @@ type Network interface { // Specific features of the listener may be modified once it is returned, such as the used log and/or the // accepted protocol. Listen(address string) (NetworkListener, error) + + // Compression returns a new compression instance used by this Protocol. + Compression(conn net.Conn) packet.Compression } // NetworkListener represents a listening connection to a remote server. It is the equivalent of net.Listener, but with extra diff --git a/minecraft/protocol.go b/minecraft/protocol.go index f7d6f17f..0e79bcb1 100644 --- a/minecraft/protocol.go +++ b/minecraft/protocol.go @@ -20,6 +20,10 @@ type Protocol interface { // to true, the pool should be created for a Listener. This means that only // packets that may be sent by a client should be allowed. Packets(listener bool) packet.Pool + + // Encryption returns a new encryption instance used by this Protocol. + Encryption(key [32]byte) packet.Encryption + // NewReader returns a protocol.IO that implements reading operations for reading types // that are used for this Protocol. NewReader(r ByteReader, shieldID int32, enableLimits bool) protocol.IO @@ -53,22 +57,23 @@ type ByteWriter interface { // convert any packets, as they are already of the right type. type proto struct{} -func (proto) ID() int32 { return protocol.CurrentProtocol } -func (p proto) Ver() string { return protocol.CurrentVersion } -func (p proto) Packets(listener bool) packet.Pool { +func (proto) ID() int32 { return protocol.CurrentProtocol } +func (proto) Ver() string { return protocol.CurrentVersion } +func (proto) Packets(listener bool) packet.Pool { if listener { return packet.NewClientPool() } return packet.NewServerPool() } -func (p proto) NewReader(r ByteReader, shieldID int32, enableLimits bool) protocol.IO { +func (proto) Encryption(key [32]byte) packet.Encryption { return packet.NewCTREncryption(key[:]) } +func (proto) NewReader(r ByteReader, shieldID int32, enableLimits bool) protocol.IO { return protocol.NewReader(r, shieldID, enableLimits) } -func (p proto) NewWriter(w ByteWriter, shieldID int32) protocol.IO { +func (proto) NewWriter(w ByteWriter, shieldID int32) protocol.IO { return protocol.NewWriter(w, shieldID) } -func (p proto) ConvertToLatest(pk packet.Packet, _ *Conn) []packet.Packet { return []packet.Packet{pk} } -func (p proto) ConvertFromLatest(pk packet.Packet, _ *Conn) []packet.Packet { +func (proto) ConvertToLatest(pk packet.Packet, _ *Conn) []packet.Packet { return []packet.Packet{pk} } +func (proto) ConvertFromLatest(pk packet.Packet, _ *Conn) []packet.Packet { return []packet.Packet{pk} } diff --git a/minecraft/protocol/io.go b/minecraft/protocol/io.go index 70fd51f1..3108b48b 100644 --- a/minecraft/protocol/io.go +++ b/minecraft/protocol/io.go @@ -1,6 +1,7 @@ package protocol import ( + "fmt" "github.com/go-gl/mathgl/mgl32" "github.com/google/uuid" "github.com/sandertv/gophertunnel/minecraft/nbt" @@ -142,10 +143,10 @@ const maxSliceLength = 1024 // SliceOfLen reads/writes the elements of a slice of type T with length l. func SliceOfLen[T any, S ~*[]T, A PtrMarshaler[T]](r IO, l uint32, x S) { - rd, reader := r.(*Reader) + rd, reader := r.(Reads) if reader { - if rd.limitsEnabled && l > maxSliceLength { - rd.panicf("slice length was too long: length of %v", l) + if rd.LimitsEnabled() && l > maxSliceLength { + panic(fmt.Errorf("slice length was too long: length of %v", l)) } *x = make([]T, l) } @@ -157,10 +158,10 @@ func SliceOfLen[T any, S ~*[]T, A PtrMarshaler[T]](r IO, l uint32, x S) { // FuncSliceOfLen reads/writes the elements of a slice of type T with length l using func f. func FuncSliceOfLen[T any, S ~*[]T](r IO, l uint32, x S, f func(*T)) { - rd, reader := r.(*Reader) + rd, reader := r.(Reads) if reader { - if rd.limitsEnabled && l > maxSliceLength { - rd.panicf("slice length was too long: length of %v", l) + if rd.LimitsEnabled() && l > maxSliceLength { + panic(fmt.Errorf("slice length was too long: length of %v", l)) } *x = make([]T, l) } diff --git a/minecraft/protocol/login/data.go b/minecraft/protocol/login/data.go index 0a391274..7d596825 100644 --- a/minecraft/protocol/login/data.go +++ b/minecraft/protocol/login/data.go @@ -275,6 +275,10 @@ var checkVersion = regexp.MustCompile("[0-9.]").MatchString // Validate validates the client data. It returns an error if any of the fields checked did not carry a valid // value. func (data ClientData) Validate() error { + if data.GameVersion != protocol.CurrentVersion { + // We shouldn't validate the client data if the client may not be on the latest version. + return nil + } if data.DeviceOS <= 0 || data.DeviceOS > 15 { return fmt.Errorf("DeviceOS must carry a value between 1 and 15, but got %v", data.DeviceOS) } diff --git a/minecraft/protocol/login/request.go b/minecraft/protocol/login/request.go index 190e725f..49f910ce 100644 --- a/minecraft/protocol/login/request.go +++ b/minecraft/protocol/login/request.go @@ -68,7 +68,7 @@ func Parse(request []byte) (IdentityData, ClientData, AuthResult, error) { return iData, cData, res, fmt.Errorf("parse token 0: %w", err) } - // The first token holds the client's public key in the x5u (it's self signed). + // The first token holds the client's public key in the x5u (it's self-signed). //lint:ignore S1005 Double assignment is done explicitly to prevent panics. raw, _ := tok.Headers[0].ExtraHeaders["x5u"] if err := parseAsKey(raw, key); err != nil { diff --git a/minecraft/protocol/packet/compression.go b/minecraft/protocol/packet/compression.go index 840a1bd6..38c63120 100644 --- a/minecraft/protocol/packet/compression.go +++ b/minecraft/protocol/packet/compression.go @@ -7,6 +7,7 @@ import ( "github.com/klauspost/compress/flate" "github.com/sandertv/gophertunnel/minecraft/internal" "io" + "math" "sync" ) @@ -33,6 +34,10 @@ var ( DefaultCompression Compression = FlateCompression ) +func NewOnTheFlyCompression(underlyingCompression Compression) Compression { + return onTheFlyCompression{underlyingCompression} +} + type ( // nopCompression is an empty implementation that does not compress data. nopCompression struct{} @@ -40,14 +45,16 @@ type ( flateCompression struct{} // snappyCompression is the implementation of the Snappy compression algorithm. This is used by default. snappyCompression struct{} + // onTheFlyCompression is the implementation of the both compression algorithms. This is used by default for decoding. + onTheFlyCompression struct{ c Compression } ) -// flateDecompressPool is a sync.Pool for io.ReadCloser flate readers. These are -// pooled for connections. var ( + // flateDecompressPool is a sync.Pool for io.ReadCloser flate readers. These are pooled for connections. flateDecompressPool = sync.Pool{ New: func() any { return flate.NewReader(bytes.NewReader(nil)) }, } + // flateCompressPool is a sync.Pool for io.ReadCloser flate writers. These are pooled for connections. flateCompressPool = sync.Pool{ New: func() any { w, _ := flate.NewWriter(io.Discard, 6) @@ -146,6 +153,38 @@ func (snappyCompression) Decompress(compressed []byte) ([]byte, error) { return decompressed, nil } +// EncodeCompression ... +func (onTheFlyCompression) EncodeCompression() uint16 { + return math.MaxUint16 +} + +// Compress ... +func (c onTheFlyCompression) Compress(decompressed []byte) ([]byte, error) { + prepend := []byte{byte(c.c.EncodeCompression())} + compressed, err := c.c.Compress(decompressed) + if err != nil { + return nil, err + } + return append(prepend, compressed...), nil +} + +// Decompress ... +func (onTheFlyCompression) Decompress(compressed []byte) ([]byte, error) { + var compression Compression + if compressed[0] != 0xff { + var ok bool + compression, ok = CompressionByID(uint16(compressed[0])) + if !ok { + return nil, fmt.Errorf("error decompressing packet: unknown compression algorithm %v", compressed[0]) + } + } + compressed = compressed[1:] + if compression != nil { + return compression.Decompress(compressed) + } + return compressed, nil +} + // init registers all valid compressions with the protocol. func init() { RegisterCompression(flateCompression{}) @@ -168,3 +207,20 @@ func CompressionByID(id uint16) (Compression, bool) { } return c, ok } + +type CompressionError struct { + // Op is the operation which caused the error. + Op string + // Err is the error that occurred during the operation. + // The Error method panics if the error is nil. + Err error +} + +func (e *CompressionError) Unwrap() error { return e.Err } + +func (e *CompressionError) Error() string { + if e == nil { + return "" + } + return e.Op + ": " + e.Err.Error() +} diff --git a/minecraft/protocol/packet/decoder.go b/minecraft/protocol/packet/decoder.go index 44047a02..00bf69d2 100644 --- a/minecraft/protocol/packet/decoder.go +++ b/minecraft/protocol/packet/decoder.go @@ -2,8 +2,6 @@ package packet import ( "bytes" - "crypto/aes" - "crypto/cipher" "fmt" "github.com/sandertv/gophertunnel/minecraft/protocol" "io" @@ -21,8 +19,8 @@ type Decoder struct { // NewDecoder implements the packetReader interface. pr packetReader - decompress bool - encrypt *encrypt + compression Compression + encryption Encryption checkPacketLimit bool } @@ -48,16 +46,13 @@ func NewDecoder(reader io.Reader) *Decoder { // EnableEncryption enables encryption for the Decoder using the secret key bytes passed. Each packet received // will be decrypted. -func (decoder *Decoder) EnableEncryption(keyBytes [32]byte) { - block, _ := aes.NewCipher(keyBytes[:]) - first12 := append([]byte(nil), keyBytes[:12]...) - stream := cipher.NewCTR(block, append(first12, 0, 0, 0, 2)) - decoder.encrypt = newEncrypt(keyBytes[:], stream) +func (decoder *Decoder) EnableEncryption(encryption Encryption) { + decoder.encryption = encryption } // EnableCompression enables compression for the Decoder. -func (decoder *Decoder) EnableCompression() { - decoder.decompress = true +func (decoder *Decoder) EnableCompression(compression Compression) { + decoder.compression = compression } // DisableBatchPacketLimit disables the check that limits the number of packets allowed in a single packet @@ -86,7 +81,7 @@ func (decoder *Decoder) Decode() (packets [][]byte, err error) { data, err = decoder.pr.ReadPacket() } if err != nil { - return nil, fmt.Errorf("read batch: %w", err) + return nil, &CompressionError{Op: "read batch", Err: err} } if len(data) == 0 { return nil, nil @@ -95,27 +90,19 @@ func (decoder *Decoder) Decode() (packets [][]byte, err error) { return nil, fmt.Errorf("decode batch: invalid header %x, expected %x", data[0], header) } data = data[1:] - if decoder.encrypt != nil { - decoder.encrypt.decrypt(data) - if err := decoder.encrypt.verify(data); err != nil { + if decoder.encryption != nil { + decoder.encryption.Decrypt(data) + if err := decoder.encryption.Verify(data); err != nil { // The packet did not have a correct checksum. - return nil, fmt.Errorf("verify batch: %w", err) + return nil, &CompressionError{Op: "verify batch", Err: err} } data = data[:len(data)-8] } - if decoder.decompress { - if data[0] == 0xff { - data = data[1:] - } else { - compression, ok := CompressionByID(uint16(data[0])) - if !ok { - return nil, fmt.Errorf("decompress batch: unknown compression algorithm %v", data[0]) - } - data, err = compression.Decompress(data[1:]) - if err != nil { - return nil, fmt.Errorf("decompress batch: %w", err) - } + if decoder.compression != nil { + data, err = decoder.compression.Decompress(data) + if err != nil { + return nil, &CompressionError{Op: "decompress batch", Err: err} } } @@ -123,7 +110,7 @@ func (decoder *Decoder) Decode() (packets [][]byte, err error) { for b.Len() != 0 { var length uint32 if err := protocol.Varuint32(b, &length); err != nil { - return nil, fmt.Errorf("decode batch: read packet length: %w", err) + return nil, &CompressionError{Op: "decode batch: read packet length", Err: err} } packets = append(packets, b.Next(int(length))) } diff --git a/minecraft/protocol/packet/encoder.go b/minecraft/protocol/packet/encoder.go index f3ccef8a..3906cc5d 100644 --- a/minecraft/protocol/packet/encoder.go +++ b/minecraft/protocol/packet/encoder.go @@ -2,9 +2,6 @@ package packet import ( "bytes" - "crypto/aes" - "crypto/cipher" - "fmt" "github.com/sandertv/gophertunnel/minecraft/internal" "io" ) @@ -15,7 +12,7 @@ type Encoder struct { w io.Writer compression Compression - encrypt *encrypt + encryption Encryption } // NewEncoder returns a new Encoder for the io.Writer passed. Each final packet produced by the Encoder is @@ -26,11 +23,8 @@ func NewEncoder(w io.Writer) *Encoder { // EnableEncryption enables encryption for the Encoder using the secret key bytes passed. Each packet sent // after encryption is enabled will be encrypted. -func (encoder *Encoder) EnableEncryption(keyBytes [32]byte) { - block, _ := aes.NewCipher(keyBytes[:]) - first12 := append([]byte(nil), keyBytes[:12]...) - stream := cipher.NewCTR(block, append(first12, 0, 0, 0, 2)) - encoder.encrypt = newEncrypt(keyBytes[:], stream) +func (encoder *Encoder) EnableEncryption(encryption Encryption) { + encoder.encryption = encryption } // EnableCompression enables compression for the Encoder. @@ -52,32 +46,30 @@ func (encoder *Encoder) Encode(packets [][]byte) error { for _, packet := range packets { // Each packet is prefixed with a varuint32 specifying the length of the packet. if err := writeVaruint32(buf, uint32(len(packet)), l); err != nil { - return fmt.Errorf("encode batch: write packet length: %w", err) + return &CompressionError{Op: "encode batch: write packet length", Err: err} } if _, err := buf.Write(packet); err != nil { - return fmt.Errorf("encode batch: write packet payload: %w", err) + return &CompressionError{Op: "encode batch: write packet payload", Err: err} } } data := buf.Bytes() - prepend := []byte{header} if encoder.compression != nil { - prepend = append(prepend, byte(encoder.compression.EncodeCompression())) var err error data, err = encoder.compression.Compress(data) if err != nil { - return fmt.Errorf("compress batch: %w", err) + return &CompressionError{Op: "compress batch", Err: err} } } - data = append(prepend, data...) - if encoder.encrypt != nil { + data = append([]byte{header}, data...) + if encoder.encryption != nil { // If the encryption session is not nil, encryption is enabled, meaning we should encrypt the // compressed data of this packet. - data = encoder.encrypt.encrypt(data) + data = encoder.encryption.Encrypt(data) } if _, err := encoder.w.Write(data); err != nil { - return fmt.Errorf("write batch: %w", err) + return &CompressionError{Op: "write batch", Err: err} } return nil } diff --git a/minecraft/protocol/packet/encryption.go b/minecraft/protocol/packet/encryption.go index af25ee21..a803ce34 100644 --- a/minecraft/protocol/packet/encryption.go +++ b/minecraft/protocol/packet/encryption.go @@ -2,75 +2,89 @@ package packet import ( "bytes" + "crypto/aes" "crypto/cipher" "crypto/sha256" "encoding/binary" "fmt" ) -// encrypt holds an encryption session with several fields required to encrypt and/or decrypt incoming +// Encryption represents an interface for encrypting, decrypting, and verifying batches of data. +type Encryption interface { + // Encrypt encrypts the data passed, adding the packet checksum at the end of it before encrypting it. + Encrypt(data []byte) []byte + // Decrypt decrypts the data passed. It does not verify the packet checksum. Verifying the checksum should be + // done using (Encryption).Verify(data). + Decrypt(data []byte) + // Verify verifies the packet checksum of the decrypted data passed. If successful, nil is returned. Otherwise, + // an error is returned describing the invalid checksum. + Verify(data []byte) error +} + +// ctr holds an encryption session with several fields required to encryption and/or decrypt incoming // packets. It may be initialised using secret key bytes computed using the shared secret produced with a // private and a public ECDSA key. -type encrypt struct { +type ctr struct { sendCounter uint64 buf [8]byte keyBytes []byte stream cipher.Stream } -// newEncrypt returns a new encryption 'session' using the secret key bytes passed. The session has its cipher -// block and IV prepared so that it may be used to decrypt and encrypt data. -func newEncrypt(keyBytes []byte, stream cipher.Stream) *encrypt { - return &encrypt{keyBytes: keyBytes, stream: stream} +// NewCTREncryption returns a new CTR encryption 'session' using the secret key bytes passed. The session has its cipher +// block and IV prepared so that it may be used to decrypt and encryption data. +func NewCTREncryption(keyBytes []byte) Encryption { + block, _ := aes.NewCipher(keyBytes[:]) + first12 := append([]byte(nil), keyBytes[:12]...) + stream := cipher.NewCTR(block, append(first12, 0, 0, 0, 2)) + return &ctr{keyBytes: keyBytes, stream: stream} } -// encrypt encrypts the data passed, adding the packet checksum at the end of it before CFB8 encrypting it. -func (encrypt *encrypt) encrypt(data []byte) []byte { +// Encrypt ... +func (c *ctr) Encrypt(data []byte) []byte { // We first write the current send counter to a buffer and use it to produce a packet checksum. - binary.LittleEndian.PutUint64(encrypt.buf[:], encrypt.sendCounter) - encrypt.sendCounter++ + binary.LittleEndian.PutUint64(c.buf[:], c.sendCounter) + c.sendCounter++ // We produce a hash existing of the send counter, packet data and key bytes. hash := sha256.New() - hash.Write(encrypt.buf[:]) + hash.Write(c.buf[:]) hash.Write(data[1:]) - hash.Write(encrypt.keyBytes) + hash.Write(c.keyBytes) - // We add the first 8 bytes of the checksum to the data and encrypt it. + // We add the first 8 bytes of the checksum to the data and encryption it. data = append(data, hash.Sum(nil)[:8]...) - encrypt.stream.XORKeyStream(data[1:], data[1:]) + c.stream.XORKeyStream(data[1:], data[1:]) return data } -// decrypt decrypts the data passed. It does not verify the packet checksum. Verifying the checksum should be -// done using encrypt.verify(data). -func (encrypt *encrypt) decrypt(data []byte) { - encrypt.stream.XORKeyStream(data, data) +// Decrypt ... +func (c *ctr) Decrypt(data []byte) { + c.stream.XORKeyStream(data, data) } -// verify verifies the packet checksum of the decrypted data passed. If successful, nil is returned. Otherwise -// an error is returned describing the invalid checksum. -func (encrypt *encrypt) verify(data []byte) error { +// Verify ... +func (c *ctr) Verify(data []byte) error { if len(data) < 8 { return fmt.Errorf("encrypted packet must be at least 8 bytes long, got %v", len(data)) } sum := data[len(data)-8:] // We first write the current send counter to a buffer and use it to produce a packet checksum. - binary.LittleEndian.PutUint64(encrypt.buf[:], encrypt.sendCounter) - encrypt.sendCounter++ + binary.LittleEndian.PutUint64(c.buf[:], c.sendCounter) + c.sendCounter++ // We produce a hash existing of the send counter, packet data and key bytes. hash := sha256.New() - hash.Write(encrypt.buf[:]) + hash.Write(c.buf[:]) hash.Write(data[:len(data)-8]) - hash.Write(encrypt.keyBytes) + hash.Write(c.keyBytes) ourSum := hash.Sum(nil)[:8] // Finally we check if the original sum was equal to the sum we just produced. if !bytes.Equal(sum, ourSum) { - return fmt.Errorf("invalid checksum of packet %v: expected %x, got %x", encrypt.sendCounter-1, ourSum, sum) + return fmt.Errorf("invalid checksum of packet %v: expected %x, got %x", c.sendCounter-1, ourSum, sum) } return nil } diff --git a/minecraft/protocol/packet/player_auth_input.go b/minecraft/protocol/packet/player_auth_input.go index ae4c210a..4a2a5732 100644 --- a/minecraft/protocol/packet/player_auth_input.go +++ b/minecraft/protocol/packet/player_auth_input.go @@ -161,11 +161,11 @@ func (pk *PlayerAuthInput) Marshal(io protocol.IO) { if pk.InputData&InputFlagPerformItemStackRequest != 0 { protocol.Single(io, &pk.ItemStackRequest) } - + if pk.InputData&InputFlagPerformBlockActions != 0 { protocol.SliceVarint32Length(io, &pk.BlockActions) } - + if pk.InputData&InputFlagClientPredictedVehicle != 0 { io.Vec2(&pk.VehicleRotation) io.Varint64(&pk.ClientPredictedVehicle) diff --git a/minecraft/protocol/reader.go b/minecraft/protocol/reader.go index 86bbf8be..24c7480f 100644 --- a/minecraft/protocol/reader.go +++ b/minecraft/protocol/reader.go @@ -33,6 +33,19 @@ func NewReader(r interface { return &Reader{r: r, shieldID: shieldID, limitsEnabled: enableLimits} } +type Reads interface { + Reads() bool + LimitsEnabled() bool +} + +func (r *Reader) Reads() bool { + return true +} + +func (r *Reader) LimitsEnabled() bool { + return r.limitsEnabled +} + // Uint8 reads a uint8 from the underlying buffer. func (r *Reader) Uint8(x *uint8) { var err error @@ -499,7 +512,7 @@ func (r *Reader) Recipe(x *Recipe) { r.UnknownEnumOption(recipeType, "crafting data recipe type") return } - (*x).Unmarshal(r) + (*x).Marshal(r) } // EventType reads an Event's type from the reader. diff --git a/minecraft/protocol/recipe.go b/minecraft/protocol/recipe.go index 73ac8e3d..c0a91693 100644 --- a/minecraft/protocol/recipe.go +++ b/minecraft/protocol/recipe.go @@ -96,10 +96,7 @@ const ( // Recipe represents a recipe that may be sent in a CraftingData packet to let the client know what recipes // are available server-side. type Recipe interface { - // Marshal encodes the recipe data to its binary representation into buf. - Marshal(w *Writer) - // Unmarshal decodes a serialised recipe from Reader r into the recipe instance. - Unmarshal(r *Reader) + Marshaler } // lookupRecipe looks up the Recipe for a recipe type. False is returned if not @@ -321,113 +318,54 @@ type SmithingTrimRecipe struct { } // Marshal ... -func (recipe *ShapelessRecipe) Marshal(w *Writer) { - marshalShapeless(w, recipe) -} - -// Unmarshal ... -func (recipe *ShapelessRecipe) Unmarshal(r *Reader) { +func (recipe *ShapelessRecipe) Marshal(r IO) { marshalShapeless(r, recipe) } // Marshal ... -func (recipe *ShulkerBoxRecipe) Marshal(w *Writer) { - marshalShapeless(w, &recipe.ShapelessRecipe) -} - -// Unmarshal ... -func (recipe *ShulkerBoxRecipe) Unmarshal(r *Reader) { +func (recipe *ShulkerBoxRecipe) Marshal(r IO) { marshalShapeless(r, &recipe.ShapelessRecipe) } // Marshal ... -func (recipe *ShapelessChemistryRecipe) Marshal(w *Writer) { - marshalShapeless(w, &recipe.ShapelessRecipe) -} - -// Unmarshal ... -func (recipe *ShapelessChemistryRecipe) Unmarshal(r *Reader) { +func (recipe *ShapelessChemistryRecipe) Marshal(r IO) { marshalShapeless(r, &recipe.ShapelessRecipe) } // Marshal ... -func (recipe *ShapedRecipe) Marshal(w *Writer) { - marshalShaped(w, recipe) -} - -// Unmarshal ... -func (recipe *ShapedRecipe) Unmarshal(r *Reader) { +func (recipe *ShapedRecipe) Marshal(r IO) { marshalShaped(r, recipe) } // Marshal ... -func (recipe *ShapedChemistryRecipe) Marshal(w *Writer) { - marshalShaped(w, &recipe.ShapedRecipe) -} - -// Unmarshal ... -func (recipe *ShapedChemistryRecipe) Unmarshal(r *Reader) { +func (recipe *ShapedChemistryRecipe) Marshal(r IO) { marshalShaped(r, &recipe.ShapedRecipe) } // Marshal ... -func (recipe *FurnaceRecipe) Marshal(w *Writer) { - w.Varint32(&recipe.InputType.NetworkID) - w.Item(&recipe.Output) - w.String(&recipe.Block) -} - -// Unmarshal ... -func (recipe *FurnaceRecipe) Unmarshal(r *Reader) { +func (recipe *FurnaceRecipe) Marshal(r IO) { r.Varint32(&recipe.InputType.NetworkID) r.Item(&recipe.Output) r.String(&recipe.Block) } // Marshal ... -func (recipe *FurnaceDataRecipe) Marshal(w *Writer) { - w.Varint32(&recipe.InputType.NetworkID) - aux := int32(recipe.InputType.MetadataValue) - w.Varint32(&aux) - w.Item(&recipe.Output) - w.String(&recipe.Block) -} - -// Unmarshal ... -func (recipe *FurnaceDataRecipe) Unmarshal(r *Reader) { - var dataValue int32 +func (recipe *FurnaceDataRecipe) Marshal(r IO) { r.Varint32(&recipe.InputType.NetworkID) - r.Varint32(&dataValue) - recipe.InputType.MetadataValue = uint32(dataValue) + aux := int32(recipe.InputType.MetadataValue) + r.Varint32(&aux) r.Item(&recipe.Output) r.String(&recipe.Block) } // Marshal ... -func (recipe *MultiRecipe) Marshal(w *Writer) { - w.UUID(&recipe.UUID) - w.Varuint32(&recipe.RecipeNetworkID) -} - -// Unmarshal ... -func (recipe *MultiRecipe) Unmarshal(r *Reader) { +func (recipe *MultiRecipe) Marshal(r IO) { r.UUID(&recipe.UUID) r.Varuint32(&recipe.RecipeNetworkID) } // Marshal ... -func (recipe *SmithingTransformRecipe) Marshal(w *Writer) { - w.String(&recipe.RecipeID) - w.ItemDescriptorCount(&recipe.Template) - w.ItemDescriptorCount(&recipe.Base) - w.ItemDescriptorCount(&recipe.Addition) - w.Item(&recipe.Result) - w.String(&recipe.Block) - w.Varuint32(&recipe.RecipeNetworkID) -} - -// Unmarshal ... -func (recipe *SmithingTransformRecipe) Unmarshal(r *Reader) { +func (recipe *SmithingTransformRecipe) Marshal(r IO) { r.String(&recipe.RecipeID) r.ItemDescriptorCount(&recipe.Template) r.ItemDescriptorCount(&recipe.Base) @@ -438,17 +376,7 @@ func (recipe *SmithingTransformRecipe) Unmarshal(r *Reader) { } // Marshal ... -func (recipe *SmithingTrimRecipe) Marshal(w *Writer) { - w.String(&recipe.RecipeID) - w.ItemDescriptorCount(&recipe.Template) - w.ItemDescriptorCount(&recipe.Base) - w.ItemDescriptorCount(&recipe.Addition) - w.String(&recipe.Block) - w.Varuint32(&recipe.RecipeNetworkID) -} - -// Unmarshal ... -func (recipe *SmithingTrimRecipe) Unmarshal(r *Reader) { +func (recipe *SmithingTrimRecipe) Marshal(r IO) { r.String(&recipe.RecipeID) r.ItemDescriptorCount(&recipe.Template) r.ItemDescriptorCount(&recipe.Base) diff --git a/minecraft/raknet.go b/minecraft/raknet.go index 2412846a..066321ea 100644 --- a/minecraft/raknet.go +++ b/minecraft/raknet.go @@ -3,26 +3,22 @@ package minecraft import ( "context" "github.com/sandertv/go-raknet" + "github.com/sandertv/gophertunnel/minecraft/protocol/packet" "net" ) // RakNet is an implementation of a RakNet v10 Network. type RakNet struct{} -// DialContext ... -func (r RakNet) DialContext(ctx context.Context, address string) (net.Conn, error) { - return raknet.DialContext(ctx, address) +func (RakNet) DialContext(ctx context.Context, a string) (net.Conn, error) { + return raknet.DialContext(ctx, a) } - -// PingContext ... -func (r RakNet) PingContext(ctx context.Context, address string) (response []byte, err error) { - return raknet.PingContext(ctx, address) +func (RakNet) PingContext(ctx context.Context, a string) ([]byte, error) { + return raknet.PingContext(ctx, a) } +func (RakNet) Listen(address string) (NetworkListener, error) { return raknet.Listen(address) } -// Listen ... -func (r RakNet) Listen(address string) (NetworkListener, error) { - return raknet.Listen(address) -} +func (RakNet) Compression(net.Conn) packet.Compression { return packet.FlateCompression } // init registers the RakNet network. func init() {