-
Notifications
You must be signed in to change notification settings - Fork 96
/
conn.go
1084 lines (996 loc) · 41.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"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"github.com/google/uuid"
"github.com/sandertv/go-raknet"
"github.com/sandertv/gophertunnel/minecraft/protocol"
"github.com/sandertv/gophertunnel/minecraft/protocol/login"
"github.com/sandertv/gophertunnel/minecraft/protocol/login/jwt"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
"github.com/sandertv/gophertunnel/minecraft/resource"
"io"
"log"
"net"
"strings"
"sync"
"sync/atomic"
"time"
)
const defaultChunkRadius = 16
// 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.
type Conn struct {
conn net.Conn
log *log.Logger
pool packet.Pool
encoder *packet.Encoder
decoder *packet.Decoder
identityData login.IdentityData
clientData login.ClientData
gameData GameData
// 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 []byte
pushedBackPacketsLock sync.Mutex
// pushedBackPackets 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.
pushedBackPackets [][]byte
readDeadline <-chan time.Time
sendMutex 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
// 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 bool
waitingForSpawn atomic.Value
// onlyLogin specifies if the connection should only handle the login and stop handling packets after it
// it is completed.
onlyLogin bool
// expectedIDs is a slice of packet identifiers that are next expected to arrive, until the connection is
// logged in.
expectedIDs atomic.Value
// 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
// 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
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)
close chan bool
}
// 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) *Conn {
if key == nil {
// If no key is passed, we generate one in this function and use it instead.
key, _ = ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
}
conn := &Conn{
conn: netConn,
encoder: packet.NewEncoder(netConn),
decoder: packet.NewDecoder(netConn),
pool: packet.NewPool(),
packets: make(chan []byte, 32),
close: make(chan bool, 2),
spawn: make(chan bool),
privateKey: key,
salt: make([]byte, 16),
log: log,
}
conn.waitingForSpawn.Store(false)
conn.expectedIDs.Store([]uint32{packet.IDLogin})
_, _ = rand.Read(conn.salt)
go func() {
ticker := time.NewTicker(time.Second / 20)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if err := conn.Flush(); err != nil {
_ = conn.Close()
}
case <-conn.close:
// Break out of the goroutine and propagate the close signal again.
conn.close <- true
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
}
// 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().
// If StartGame is called for an OnlyLogin connection, the method will panic.
func (conn *Conn) StartGame(data GameData) error {
if conn.onlyLogin {
panic("cannot start game for minecraft.Conn with OnlyLogin")
}
if data.WorldName == "" {
data.WorldName = conn.gameData.WorldName
}
conn.gameData = data
conn.waitingForSpawn.Store(true)
conn.startGame()
timeout := time.After(time.Second * 10)
select {
case <-conn.spawn:
// Conn was spawned successfully.
return nil
case <-timeout:
return fmt.Errorf("start game spawning timeout")
case <-conn.close:
return fmt.Errorf("connection closed")
}
}
// 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.
// If DoSpawn is called for an OnlyLogin connection, the method will panic.
func (conn *Conn) DoSpawn() error {
if conn.onlyLogin {
panic("cannot do spawn for minecraft.Conn with OnlyLogin")
}
conn.waitingForSpawn.Store(true)
timeout := time.After(time.Second * 10)
select {
case <-conn.spawn:
// Conn was spawned successfully.
return nil
case <-timeout:
return fmt.Errorf("start game spawning timeout")
case <-conn.close:
return fmt.Errorf("connection closed")
}
}
// 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 {
header := &packet.Header{PacketID: pk.ID()}
buffer := bytes.NewBuffer(make([]byte, 0, 5))
if err := header.Write(buffer); err != nil {
return fmt.Errorf("error writing packet header: %v", err)
}
// Record the length of the header so we can filter it out for the packet func.
headerLen := buffer.Len()
pk.Marshal(buffer)
if conn.packetFunc != nil {
// The packet func was set, so we call it.
conn.packetFunc(*header, buffer.Bytes()[headerLen:], conn.LocalAddr(), conn.RemoteAddr())
}
_, err := conn.Write(buffer.Bytes())
return err
}
// 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.
// The packet received must not be held until the next packet is read using ReadPacket(). If the same type of
// packet is read, the previous one will be invalidated.
//
// 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) {
read:
if data, ok := conn.takePushedBackPacket(); ok {
pk, err := conn.parsePacket(data)
if err != nil {
conn.log.Println(err)
goto read
}
return pk, nil
}
pk, tryNext, err := conn.readPacket()
if tryNext {
goto read
}
return pk, err
}
// readPacket reads a new 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.
//
// 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, readNext bool, err error) {
select {
case data := <-conn.packets:
pk, err := conn.parsePacket(data)
if err != nil {
conn.log.Println(err)
return nil, true, nil
}
return pk, false, nil
case <-conn.readDeadline:
return nil, false, fmt.Errorf("error reading packet: read timeout")
case <-conn.close:
conn.close <- true
return nil, false, fmt.Errorf("error reading packet: connection closed")
}
}
// 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.sendMutex.Lock()
defer conn.sendMutex.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.takePushedBackPacket(); ok {
if len(b) < len(data) {
return 0, fmt.Errorf("error reading data: A message sent on a Minecraft socket was larger than the buffer used to receive the message into")
}
return copy(b, data), nil
}
select {
case data := <-conn.packets:
if len(b) < len(data) {
return 0, fmt.Errorf("error reading data: A message sent on a Minecraft socket was larger than the buffer used to receive the message into")
}
return copy(b, data), nil
case <-conn.readDeadline:
return 0, fmt.Errorf("error reading packet: read timeout")
case <-conn.close:
conn.close <- true
return 0, fmt.Errorf("error reading packet: connection closed")
}
}
// 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 {
conn.sendMutex.Lock()
defer conn.sendMutex.Unlock()
if len(conn.bufferedSend) > 0 {
if err := conn.encoder.Encode(conn.bufferedSend); err != nil {
return fmt.Errorf("error encoding packet batch: %v", err)
}
// Reset the send slice so that we don't accidentally send the same packets.
conn.bufferedSend = nil
}
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 {
if len(conn.close) != 0 {
// The connection was already closed, no need to do anything.
return nil
}
_ = conn.Flush()
conn.close <- true
return conn.conn.Close()
}
// 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()) {
return fmt.Errorf("error setting read deadline: time passed is before time.Now()")
}
empty := time.Time{}
if t == empty {
// Empty time, so we just set the time to some crazy high value to ensure the read deadline is never
// actually reached.
conn.readDeadline = time.After(time.Hour * 1000000)
} else {
conn.readDeadline = time.After(t.Sub(time.Now()))
}
return nil
}
// SetWriteDeadline is a stub function to implement net.Conn. It has no functionality.
func (conn *Conn) SetWriteDeadline(t time.Time) error {
return nil
}
// SimulatePacketLoss makes the connection simulate packet loss, with a loss chance passed. It will start
// to discard packets randomly depending on the loss chance, both for sending and for receiving packets.
// The function panics if a loss change is higher than 1 or lower than 0.
func (conn *Conn) SimulatePacketLoss(lossChance float64) {
if lossChance > 1 || lossChance < 0 {
panic(fmt.Sprintf("packet loss must be between 0-1, but got %v", lossChance))
}
conn.conn.(*raknet.Conn).SimulatePacketLoss(lossChance)
}
// 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
}
// parsePacket parses a packet from the data passed and returns it, if successful. If the packet could not be
// parsed successfully, nil and an error is returned.
func (conn *Conn) parsePacket(data []byte) (packet.Packet, error) {
buf := bytes.NewBuffer(data)
header := &packet.Header{}
if err := header.Read(buf); err != nil {
// We don't return this as an error as it's not in the hand of the user to control this. Instead,
// we return to reading a new packet.
return nil, fmt.Errorf("error reading packet header: %v", err)
}
if conn.packetFunc != nil {
// The packet func was set, so we call it.
conn.packetFunc(*header, buf.Bytes(), conn.RemoteAddr(), conn.LocalAddr())
}
// Attempt to fetch the packet with the right packet ID from the pool.
pk, ok := conn.pool[header.PacketID]
if !ok {
// We haven't implemented this packet ID, so we return an unknown packet which could be used by
// the reader.
pk = &packet.Unknown{PacketID: header.PacketID}
}
if err := pk.Unmarshal(buf); err != nil {
// We don't return this as an error as it's not in the hand of the user to control this. Instead,
// we return to reading a new packet.
return nil, fmt.Errorf("error decoding packet %T: %v", pk, err)
}
if buf.Len() != 0 {
return nil, fmt.Errorf("%v unread bytes left in packet %T%v: 0x%x (full payload: 0x%x)", buf.Len(), pk, fmt.Sprintf("%+v", pk)[1:], buf.Bytes(), data)
}
return pk, nil
}
// takePushedBackPacketLocked locks the pushed back packets lock and takes the next packet from the list of
// pushed back packets. If none was found, it returns false, and if one was found, the data and true is
// returned.
func (conn *Conn) takePushedBackPacket() ([]byte, bool) {
conn.pushedBackPacketsLock.Lock()
defer conn.pushedBackPacketsLock.Unlock()
if len(conn.pushedBackPackets) == 0 {
return nil, false
}
data := conn.pushedBackPackets[0]
conn.pushedBackPackets = conn.pushedBackPackets[1:]
return data, true
}
// handleIncoming handles an incoming serialised packet from the underlying connection. If the connection is
// not yet logged in, the packet is immediately read and processed.
func (conn *Conn) handleIncoming(data []byte) error {
select {
case conn.packets <- data:
case <-conn.close:
conn.close <- true
return nil
}
if !conn.loggedIn || conn.waitingForSpawn.Load().(bool) {
pk, tryNext, err := conn.readPacket()
if tryNext {
// Some non-critical error occurred that was already logged to the logger. We simply stop handling
// this packet.
return nil
}
if err != nil {
return err
}
found := false
for _, id := range conn.expectedIDs.Load().([]uint32) {
if id == pk.ID() || pk.ID() == packet.IDDisconnect {
// If the packet was expected, we set found to true and handle it. If not, we skip it and
// ignore it eventually.
found = true
break
}
}
if !found {
// 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.pushedBackPackets = append(conn.pushedBackPackets, data)
return nil
}
return conn.handlePacket(pk)
}
return nil
}
// 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 {
switch pk := pk.(type) {
// Internal packets destined for the server.
case *packet.Login:
return conn.handleLogin(pk)
case *packet.ClientToServerHandshake:
return conn.handleClientToServerHandshake(pk)
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.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)
case *packet.Disconnect:
_ = conn.Close()
return errors.New("Disconnected: " + pk.Message)
}
return nil
}
// handleLogin handles an incoming login packet. It verifies an 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)
if pk.ClientProtocol != protocol.CurrentProtocol {
// By default we assume the client is outdated.
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})
_ = conn.Close()
return fmt.Errorf("incompatible protocol: expected protocol = %v, client protocol = %v", protocol.CurrentProtocol, pk.ClientProtocol)
}
publicKey, authenticated, err := login.Verify(pk.ConnectionRequest)
if err != nil {
return fmt.Errorf("error verifying login request: %v", err)
}
if !authenticated {
return fmt.Errorf("connection %v was not authenticated to XBOX Live", conn.RemoteAddr())
}
conn.identityData, conn.clientData, err = login.Decode(pk.ConnectionRequest)
if err != nil {
return fmt.Errorf("error decoding login request: %v", err)
}
// First validate the identity data and the client data to ensure we're working with valid data. Mojang
// might change this data, or some custom client might fiddle with the data, so we can never be too sure.
if err := conn.identityData.Validate(); err != nil {
return fmt.Errorf("invalid identity data: %v", err)
}
if err := conn.clientData.Validate(); err != nil {
return fmt.Errorf("invalid client data: %v", err)
}
if err := conn.enableEncryption(publicKey); err != nil {
return fmt.Errorf("error enabling encryption: %v", err)
}
return nil
}
// handleClientToServerHandshake handles an incoming ClientToServerHandshake packet.
func (conn *Conn) handleClientToServerHandshake(*packet.ClientToServerHandshake) 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)
}
if conn.onlyLogin {
// Only login, so we stop handling packets after that.
conn.loggedIn = true
return nil
}
pk := &packet.ResourcePacksInfo{TexturePackRequired: conn.texturePacksRequired}
for _, pack := range conn.resourcePacks {
resourcePack := protocol.ResourcePackInfo{UUID: pack.UUID(), Version: pack.Version(), Size: int64(pack.Len())}
if pack.HasScripts() {
// One of the resource packs has scripts, so we set HasScripts in the packet to true.
pk.HasScripts = true
resourcePack.HasScripts = true
}
// 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)
}
// 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
}
// 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 {
headerData, err := jwt.HeaderFrom(pk.JWT)
if err != nil {
return fmt.Errorf("error reading ServerToClientHandshake JWT header: %v", err)
}
header := &jwt.Header{}
if err := json.Unmarshal(headerData, header); err != nil {
return fmt.Errorf("error parsing ServerToClientHandshake JWT header JSON: %v", err)
}
if !jwt.AllowedAlg(header.Algorithm) {
return fmt.Errorf("ServerToClientHandshake JWT header had unexpected alg: expected %v, got %v", "ES384", header.Algorithm)
}
// First parse the public pubKey, so that we can use it to verify the entire JWT afterwards. The JWT is self-
// signed by the server.
pubKey := &ecdsa.PublicKey{}
if err := jwt.ParsePublicKey(header.X5U, pubKey); err != nil {
return fmt.Errorf("error parsing ServerToClientHandshake header x5u public pubKey: %v", err)
}
if _, err := jwt.Verify(pk.JWT, pubKey, false); err != nil {
return fmt.Errorf("error verifying ServerToClientHandshake JWT: %v", err)
}
// We already know the JWT is valid as we verified it, so no need to error check.
body, _ := jwt.Payload(pk.JWT)
m := make(map[string]string)
if err := json.Unmarshal(body, &m); err != nil {
return fmt.Errorf("error parsing ServerToClientHandshake JWT payload JSON: %v", err)
}
b64Salt, ok := m["salt"]
if !ok {
return fmt.Errorf("ServerToClientHandshake JWT payload contained no 'salt'")
}
// Some (faulty) JWT implementations use padded base64, whereas it should be raw. We trim this off.
b64Salt = strings.TrimRight(b64Salt, "=")
salt, err := base64.RawStdEncoding.DecodeString(b64Salt)
if err != nil {
return fmt.Errorf("error base64 decoding ServerToClientHandshake salt: %v", err)
}
x, _ := pubKey.Curve.ScalarMult(pubKey.X, pubKey.Y, conn.privateKey.D.Bytes())
sharedSecret := x.Bytes()
keyBytes := sha256.Sum256(append(salt, sharedSecret...))
// Finally we enable encryption for the encoder and decoder using the secret pubKey bytes we produced.
conn.encoder.EnableEncryption(keyBytes)
conn.decoder.EnableEncryption(keyBytes)
// We write a ClientToServerHandshake packet (which has no payload) as a response.
return conn.WritePacket(&packet.ClientToServerHandshake{})
}
// 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 {
// 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)}
}
for _, pack := range pk.BehaviourPacks {
// 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)}
}
if len(packsToDownload) != 0 {
conn.expect(packet.IDResourcePackDataInfo, packet.IDResourcePackChunkData)
return conn.WritePacket(&packet.ResourcePackClientResponse{
Response: packet.PackResponseSendPacks,
PacksToDownload: packsToDownload,
})
}
conn.expect(packet.IDResourcePackStack)
return conn.WritePacket(&packet.ResourcePackClientResponse{Response: packet.PackResponseAllPacksDownloaded})
}
// 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 {
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)
if err := conn.WritePacket(&packet.ClientCacheStatus{Enabled: conn.cacheEnabled}); err != nil {
return fmt.Errorf("error sending client cache status: %v", err)
}
return conn.WritePacket(&packet.ResourcePackClientResponse{Response: packet.PackResponseCompleted})
}
// 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 _, 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 * 512
// 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{TexturePackRequired: conn.texturePacksRequired}
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)
}
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{
EntityUniqueID: data.EntityUniqueID,
EntityRuntimeID: data.EntityRuntimeID,
PlayerGameMode: data.PlayerGameMode,
PlayerPosition: data.PlayerPosition,
Pitch: data.Pitch,
Yaw: data.Yaw,
Dimension: data.Dimension,
WorldSpawn: data.WorldSpawn,
GameRules: data.GameRules,
Time: data.Time,
Blocks: data.Blocks,
Items: data.Items,
AchievementsDisabled: true,
Generator: 1,
EducationFeaturesEnabled: true,
MultiPlayerGame: true,
MultiPlayerCorrelationID: uuid.Must(uuid.NewRandom()).String(),
CommandsEnabled: true,
WorldName: conn.gameData.WorldName,
LANBroadcastEnabled: true,
})
conn.expect(packet.IDRequestChunkRadius, packet.IDSetLocalPlayerAsInitialised)
}
// nextResourcePackDownload moves to the next resource pack to download and sends a resource pack data info
// packet with information about it.
func (conn *Conn) nextResourcePackDownload() error {
pk, ok := conn.packQueue.NextPack()
if !ok {
return fmt.Errorf("no resource packs to download")
}
if err := conn.WritePacket(pk); err != nil {
return fmt.Errorf("error sending resource pack data info packet: %v", err)
}
// Set the next expected packet to ResourcePackChunkRequest packets.
conn.expect(packet.IDResourcePackChunkRequest)
return nil
}
// handleResourcePackDataInfo handles a resource pack data info packet, which initiates the downloading of the
// pack by the client.
func (conn *Conn) handleResourcePackDataInfo(pk *packet.ResourcePackDataInfo) error {
id := pk.UUID
chunkCount := pk.ChunkCount
downloadingPack, ok := conn.packQueue.downloadingPacks[id]
if !ok {
// We either already downloaded the pack or we got sent an invalid UUID, that did not match any pack
// sent in the ResourcePacksInfo packet.
return fmt.Errorf("unknown pack to download with UUID %v", id)
}
if downloadingPack.size != pk.Size {
// Size mismatch: The ResourcePacksInfo packet had a size for the pack that did not match with the
// size sent here.
return fmt.Errorf("pack %v had a different size in the ResourcePacksInfo packet than the ResourcePackDataInfo packet", id)
}
// Remove the resource pack from the downloading packs and add it to the awaiting packets.
delete(conn.packQueue.downloadingPacks, id)
conn.packQueue.awaitingPacks[id] = &downloadingPack
downloadingPack.chunkSize = pk.DataChunkSize
go func() {
for i := int32(0); i < chunkCount; i++ {
_ = conn.WritePacket(&packet.ResourcePackChunkRequest{
UUID: id,
ChunkIndex: i,
})
select {
case frag := <-downloadingPack.newFrag:
// Write the fragment to the full buffer of the downloading resource pack.
_, _ = downloadingPack.buf.Write(frag)
case <-conn.close:
conn.close <- true
return
}
}
if downloadingPack.buf.Len() != int(downloadingPack.size) {
conn.log.Printf("incorrect resource pack size: expected %v, but got %v\n", downloadingPack.size, downloadingPack.buf.Len())
return
}
// First parse the resource pack from the total byte buffer we obtained.
pack, err := resource.FromBytes(downloadingPack.buf.Bytes())
if err != nil {
conn.log.Printf("invalid full resource pack data for UUID %v: %v\n", id, err)
return
}
conn.packQueue.packAmount--
// Finally we add the resource to the resource packs slice.
conn.resourcePacks = append(conn.resourcePacks, pack)
if conn.packQueue.packAmount == 0 {
conn.expect(packet.IDResourcePackStack)
_ = conn.WritePacket(&packet.ResourcePackClientResponse{Response: packet.PackResponseAllPacksDownloaded})
}
}()
return nil
}
// handleResourcePackChunkData handles a resource pack chunk data packet, which holds a fragment of a resource
// pack that is being downloaded.
func (conn *Conn) handleResourcePackChunkData(pk *packet.ResourcePackChunkData) error {
downloadingPack, ok := conn.packQueue.awaitingPacks[pk.UUID]
if !ok {
// We haven't received a ResourcePackDataInfo packet from the server, so we can't use this data to
// download a resource pack.
return fmt.Errorf("resource pack chunk data for resource pack that was not being downloaded")
}
lastData := downloadingPack.buf.Len()+int(downloadingPack.chunkSize) >= int(downloadingPack.size)
if !lastData && int32(len(pk.Data)) != downloadingPack.chunkSize {
// The chunk data didn't have the full size and wasn't the last data to be sent for the resource pack,
// meaning we got too little data.
return fmt.Errorf("resource pack chunk data had a length of %v, but expected %v", len(pk.Data), downloadingPack.chunkSize)
}
if pk.ChunkIndex != downloadingPack.expectedIndex {
return fmt.Errorf("resource pack chunk data had chunk index %v, but expected %v", pk.ChunkIndex, downloadingPack.expectedIndex)
}
downloadingPack.expectedIndex++
downloadingPack.newFrag <- pk.Data
return nil
}
// handleResourcePackChunkRequest handles a resource pack chunk request, which requests a part of the resource
// pack to be downloaded.
func (conn *Conn) handleResourcePackChunkRequest(pk *packet.ResourcePackChunkRequest) error {
current := conn.packQueue.currentPack
if current.UUID() != pk.UUID {
return fmt.Errorf("resource pack chunk request had unexpected UUID: expected %v, but got %v", current.UUID(), pk.UUID)
}
if conn.packQueue.currentOffset != int64(pk.ChunkIndex)*packChunkSize {
return fmt.Errorf("resource pack chunk request had unexpected chunk index: expected %v, but got %v", conn.packQueue.currentOffset/packChunkSize, pk.ChunkIndex)
}
response := &packet.ResourcePackChunkData{
UUID: pk.UUID,
ChunkIndex: pk.ChunkIndex,
DataOffset: conn.packQueue.currentOffset,
Data: make([]byte, packChunkSize),
}
conn.packQueue.currentOffset += packChunkSize
// We read the data directly into the response's data.
if n, err := current.ReadAt(response.Data, response.DataOffset); err != nil {
// If we hit an EOF, we don't need to return an error, as we've simply reached the end of the content
// AKA the last chunk.
if err != io.EOF {
return fmt.Errorf("error reading resource pack chunk: %v", err)
}
response.Data = response.Data[:n]
defer func() {
if !conn.packQueue.AllDownloaded() {
_ = conn.nextResourcePackDownload()
} else {
conn.expect(packet.IDResourcePackClientResponse)
}
}()
}
if err := conn.WritePacket(response); err != nil {
return fmt.Errorf("error writing resource pack chunk data packet: %v", err)
}
return nil
}
// handleStartGame handles an incoming StartGame packet. It is the signal that the player has been added to a
// world, and it obtains most of its dedicated properties.
func (conn *Conn) handleStartGame(pk *packet.StartGame) error {
conn.gameData = GameData{
WorldName: pk.WorldName,
EntityUniqueID: pk.EntityUniqueID,
EntityRuntimeID: pk.EntityRuntimeID,
PlayerGameMode: pk.PlayerGameMode,
PlayerPosition: pk.PlayerPosition,
Pitch: pk.Pitch,
Yaw: pk.Yaw,
Dimension: pk.Dimension,
WorldSpawn: pk.WorldSpawn,
GameRules: pk.GameRules,
Time: pk.Time,
Blocks: pk.Blocks,
Items: pk.Items,
}
conn.loggedIn = true
conn.expect(packet.IDChunkRadiusUpdated, packet.IDPlayStatus)
return conn.WritePacket(&packet.RequestChunkRadius{ChunkRadius: defaultChunkRadius})
}
// handleRequestChunkRadius handles an incoming RequestChunkRadius packet. It sets the initial chunk radius
// of the connection, and spawns the player.
func (conn *Conn) handleRequestChunkRadius(pk *packet.RequestChunkRadius) error {
conn.expect(packet.IDSetLocalPlayerAsInitialised)
_ = conn.WritePacket(&packet.ChunkRadiusUpdated{ChunkRadius: defaultChunkRadius})
return conn.WritePacket(&packet.PlayStatus{Status: packet.PlayStatusPlayerSpawn})
}
// handleChunkRadiusUpdated handles an incoming ChunkRadiusUpdated packet, which updates the initial chunk
// radius of the connection.
func (conn *Conn) handleChunkRadiusUpdated(pk *packet.ChunkRadiusUpdated) error {
conn.expect(packet.IDPlayStatus)
return nil
}
// handleSetLocalPlayerAsInitialised handles an incoming SetLocalPlayerAsInitialised packet. It is the final
// packet in the spawning sequence and it marks the point where a server sided connection is considered
// logged in.
func (conn *Conn) handleSetLocalPlayerAsInitialised(pk *packet.SetLocalPlayerAsInitialised) error {
if pk.EntityRuntimeID != conn.gameData.EntityRuntimeID {
return fmt.Errorf("entity runtime ID mismatch: entity runtime ID in StartGame and SetLocalPlayerAsInitialised packets should be equal")
}
conn.spawn <- true