-
Notifications
You must be signed in to change notification settings - Fork 99
/
conn.go
1391 lines (1270 loc) · 56.9 KB
/
conn.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package minecraft
import (
"bytes"
"context"
"crypto/ecdsa"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"fmt"
"github.com/google/uuid"
"github.com/sandertv/go-raknet"
"github.com/sandertv/gophertunnel/internal"
"github.com/sandertv/gophertunnel/minecraft/nbt"
"github.com/sandertv/gophertunnel/minecraft/protocol"
"github.com/sandertv/gophertunnel/minecraft/protocol/login"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
"github.com/sandertv/gophertunnel/minecraft/resource"
"github.com/sandertv/gophertunnel/minecraft/text"
"go.uber.org/atomic"
"gopkg.in/square/go-jose.v2"
"gopkg.in/square/go-jose.v2/jwt"
"io"
"log"
"net"
"strings"
"sync"
"time"
)
// exemptedResourcePack is a resource pack that is exempted from being downloaded. These packs may be directly
// applied by sending them in the ResourcePackStack packet.
type exemptedResourcePack struct {
uuid string
version string
}
// exemptedPacks is a list of all resource packs that do not need to be downloaded, but may always be applied
// in the ResourcePackStack packet.
var exemptedPacks = []exemptedResourcePack{
{
uuid: "0fba4063-dba1-4281-9b89-ff9390653530",
version: "1.0.0",
},
}
// Conn represents a Minecraft (Bedrock Edition) connection over a specific net.Conn transport layer. Its
// methods (Read, Write etc.) are safe to be called from multiple goroutines simultaneously, but ReadPacket
// must not be called on multiple goroutines simultaneously.
type Conn struct {
// once is used to ensure the Conn is closed only a single time. It protects the channel below from being
// closed multiple times.
once sync.Once
close chan struct{}
conn net.Conn
log *log.Logger
authEnabled bool
proto Protocol
acceptedProto []Protocol
pool packet.Pool
enc *packet.Encoder
dec *packet.Decoder
compression packet.Compression
identityData login.IdentityData
clientData login.ClientData
gameData GameData
gameDataReceived atomic.Bool
// privateKey is the private key of this end of the connection. Each connection, regardless of which side
// the connection is on, server or client, has a unique private key generated.
privateKey *ecdsa.PrivateKey
// salt is a 16 byte long randomly generated byte slice which is only used if the Conn is a server sided
// connection. It is otherwise left unused.
salt []byte
// packets is a channel of byte slices containing serialised packets that are coming in from the other
// side of the connection.
packets chan *packetData
deferredPacketMu sync.Mutex
// deferredPackets is a list of packets that were pushed back during the login sequence because they
// were not used by the connection yet. These packets are read the first when calling to Read or
// ReadPacket after being connected.
deferredPackets []*packetData
readDeadline <-chan time.Time
sendMu sync.Mutex
// bufferedSend is a slice of byte slices containing packets that are 'written'. They are buffered until
// they are sent each 20th of a second.
bufferedSend [][]byte
hdr *packet.Header
// readyToLogin is a bool indicating if the connection is ready to login. This is used to ensure that the client
// has received the relevant network settings before the login sequence starts.
readyToLogin bool
// loggedIn is a bool indicating if the connection was logged in. It is set to true after the entire login
// sequence is completed.
loggedIn bool
// spawn is a bool channel indicating if the connection is currently waiting for its spawning in
// the world: It is completing a sequence that will result in the spawning.
spawn chan struct{}
waitingForSpawn atomic.Bool
// expectedIDs is a slice of packet identifiers that are next expected to arrive, until the connection is
// logged in.
expectedIDs atomic.Value
packMu sync.Mutex
// resourcePacks is a slice of resource packs that the listener may hold. Each client will be asked to
// download these resource packs upon joining.
resourcePacks []*resource.Pack
// biomes is a map of biome definitions that the listener may hold. Each client will be sent these biome
// definitions upon joining.
biomes map[string]any
// texturePacksRequired specifies if clients that join must accept the texture pack in order for them to
// be able to join the server. If they don't accept, they can only leave the server.
texturePacksRequired bool
packQueue *resourcePackQueue
// downloadResourcePack is an optional function passed to a Dial() call. If set, each resource pack received
// from the server will call this function to see if it should be downloaded or not.
downloadResourcePack func(id uuid.UUID, version string) bool
// ignoredResourcePacks is a slice of resource packs that are not being downloaded due to the downloadResourcePack
// func returning false for the specific pack.
ignoredResourcePacks []exemptedResourcePack
cacheEnabled bool
// packetFunc is an optional function passed to a Dial() call. If set, each packet read from and written
// to this connection will call this function.
packetFunc func(header packet.Header, payload []byte, src, dst net.Addr)
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
// 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) *Conn {
conn := &Conn{
enc: packet.NewEncoder(netConn),
dec: packet.NewDecoder(netConn),
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,
privateKey: key,
log: log,
hdr: &packet.Header{},
proto: proto,
}
conn.expectedIDs.Store([]uint32{packet.IDRequestNetworkSettings})
_, _ = rand.Read(conn.salt)
if flushRate <= 0 {
return conn
}
go func() {
ticker := time.NewTicker(flushRate)
defer ticker.Stop()
for range ticker.C {
if err := conn.Flush(); err != nil {
_ = conn.Close()
return
}
}
}()
return conn
}
// IdentityData returns the identity data of the connection. It holds the UUID, XUID and username of the
// connected client.
func (conn *Conn) IdentityData() login.IdentityData {
return conn.identityData
}
// ClientData returns the client data the client connected with. Note that this client data may be changed
// during the session, so the data should only be used directly after connection, and should be updated after
// that by the caller.
func (conn *Conn) ClientData() login.ClientData {
return conn.clientData
}
// Authenticated returns true if the connection was authenticated through XBOX Live services.
func (conn *Conn) Authenticated() bool {
return conn.IdentityData().XUID != ""
}
// GameData returns specific game data set to the connection for the player to be initialised with. If the
// Conn is obtained using Listen, this game data may be set to the Listener. If obtained using Dial, the data
// is obtained from the server.
func (conn *Conn) GameData() GameData {
return conn.gameData
}
// StartGame starts the game for a client that connected to the server. StartGame should be called for a Conn
// obtained using a minecraft.Listener. The game data passed will be used to spawn the player in the world of
// the server. To spawn a Conn obtained from a call to minecraft.Dial(), use Conn.DoSpawn().
func (conn *Conn) StartGame(data GameData) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
return conn.StartGameContext(ctx, data)
}
// StartGameTimeout starts the game for a client that connected to the server, returning an error if the
// connection is not yet fully connected while the timeout expires.
// StartGameTimeout should be called for a Conn obtained using a minecraft.Listener. The game data passed will
// be used to spawn the player in the world of the server. To spawn a Conn obtained from a call to
// minecraft.Dial(), use Conn.DoSpawn().
func (conn *Conn) StartGameTimeout(data GameData, timeout time.Duration) error {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
return conn.StartGameContext(ctx, data)
}
// StartGameContext starts the game for a client that connected to the server, returning an error if the
// context is closed while spawning the client.
// StartGameContext should be called for a Conn obtained using a minecraft.Listener. The game data passed will
// be used to spawn the player in the world of the server. To spawn a Conn obtained from a call to
// minecraft.Dial(), use Conn.DoSpawn().
func (conn *Conn) StartGameContext(ctx context.Context, data GameData) error {
if conn.gameDataReceived.Load() {
panic("(*Conn).StartGame must only be called on Listener connections")
}
if data.WorldName == "" {
data.WorldName = conn.gameData.WorldName
}
conn.gameData = data
for _, item := range data.Items {
if item.Name == "minecraft:shield" {
conn.shieldID.Store(int32(item.RuntimeID))
}
}
conn.waitingForSpawn.Store(true)
conn.startGame()
select {
case <-conn.close:
return conn.closeErr("start game")
case <-ctx.Done():
return conn.wrap(ctx.Err(), "start game")
case <-conn.spawn:
// Conn was spawned successfully.
return nil
}
}
// DoSpawn starts the game for the client in the server. DoSpawn should be called for a Conn obtained using
// minecraft.Dial(). Use Conn.StartGame to spawn a Conn obtained using a minecraft.Listener.
// DoSpawn will start the spawning sequence using the game data found in conn.GameData(), which was sent
// earlier by the server.
// DoSpawn has a default timeout of 1 minute. DoSpawnContext or DoSpawnTimeout may be used for cancellation
// at any other times.
func (conn *Conn) DoSpawn() error {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
return conn.DoSpawnContext(ctx)
}
// DoSpawnTimeout starts the game for the client in the server with a timeout after which an error is
// returned if the client has not yet spawned by that time. DoSpawnTimeout should be called for a Conn
// obtained using minecraft.Dial(). Use Conn.StartGame to spawn a Conn obtained using a minecraft.Listener.
// DoSpawnTimeout will start the spawning sequence using the game data found in conn.GameData(), which was
// sent earlier by the server.
func (conn *Conn) DoSpawnTimeout(timeout time.Duration) error {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
return conn.DoSpawnContext(ctx)
}
// DoSpawnContext starts the game for the client in the server, using a specific context for cancellation.
// DoSpawnContext should be called for a Conn obtained using minecraft.Dial(). Use Conn.StartGame to spawn a
// Conn obtained using a minecraft.Listener.
// DoSpawnContext will start the spawning sequence using the game data found in conn.GameData(), which was
// sent earlier by the server.
func (conn *Conn) DoSpawnContext(ctx context.Context) error {
select {
case <-conn.close:
return conn.closeErr("do spawn")
case <-ctx.Done():
return conn.wrap(ctx.Err(), "do spawn")
case <-conn.spawn:
// Conn was spawned successfully.
return nil
}
}
// WritePacket encodes the packet passed and writes it to the Conn. The encoded data is buffered until the
// next 20th of a second, after which the data is flushed and sent over the connection.
func (conn *Conn) WritePacket(pk packet.Packet) error {
select {
case <-conn.close:
return conn.closeErr("write packet")
default:
}
conn.sendMu.Lock()
defer conn.sendMu.Unlock()
buf := internal.BufferPool.Get().(*bytes.Buffer)
defer func() {
// Reset the buffer, so we can return it to the buffer pool safely.
buf.Reset()
internal.BufferPool.Put(buf)
}()
conn.hdr.PacketID = pk.ID()
_ = conn.hdr.Write(buf)
l := buf.Len()
for _, converted := range conn.proto.ConvertFromLatest(pk, conn) {
converted.Marshal(protocol.NewWriter(buf, conn.shieldID.Load()))
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
}
// ReadPacket reads a packet from the Conn, depending on the packet ID that is found in front of the packet
// data. If a read deadline is set, an error is returned if the deadline is reached before any packet is
// received. ReadPacket must not be called on multiple goroutines simultaneously.
//
// 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()
}
if len(pk) == 0 {
return conn.ReadPacket()
}
for _, additional := range pk[1:] {
conn.additional <- additional
}
return pk[0], nil
}
select {
case <-conn.close:
return nil, conn.closeErr("read packet")
case <-conn.readDeadline:
return nil, conn.wrap(context.DeadlineExceeded, "read packet")
case data := <-conn.packets:
pk, err := data.decode(conn)
if err != nil {
conn.log.Println(err)
return conn.ReadPacket()
}
if len(pk) == 0 {
return conn.ReadPacket()
}
for _, additional := range pk[1:] {
conn.additional <- additional
}
return pk[0], nil
}
}
// ResourcePacks returns a slice of all resource packs the connection holds. For a Conn obtained using a
// Listener, this holds all resource packs set to the Listener. For a Conn obtained using Dial, the resource
// packs include all packs sent by the server connected to.
func (conn *Conn) ResourcePacks() []*resource.Pack {
return conn.resourcePacks
}
// Write writes a slice of serialised packet data to the Conn. The data is buffered until the next 20th of a
// tick, after which it is flushed to the connection. Write returns the amount of bytes written n.
func (conn *Conn) Write(b []byte) (n int, err error) {
conn.sendMu.Lock()
defer conn.sendMu.Unlock()
conn.bufferedSend = append(conn.bufferedSend, b)
return len(b), nil
}
// Read reads a packet from the connection into the byte slice passed, provided the byte slice is big enough
// to carry the full packet.
// It is recommended to use ReadPacket() rather than Read() in cases where reading is done directly.
func (conn *Conn) Read(b []byte) (n int, err error) {
if data, ok := conn.takeDeferredPacket(); ok {
if len(b) < len(data.full) {
return 0, conn.wrap(errBufferTooSmall, "read")
}
return copy(b, data.full), nil
}
select {
case <-conn.close:
return 0, conn.closeErr("read")
case <-conn.readDeadline:
return 0, conn.wrap(context.DeadlineExceeded, "read")
case data := <-conn.packets:
if len(b) < len(data.full) {
return 0, conn.wrap(errBufferTooSmall, "read")
}
return copy(b, data.full), nil
}
}
// Flush flushes the packets currently buffered by the connections to the underlying net.Conn, so that they
// are directly sent.
func (conn *Conn) Flush() error {
select {
case <-conn.close:
return conn.closeErr("flush")
default:
}
conn.sendMu.Lock()
defer conn.sendMu.Unlock()
if len(conn.bufferedSend) > 0 {
if err := conn.enc.Encode(conn.bufferedSend); err != nil && !raknet.ErrConnectionClosed(err) {
// Should never happen.
panic(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.
for i := range conn.bufferedSend {
conn.bufferedSend[i] = nil
}
// Slice the conn.bufferedSend to a length of 0 so we don't have to re-allocate space in this slice
// every time.
conn.bufferedSend = conn.bufferedSend[:0]
}
return nil
}
// Close closes the Conn and its underlying connection. Before closing, it also calls Flush() so that any
// packets currently pending are sent out.
func (conn *Conn) Close() error {
var err error
conn.once.Do(func() {
err = conn.Flush()
close(conn.close)
_ = conn.conn.Close()
})
return err
}
// LocalAddr returns the local address of the underlying connection.
func (conn *Conn) LocalAddr() net.Addr {
return conn.conn.LocalAddr()
}
// RemoteAddr returns the remote address of the underlying connection.
func (conn *Conn) RemoteAddr() net.Addr {
return conn.conn.RemoteAddr()
}
// 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 {
return conn.SetReadDeadline(t)
}
// SetReadDeadline sets the read deadline of the Conn to the time passed. The time must be after time.Now().
// Passing an empty time.Time to the method (time.Time{}) results in the read deadline being cleared.
func (conn *Conn) SetReadDeadline(t time.Time) error {
if t.Before(time.Now()) {
panic(fmt.Errorf("error setting read deadline: time passed is before time.Now()"))
}
empty := time.Time{}
if t == empty {
conn.readDeadline = make(chan time.Time)
} else {
conn.readDeadline = time.After(time.Until(t))
}
return nil
}
// SetWriteDeadline is a stub function to implement net.Conn. It has no functionality.
func (conn *Conn) SetWriteDeadline(time.Time) error {
return nil
}
// Latency returns a rolling average of latency between the sending and the receiving end of the connection.
// The latency returned is updated continuously and is half the round trip time (RTT).
func (conn *Conn) Latency() time.Duration {
if c, ok := conn.conn.(interface {
Latency() time.Duration
}); ok {
return c.Latency()
}
panic(fmt.Sprintf("connection type %T has no Latency() time.Duration method", conn.conn))
}
// ClientCacheEnabled checks if the connection has the client blob cache enabled. If true, the server may send
// blobs to the client to reduce network transmission, but if false, the client does not support it, and the
// server must send chunks as usual.
func (conn *Conn) ClientCacheEnabled() bool {
return conn.cacheEnabled
}
// ChunkRadius returns the initial chunk radius of the connection. For connections obtained through a
// Listener, this is the radius that the client requested. For connections obtained through a Dialer, this
// is the radius that the server approved upon.
func (conn *Conn) ChunkRadius() int {
return int(conn.gameData.ChunkRadius)
}
// takeDeferredPacket locks the deferred packets lock and takes the next packet from the list of deferred
// packets. If none was found, it returns false, and if one was found, the data and true is returned.
func (conn *Conn) takeDeferredPacket() (*packetData, bool) {
conn.deferredPacketMu.Lock()
defer conn.deferredPacketMu.Unlock()
if len(conn.deferredPackets) == 0 {
return nil, false
}
data := conn.deferredPackets[0]
// Explicitly clear out the packet at offset 0. When we slice it to remove the first element, that element
// will not be garbage collectable, because the array it's in is still referenced by the slice. Doing this
// makes sure garbage collecting the packet is possible.
conn.deferredPackets[0] = nil
conn.deferredPackets = conn.deferredPackets[1:]
return data, true
}
// deferPacket defers a packet so that it is obtained in the next ReadPacket call
func (conn *Conn) deferPacket(pk *packetData) {
conn.deferredPacketMu.Lock()
conn.deferredPackets = append(conn.deferredPackets, pk)
conn.deferredPacketMu.Unlock()
}
// receive receives an incoming serialised packet from the underlying connection. If the connection is not yet
// logged in, the packet is immediately handled.
func (conn *Conn) receive(data []byte) error {
pkData, err := parseData(data, conn)
if err != nil {
return err
}
if pkData.h.PacketID == packet.IDDisconnect {
// We always handle disconnect packets and close the connection if one comes in.
if pks, err := pkData.decode(conn); err != nil {
conn.disconnectMessage.Store(pks[0].(*packet.Disconnect).Message)
}
_ = conn.Close()
return nil
}
if conn.loggedIn && !conn.waitingForSpawn.Load() {
select {
case <-conn.close:
case previous := <-conn.packets:
// There was already a packet in this channel, so take it out and defer it so that it is read
// next.
conn.deferPacket(previous)
default:
}
select {
case <-conn.close:
case conn.packets <- pkData:
}
return nil
}
return conn.handle(pkData)
}
// handle tries to handle the incoming packetData.
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.
pks, err := pkData.decode(conn)
if err != nil {
return err
}
return conn.handleMultiple(pks)
}
}
// This is not the packet we expected next in the login sequence. We push it back so that it may
// be handled by the user.
conn.deferPacket(pkData)
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 {
defer func() {
_ = conn.Flush()
}()
switch pk := pk.(type) {
// Internal packets destined for the server.
case *packet.RequestNetworkSettings:
return conn.handleRequestNetworkSettings(pk)
case *packet.Login:
return conn.handleLogin(pk)
case *packet.ClientToServerHandshake:
return conn.handleClientToServerHandshake()
case *packet.ClientCacheStatus:
return conn.handleClientCacheStatus(pk)
case *packet.ResourcePackClientResponse:
return conn.handleResourcePackClientResponse(pk)
case *packet.ResourcePackChunkRequest:
return conn.handleResourcePackChunkRequest(pk)
case *packet.RequestChunkRadius:
return conn.handleRequestChunkRadius(pk)
case *packet.SetLocalPlayerAsInitialised:
return conn.handleSetLocalPlayerAsInitialised(pk)
// Internal packets destined for the client.
case *packet.NetworkSettings:
return conn.handleNetworkSettings(pk)
case *packet.ServerToClientHandshake:
return conn.handleServerToClientHandshake(pk)
case *packet.PlayStatus:
return conn.handlePlayStatus(pk)
case *packet.ResourcePacksInfo:
return conn.handleResourcePacksInfo(pk)
case *packet.ResourcePackDataInfo:
return conn.handleResourcePackDataInfo(pk)
case *packet.ResourcePackChunkData:
return conn.handleResourcePackChunkData(pk)
case *packet.ResourcePackStack:
return conn.handleResourcePackStack(pk)
case *packet.StartGame:
return conn.handleStartGame(pk)
case *packet.ChunkRadiusUpdated:
return conn.handleChunkRadiusUpdated(pk)
}
return nil
}
// handleRequestNetworkSettings handles an incoming RequestNetworkSettings packet. It returns an error if the protocol
// version is not supported, otherwise sending back a NetworkSettings packet.
func (conn *Conn) handleRequestNetworkSettings(pk *packet.RequestNetworkSettings) error {
found := false
for _, pro := range conn.acceptedProto {
if pro.ID() == pk.ClientProtocol {
conn.proto = pro
conn.pool = pro.Packets()
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)
}
conn.expect(packet.IDLogin)
if err := conn.WritePacket(&packet.NetworkSettings{
CompressionThreshold: 512,
CompressionAlgorithm: conn.compression,
}); err != nil {
return fmt.Errorf("error sending network settings: %v", err)
}
_ = conn.Flush()
conn.enc.EnableCompression(conn.compression)
conn.dec.EnableCompression(conn.compression)
return nil
}
// handleNetworkSettings handles an incoming NetworkSettings packet, enabling compression for future packets.
func (conn *Conn) handleNetworkSettings(pk *packet.NetworkSettings) error {
conn.enc.EnableCompression(pk.CompressionAlgorithm)
conn.dec.EnableCompression(pk.CompressionAlgorithm)
conn.readyToLogin = true
return nil
}
// 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 {
// The next expected packet is a response from the client to the handshake.
conn.expect(packet.IDClientToServerHandshake)
var (
err error
authResult login.AuthResult
)
conn.identityData, conn.clientData, authResult, err = login.Parse(pk.ConnectionRequest)
if err != nil {
return fmt.Errorf("parse login request: %w", err)
}
// Make sure the player is logged in with XBOX Live when necessary.
if !authResult.XBOXLiveAuthenticated && conn.authEnabled {
_ = 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())
}
if err := conn.enableEncryption(authResult.PublicKey); err != nil {
return fmt.Errorf("error enabling encryption: %v", err)
}
return nil
}
// handleClientToServerHandshake handles an incoming ClientToServerHandshake packet.
func (conn *Conn) handleClientToServerHandshake() error {
// The next expected packet is a resource pack client response.
conn.expect(packet.IDResourcePackClientResponse, packet.IDClientCacheStatus)
if err := conn.WritePacket(&packet.PlayStatus{Status: packet.PlayStatusLoginSuccess}); err != nil {
return fmt.Errorf("error sending play status login success: %v", err)
}
pk := &packet.ResourcePacksInfo{TexturePackRequired: conn.texturePacksRequired}
for _, pack := range conn.resourcePacks {
// If it has behaviours, add it to the behaviour pack list. If not, we add it to the texture packs
// list.
if pack.HasBehaviours() {
behaviourPack := protocol.BehaviourPackInfo{UUID: pack.UUID(), Version: pack.Version(), Size: uint64(pack.Len())}
if pack.HasScripts() {
// One of the resource packs has scripts, so we set HasScripts in the packet to true.
pk.HasScripts = true
behaviourPack.HasScripts = true
}
pk.BehaviourPacks = append(pk.BehaviourPacks, behaviourPack)
continue
}
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
}
pk.TexturePacks = append(pk.TexturePacks, texturePack)
}
// Finally we send the packet after the play status.
if err := conn.WritePacket(pk); err != nil {
return fmt.Errorf("error sending resource packs info: %v", err)
}
return nil
}
// saltClaims holds the claims for the salt sent by the server in the ServerToClientHandshake packet.
type saltClaims struct {
Salt string `json:"salt"`
}
// handleServerToClientHandshake handles an incoming ServerToClientHandshake packet. It initialises encryption
// on the client side of the connection, using the hash and the public key from the server exposed in the
// packet.
func (conn *Conn) handleServerToClientHandshake(pk *packet.ServerToClientHandshake) error {
tok, err := jwt.ParseSigned(string(pk.JWT))
if err != nil {
return fmt.Errorf("parse server token: %w", err)
}
//lint:ignore S1005 Double assignment is done explicitly to prevent panics.
raw, _ := tok.Headers[0].ExtraHeaders["x5u"]
kStr, _ := raw.(string)
pub := new(ecdsa.PublicKey)
if err := login.ParsePublicKey(kStr, pub); err != nil {
return fmt.Errorf("parse server public key: %w", err)
}
var c saltClaims
if err := tok.Claims(pub, &c); err != nil {
return fmt.Errorf("verify claims: %w", err)
}
c.Salt = strings.TrimRight(c.Salt, "=")
salt, err := base64.RawStdEncoding.DecodeString(c.Salt)
if err != nil {
return fmt.Errorf("error base64 decoding ServerToClientHandshake salt: %v", err)
}
x, _ := pub.Curve.ScalarMult(pub.X, pub.Y, conn.privateKey.D.Bytes())
// Make sure to pad the shared secret up to 96 bytes.
sharedSecret := append(bytes.Repeat([]byte{0}, 48-len(x.Bytes())), x.Bytes()...)
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)
// We write a ClientToServerHandshake packet (which has no payload) as a response.
_ = conn.WritePacket(&packet.ClientToServerHandshake{})
return nil
}
// handleClientCacheStatus handles a ClientCacheStatus packet sent by the client. It specifies if the client
// has support for the client blob cache.
func (conn *Conn) handleClientCacheStatus(pk *packet.ClientCacheStatus) error {
conn.cacheEnabled = pk.Enabled
return nil
}
// handleResourcePacksInfo handles a ResourcePacksInfo packet sent by the server. The client responds by
// sending the packs it needs downloaded.
func (conn *Conn) handleResourcePacksInfo(pk *packet.ResourcePacksInfo) error {
// First create a new resource pack queue with the information in the packet so we can download them
// properly later.
conn.packQueue = &resourcePackQueue{
packAmount: len(pk.TexturePacks) + len(pk.BehaviourPacks),
downloadingPacks: make(map[string]downloadingPack),
awaitingPacks: make(map[string]*downloadingPack),
}
packsToDownload := make([]string, 0, len(pk.TexturePacks)+len(pk.BehaviourPacks))
for _, pack := range pk.TexturePacks {
if _, ok := conn.packQueue.downloadingPacks[pack.UUID]; ok {
conn.log.Printf("duplicate texture pack entry %v in resource pack info\n", pack.UUID)
conn.packQueue.packAmount--
continue
}
if conn.downloadResourcePack != nil && !conn.downloadResourcePack(uuid.MustParse(pack.UUID), pack.Version) {
conn.ignoredResourcePacks = append(conn.ignoredResourcePacks, exemptedResourcePack{
uuid: pack.UUID,
version: pack.Version,
})
conn.packQueue.packAmount--
continue
}
// This UUID_Version is a hack Mojang put in place.
packsToDownload = append(packsToDownload, pack.UUID+"_"+pack.Version)
conn.packQueue.downloadingPacks[pack.UUID] = downloadingPack{
size: pack.Size,
buf: bytes.NewBuffer(make([]byte, 0, pack.Size)),
newFrag: make(chan []byte),
contentKey: pack.ContentKey,
}
}
for _, pack := range pk.BehaviourPacks {
if _, ok := conn.packQueue.downloadingPacks[pack.UUID]; ok {
conn.log.Printf("duplicate behaviour pack entry %v in resource pack info\n", pack.UUID)
conn.packQueue.packAmount--
continue
}
if conn.downloadResourcePack != nil && !conn.downloadResourcePack(uuid.MustParse(pack.UUID), pack.Version) {
conn.ignoredResourcePacks = append(conn.ignoredResourcePacks, exemptedResourcePack{
uuid: pack.UUID,
version: pack.Version,
})
conn.packQueue.packAmount--
continue
}
// This UUID_Version is a hack Mojang put in place.
packsToDownload = append(packsToDownload, pack.UUID+"_"+pack.Version)
conn.packQueue.downloadingPacks[pack.UUID] = downloadingPack{
size: pack.Size,
buf: bytes.NewBuffer(make([]byte, 0, pack.Size)),
newFrag: make(chan []byte),
contentKey: pack.ContentKey,
}
}
if len(packsToDownload) != 0 {
conn.expect(packet.IDResourcePackDataInfo, packet.IDResourcePackChunkData)
_ = conn.WritePacket(&packet.ResourcePackClientResponse{
Response: packet.PackResponseSendPacks,
PacksToDownload: packsToDownload,
})
return nil
}
conn.expect(packet.IDResourcePackStack)
_ = conn.WritePacket(&packet.ResourcePackClientResponse{Response: packet.PackResponseAllPacksDownloaded})
return nil
}
// handleResourcePackStack handles a ResourcePackStack packet sent by the server. The stack defines the order
// that resource packs are applied in.
func (conn *Conn) handleResourcePackStack(pk *packet.ResourcePackStack) error {
// We currently don't apply resource packs in any way, so instead we just check if all resource packs in
// the stacks are also downloaded.
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
// pack and log it.
conn.log.Printf("dropping behaviour pack with UUID %v due to a texture pack with the same UUID\n", pack.UUID)
pk.BehaviourPacks = append(pk.BehaviourPacks[:i], pk.BehaviourPacks[i+1:]...)
}
}
if !conn.hasPack(pack.UUID, pack.Version, false) {
return fmt.Errorf("texture pack {uuid=%v, version=%v} not downloaded", pack.UUID, pack.Version)
}
}
for _, pack := range pk.BehaviourPacks {
if !conn.hasPack(pack.UUID, pack.Version, true) {
return fmt.Errorf("behaviour pack {uuid=%v, version=%v} not downloaded", pack.UUID, pack.Version)
}
}
conn.expect(packet.IDStartGame)
_ = conn.WritePacket(&packet.ResourcePackClientResponse{Response: packet.PackResponseCompleted})
return nil
}
// hasPack checks if the connection has a resource pack downloaded with the UUID and version passed, provided
// the pack either has or does not have behaviours in it.
func (conn *Conn) hasPack(uuid string, version string, hasBehaviours bool) bool {
for _, exempted := range exemptedPacks {
if exempted.uuid == uuid && exempted.version == version {
// The server may send this resource pack on the stack without sending it in the info, as the client
// always has it downloaded.
return true
}
}
conn.packMu.Lock()
defer conn.packMu.Unlock()
for _, ignored := range conn.ignoredResourcePacks {
if ignored.uuid == uuid && ignored.version == version {
return true
}
}
for _, pack := range conn.resourcePacks {
if pack.UUID() == uuid && pack.Version() == version && pack.HasBehaviours() == hasBehaviours {
return true
}
}
return false
}
// packChunkSize is the size of a single chunk of data from a resource pack: 512 kB or 0.5 MB
const packChunkSize = 1024 * 128
// handleResourcePackClientResponse handles an incoming resource pack client response packet. The packet is
// handled differently depending on the response.
func (conn *Conn) handleResourcePackClientResponse(pk *packet.ResourcePackClientResponse) error {
switch pk.Response {
case packet.PackResponseRefused:
// Even though this response is never sent, we handle it appropriately in case it is changed to work
// correctly again.
return conn.Close()
case packet.PackResponseSendPacks:
packs := pk.PacksToDownload
conn.packQueue = &resourcePackQueue{packs: conn.resourcePacks}
if err := conn.packQueue.Request(packs); err != nil {
return fmt.Errorf("error looking up resource packs to download: %v", err)
}
// Proceed with the first resource pack download. We run all downloads in sequence rather than in
// parallel, as it's less prone to packet loss.
if err := conn.nextResourcePackDownload(); err != nil {
return err
}
case packet.PackResponseAllPacksDownloaded:
pk := &packet.ResourcePackStack{BaseGameVersion: protocol.CurrentVersion}
for _, pack := range conn.resourcePacks {
resourcePack := protocol.StackResourcePack{UUID: pack.UUID(), Version: pack.Version()}
// If it has behaviours, add it to the behaviour pack list. If not, we add it to the texture packs
// list.
if pack.HasBehaviours() {
pk.BehaviourPacks = append(pk.BehaviourPacks, resourcePack)
continue
}
pk.TexturePacks = append(pk.TexturePacks, resourcePack)
}
for _, exempted := range exemptedPacks {
pk.TexturePacks = append(pk.TexturePacks, protocol.StackResourcePack{
UUID: exempted.uuid,
Version: exempted.version,
})
}
if err := conn.WritePacket(pk); err != nil {
return fmt.Errorf("error writing resource pack stack packet: %v", err)
}
case packet.PackResponseCompleted:
conn.loggedIn = true
default:
return fmt.Errorf("unknown resource pack client response: %v", pk.Response)
}
return nil
}
// startGame sends a StartGame packet using the game data of the connection.
func (conn *Conn) startGame() {
data := conn.gameData
_ = conn.WritePacket(&packet.StartGame{
Difficulty: data.Difficulty,
EntityUniqueID: data.EntityUniqueID,
EntityRuntimeID: data.EntityRuntimeID,
PlayerGameMode: data.PlayerGameMode,
PlayerPosition: data.PlayerPosition,
Pitch: data.Pitch,
Yaw: data.Yaw,