Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/multi proto #131

Merged
merged 4 commits into from Jun 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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}
}
23 changes: 14 additions & 9 deletions minecraft/text/colour.go
Expand Up @@ -52,20 +52,14 @@ func (e *enc) process(tok html.Token) {
}
switch tok.Type {
case html.TextToken:
for _, s := range e.formatStack {
e.w.WriteString(s)
}
e.w.WriteString(tok.Data)
if len(e.formatStack) != 0 {
e.w.WriteString(reset)
}
e.writeText(tok.Data)
case html.StartTagToken:
if format, ok := strMap[tok.Data]; ok {
e.formatStack = append(e.formatStack, format)
return
}
// Not a known colour, so just write the token as a string.
e.w.WriteString("<" + tok.Data + ">")
e.writeText("<" + tok.Data + ">")
case html.EndTagToken:
for i, format := range e.formatStack {
if f, ok := strMap[tok.Data]; ok && f == format {
Expand All @@ -74,6 +68,17 @@ func (e *enc) process(tok html.Token) {
}
}
// Not a known colour, so just write the token as a string.
e.w.WriteString("</" + tok.Data + ">")
e.writeText("</" + tok.Data + ">")
}
}

// writeText writes text to the encoder by encasing it in the current format stack.
func (e *enc) writeText(s string) {
for _, format := range e.formatStack {
e.w.WriteString(format)
}
e.w.WriteString(s)
if len(e.formatStack) != 0 {
e.w.WriteString(reset)
}
}