Skip to content

Commit

Permalink
Multi-protocol support (#131)
Browse files Browse the repository at this point in the history
  • Loading branch information
JustTalDevelops committed Jun 7, 2022
1 parent 56ec96a commit cf29758
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 26 deletions.
73 changes: 53 additions & 20 deletions minecraft/conn.go
Expand Up @@ -57,9 +57,11 @@ type Conn struct {
log *log.Logger
authEnabled bool

pool packet.Pool
enc *packet.Encoder
dec *packet.Decoder
proto Protocol
acceptedProto []Protocol
pool packet.Pool
enc *packet.Encoder
dec *packet.Decoder

identityData login.IdentityData
clientData login.ClientData
Expand Down Expand Up @@ -125,6 +127,8 @@ type Conn struct {
disconnectMessage atomic.String

shieldID atomic.Int32

additional chan packet.Packet
}

// newConn creates a new Minecraft connection for the net.Conn passed, reading and writing compressed
Expand All @@ -135,9 +139,9 @@ func newConn(netConn net.Conn, key *ecdsa.PrivateKey, log *log.Logger) *Conn {
conn := &Conn{
enc: packet.NewEncoder(netConn),
dec: packet.NewDecoder(netConn),
pool: packet.NewPool(),
salt: make([]byte, 16),
packets: make(chan *packetData, 8),
additional: make(chan packet.Packet, 16),
close: make(chan struct{}),
spawn: make(chan struct{}),
conn: netConn,
Expand Down Expand Up @@ -297,7 +301,7 @@ func (conn *Conn) WritePacket(pk packet.Packet) error {

buf := internal.BufferPool.Get().(*bytes.Buffer)
defer func() {
// Reset the buffer so we can return it to the buffer pool safely.
// Reset the buffer, so we can return it to the buffer pool safely.
buf.Reset()
internal.BufferPool.Put(buf)
}()
Expand All @@ -306,12 +310,14 @@ func (conn *Conn) WritePacket(pk packet.Packet) error {
_ = conn.hdr.Write(buf)
l := buf.Len()

pk.Marshal(protocol.NewWriter(buf, conn.shieldID.Load()))
if conn.packetFunc != nil {
conn.packetFunc(*conn.hdr, buf.Bytes()[l:], conn.LocalAddr(), conn.RemoteAddr())
}
for _, converted := range conn.proto.ConvertFromLatest(pk, conn) {
converted.Marshal(protocol.NewWriter(buf, conn.shieldID.Load()))

conn.bufferedSend = append(conn.bufferedSend, append([]byte(nil), buf.Bytes()...))
if conn.packetFunc != nil {
conn.packetFunc(*conn.hdr, buf.Bytes()[l:], conn.LocalAddr(), conn.RemoteAddr())
}
conn.bufferedSend = append(conn.bufferedSend, append([]byte(nil), buf.Bytes()...))
}
return nil
}

Expand All @@ -322,13 +328,19 @@ func (conn *Conn) WritePacket(pk packet.Packet) error {
// If the packet read was not implemented, a *packet.Unknown is returned, containing the raw payload of the
// packet read.
func (conn *Conn) ReadPacket() (pk packet.Packet, err error) {
if len(conn.additional) > 0 {
return <-conn.additional, nil
}
if data, ok := conn.takeDeferredPacket(); ok {
pk, err := data.decode(conn)
if err != nil {
conn.log.Println(err)
return conn.ReadPacket()
}
return pk, nil
for _, additional := range pk[1:] {
conn.additional <- additional
}
return pk[0], nil
}

select {
Expand All @@ -342,7 +354,10 @@ func (conn *Conn) ReadPacket() (pk packet.Packet, err error) {
conn.log.Println(err)
return conn.ReadPacket()
}
return pk, nil
for _, additional := range pk[1:] {
conn.additional <- additional
}
return pk[0], nil
}
}

Expand Down Expand Up @@ -521,9 +536,9 @@ func (conn *Conn) receive(data []byte) error {
}
if pkData.h.PacketID == packet.IDDisconnect {
// We always handle disconnect packets and close the connection if one comes in.
pk, _ := pkData.decode(conn)

conn.disconnectMessage.Store(pk.(*packet.Disconnect).Message)
if pks, err := pkData.decode(conn); err != nil {
conn.disconnectMessage.Store(pks[0].(*packet.Disconnect).Message)
}
_ = conn.Close()
return nil
}
Expand All @@ -550,11 +565,11 @@ func (conn *Conn) handle(pkData *packetData) error {
for _, id := range conn.expectedIDs.Load().([]uint32) {
if id == pkData.h.PacketID {
// If the packet was expected, so we handle it right now.
pk, err := pkData.decode(conn)
pks, err := pkData.decode(conn)
if err != nil {
return err
}
return conn.handlePacket(pk)
return conn.handleMultiple(pks)
}
}
// This is not the packet we expected next in the login sequence. We push it back so that it may
Expand All @@ -563,6 +578,18 @@ func (conn *Conn) handle(pkData *packetData) error {
return nil
}

// handleMultiple handles multiple packets and returns an error if at least one of those packets could not be handled
// successfully.
func (conn *Conn) handleMultiple(pks []packet.Packet) error {
var err error
for _, pk := range pks {
if e := conn.handlePacket(pk); e != nil {
err = e
}
}
return err
}

// handlePacket handles an incoming packet. It returns an error if any of the data found in the packet was not
// valid or if handling failed for any other reason.
func (conn *Conn) handlePacket(pk packet.Packet) error {
Expand Down Expand Up @@ -626,9 +653,15 @@ func (conn *Conn) handleLogin(pk *packet.Login) error {
_ = conn.WritePacket(&packet.Disconnect{Message: text.Colourf("<red>You must be logged in with XBOX Live to join.</red>")})
return fmt.Errorf("connection %v was not authenticated to XBOX Live", conn.RemoteAddr())
}
// Make sure protocol numbers match.
if pk.ClientProtocol != protocol.CurrentProtocol {
// By default we assume the client is outdated.

for _, pro := range conn.acceptedProto {
if pro.ID() == pk.ClientProtocol {
conn.proto = pro
conn.pool = pro.Packets()
break
}
}
if conn.proto == nil {
status := packet.PlayStatusLoginFailedClient
if pk.ClientProtocol > protocol.CurrentProtocol {
// The server is outdated in this case, so we have to change the status we send.
Expand Down
13 changes: 12 additions & 1 deletion minecraft/dial.go
Expand Up @@ -55,6 +55,12 @@ type Dialer struct {
// from which the packet originated, and the destination address.
PacketFunc func(header packet.Header, payload []byte, src, dst net.Addr)

// Protocol is the Protocol version used to communicate with the target server. By default, this field is
// set to the current protocol as implemented in the minecraft/protocol package. Note that packets written
// to and read from the Conn are always any of those found in the protocol/packet package, as packets
// are converted from and to this Protocol.
Protocol Protocol

// EnableClientCache, if set to true, enables the client blob cache for the client. This means that the
// server will send chunks as blobs, which may be saved by the client so that chunks don't have to be
// transmitted every time, resulting in less network transmission.
Expand Down Expand Up @@ -128,6 +134,9 @@ func (d Dialer) DialContext(ctx context.Context, network, address string) (conn
if d.ErrorLog == nil {
d.ErrorLog = log.New(os.Stderr, "", log.LstdFlags)
}
if d.Protocol == nil {
d.Protocol = proto{}
}
var netConn net.Conn

switch network {
Expand All @@ -150,6 +159,8 @@ func (d Dialer) DialContext(ctx context.Context, network, address string) (conn
return nil, err
}
conn = newConn(netConn, key, d.ErrorLog)
conn.proto = d.Protocol
conn.pool = conn.proto.Packets()
conn.identityData = d.IdentityData
conn.clientData = d.ClientData
conn.packetFunc = d.PacketFunc
Expand Down Expand Up @@ -185,7 +196,7 @@ func (d Dialer) DialContext(ctx context.Context, network, address string) (conn
go listenConn(conn, d.ErrorLog, c)

conn.expect(packet.IDServerToClientHandshake, packet.IDPlayStatus)
if err := conn.WritePacket(&packet.Login{ConnectionRequest: request, ClientProtocol: protocol.CurrentProtocol}); err != nil {
if err := conn.WritePacket(&packet.Login{ConnectionRequest: request, ClientProtocol: d.Protocol.ID()}); err != nil {
return nil, err
}
_ = conn.Flush()
Expand Down
12 changes: 11 additions & 1 deletion minecraft/listener.go
Expand Up @@ -38,6 +38,11 @@ type ListenConfig struct {
// ListenerStatusProvider, is used as provider.
StatusProvider ServerStatusProvider

// AcceptedProtocols is a slice of Protocol accepted by a Listener created with this ListenConfig. The current
// Protocol is always added to this slice. Clients with a protocol version that is not present in this slice will
// be disconnected.
AcceptedProtocols []Protocol

// ResourcePacks is a slice of resource packs that the listener may hold. Each client will be asked to
// download these resource packs upon joining.
// This field should not be edited during runtime of the Listener to avoid race conditions. Use
Expand Down Expand Up @@ -125,7 +130,7 @@ func Listen(network, address string) (*Listener, error) {

// Accept accepts a fully connected (on Minecraft layer) connection which is ready to receive and send
// packets. It is recommended to cast the net.Conn returned to a *minecraft.Conn so that it is possible to
// use the conn.ReadPacket() and conn.WritePacket() methods.
// use the Conn.ReadPacket() and Conn.WritePacket() methods.
// Accept returns an error if the listener is closed.
func (listener *Listener) Accept() (net.Conn, error) {
conn, ok := <-listener.incoming
Expand Down Expand Up @@ -205,6 +210,11 @@ func (listener *Listener) listen() {
// accepted once its login sequence is complete.
func (listener *Listener) createConn(netConn net.Conn) {
conn := newConn(netConn, listener.key, listener.cfg.ErrorLog)
conn.acceptedProto = append(listener.cfg.AcceptedProtocols, proto{})
// 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()

conn.packetFunc = listener.cfg.PacketFunc
conn.texturePacksRequired = listener.cfg.TexturePacksRequired
conn.resourcePacks = listener.cfg.ResourcePacks
Expand Down
12 changes: 8 additions & 4 deletions minecraft/packet.go
Expand Up @@ -31,9 +31,13 @@ func parseData(data []byte, conn *Conn) (*packetData, error) {
}

// decode decodes the packet payload held in the packetData and returns the packet.Packet decoded.
func (p *packetData) decode(conn *Conn) (pk packet.Packet, err error) {
func (p *packetData) decode(conn *Conn) ([]packet.Packet, error) {
// Attempt to fetch the packet with the right packet ID from the pool.
pkFunc, ok := conn.pool[p.h.PacketID]
var (
pkFunc, ok = conn.pool[p.h.PacketID]
pk packet.Packet
err error
)
if !ok {
// No packet with the ID. This may be a custom packet of some sorts.
pk = &packet.Unknown{PacketID: p.h.PacketID}
Expand All @@ -49,7 +53,7 @@ func (p *packetData) decode(conn *Conn) (pk packet.Packet, err error) {
}()
pk.Unmarshal(r)
if p.payload.Len() != 0 {
return pk, fmt.Errorf("%T: %v unread bytes left: 0x%x", pk, p.payload.Len(), p.payload.Bytes())
err = fmt.Errorf("%T: %v unread bytes left: 0x%x", pk, p.payload.Len(), p.payload.Bytes())
}
return pk, nil
return conn.proto.ConvertToLatest(pk, conn), err
}
43 changes: 43 additions & 0 deletions minecraft/protocol.go
@@ -0,0 +1,43 @@
package minecraft

import (
"github.com/sandertv/gophertunnel/minecraft/protocol"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
)

// Protocol represents the Minecraft protocol used to communicate over network. It comprises a unique set of packets
// that may be changed in any version.
// Protocol specifically handles the conversion of packets between the most recent protocol (as in the
// minecraft/protocol package) and the protocol as specified in Protocol.
type Protocol interface {
// ID returns the unique ID of the Protocol. It generally goes up for every new Minecraft version released.
ID() int32
// Ver returns the Minecraft version associated with this Protocol, such as "1.18.10".
Ver() string
// Packets returns a packet.Pool with all packets registered for this Protocol. It is used to lookup packets by a
// packet ID.
Packets() packet.Pool
// ConvertToLatest converts a packet.Packet obtained from the other end of a Conn to a slice of packet.Packets from
// the latest protocol. Any packet.Packet implementation in the packet.Pool obtained through a call to Packets that
// is not identical to the most recent version of that packet.Packet must be converted to the most recent version of
// that packet adequately in this function. ConvertToLatest returns pk if the packet.Packet was unchanged in this
// version compared to the latest. Note that packets must also be converted if only their ID changes.
ConvertToLatest(pk packet.Packet, conn *Conn) []packet.Packet
// ConvertFromLatest converts a packet.Packet of the most recent Protocol to a slice of packet.Packets of this
// specific Protocol. ConvertFromLatest must be synonymous to ConvertToLatest, in that it should convert any
// packet.Packet to the correct one from the packet.Pool returned through a call to Packets if its payload or ID was
// changed in this Protocol compared to the latest one.
ConvertFromLatest(pk packet.Packet, conn *Conn) []packet.Packet
}

// proto is the default Protocol implementation. It returns the current protocol, version and packet pool and does not
// 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() packet.Pool { return packet.NewPool() }
func (p proto) ConvertToLatest(pk packet.Packet, _ *Conn) []packet.Packet { return []packet.Packet{pk} }
func (p proto) ConvertFromLatest(pk packet.Packet, _ *Conn) []packet.Packet {
return []packet.Packet{pk}
}

0 comments on commit cf29758

Please sign in to comment.