-
-
Notifications
You must be signed in to change notification settings - Fork 89
/
dial.go
454 lines (412 loc) · 17.4 KB
/
dial.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
package minecraft
import (
"bytes"
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
_ "embed"
"encoding/base64"
"encoding/json"
"fmt"
"github.com/go-jose/go-jose/v3/jwt"
"github.com/google/uuid"
"github.com/sandertv/go-raknet"
"github.com/sandertv/gophertunnel/minecraft/auth"
"github.com/sandertv/gophertunnel/minecraft/protocol"
"github.com/sandertv/gophertunnel/minecraft/protocol/login"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
"golang.org/x/oauth2"
"log"
rand2 "math/rand"
"net"
"os"
"strconv"
"strings"
"time"
)
// Dialer allows specifying specific settings for connection to a Minecraft server.
// The zero value of Dialer is used for the package level Dial function.
type Dialer struct {
// ErrorLog is a log.Logger that errors that occur during packet handling of servers are written to. By
// default, ErrorLog is set to one equal to the global logger.
ErrorLog *log.Logger
// ClientData is the client data used to login to the server with. It includes fields such as the skin,
// locale and UUIDs unique to the client. If empty, a default is sent produced using defaultClientData().
ClientData login.ClientData
// IdentityData is the identity data used to login to the server with. It includes the username, UUID and
// XUID of the player.
// The IdentityData object is obtained using Minecraft auth if Email and Password are set. If not, the
// object provided here is used, or a default one if left empty.
IdentityData login.IdentityData
// TokenSource is the source for Microsoft Live Connect tokens. If set to a non-nil oauth2.TokenSource,
// this field is used to obtain tokens which in turn are used to authenticate to XBOX Live.
// The minecraft/auth package provides an oauth2.TokenSource implementation (auth.tokenSource) to use
// device auth to login.
// If TokenSource is nil, the connection will not use authentication.
TokenSource oauth2.TokenSource
// PacketFunc is called whenever a packet is read from or written to the connection returned when using
// Dialer.Dial(). It includes packets that are otherwise covered in the connection sequence, such as the
// Login packet. The function is called with the header of the packet and its raw payload, the address
// from which the packet originated, and the destination address.
PacketFunc func(header packet.Header, payload []byte, src, dst net.Addr)
// DownloadResourcePack is called individually for every texture and behaviour pack sent by the connection when
// using Dialer.Dial(), and can be used to stop the pack from being downloaded. The function is called with the UUID
// and version of the resource pack, the number of the current pack being downloaded, and the total amount of packs.
// The boolean returned determines if the pack will be downloaded or not.
DownloadResourcePack func(id uuid.UUID, version string, current, total int) bool
// DisconnectOnUnknownPackets specifies if the connection should disconnect if packets received are not present
// in the packet pool. If true, such packets lead to the connection being closed immediately.
// If set to false, the packets will be returned as a packet.Unknown.
DisconnectOnUnknownPackets bool
// DisconnectOnInvalidPackets specifies if invalid packets (either too few bytes or too many bytes) should be
// allowed. If true, such packets lead to the connection being closed immediately. If false,
// packets with too many bytes will be returned while packets with too few bytes will be skipped.
DisconnectOnInvalidPackets bool
// 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
// FlushRate is the rate at which packets sent are flushed. Packets are buffered for a duration up to
// FlushRate and are compressed/encrypted together to improve compression ratios. The lower this
// time.Duration, the lower the latency but the less efficient both network and cpu wise.
// The default FlushRate (when set to 0) is time.Second/20. If FlushRate is set negative, packets
// will not be flushed automatically. In this case, calling `(*Conn).Flush()` is required after any
// calls to `(*Conn).Write()` or `(*Conn).WritePacket()` to send the packets over network.
FlushRate time.Duration
// 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.
EnableClientCache bool
// KeepXBLIdentityData, if set to true, enables passing XUID and title ID to the target server
// if the authentication token is not set. This is technically not valid and some servers might kick
// the client when an XUID is present without logging in.
// For getting this to work with BDS, authentication should be disabled.
KeepXBLIdentityData bool
}
// Dial dials a Minecraft connection to the address passed over the network passed. The network is typically
// "raknet". A Conn is returned which may be used to receive packets from and send packets to.
//
// A zero value of a Dialer struct is used to initiate the connection. A custom Dialer may be used to specify
// additional behaviour.
func Dial(network, address string) (*Conn, error) {
var d Dialer
return d.Dial(network, address)
}
// DialTimeout dials a Minecraft connection to the address passed over the network passed. The network is
// typically "raknet". A Conn is returned which may be used to receive packets from and send packets to.
// If a connection is not established before the timeout ends, DialTimeout returns an error.
// DialTimeout uses a zero value of Dialer to initiate the connection.
func DialTimeout(network, address string, timeout time.Duration) (*Conn, error) {
var d Dialer
return d.DialTimeout(network, address, timeout)
}
// DialContext dials a Minecraft connection to the address passed over the network passed. The network is
// typically "raknet". A Conn is returned which may be used to receive packets from and send packets to.
// If a connection is not established before the context passed is cancelled, DialContext returns an error.
// DialContext uses a zero value of Dialer to initiate the connection.
func DialContext(ctx context.Context, network, address string) (*Conn, error) {
var d Dialer
return d.DialContext(ctx, network, address)
}
// Dial dials a Minecraft connection to the address passed over the network passed. The network is typically
// "raknet". A Conn is returned which may be used to receive packets from and send packets to.
func (d Dialer) Dial(network, address string) (*Conn, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel()
return d.DialContext(ctx, network, address)
}
// DialTimeout dials a Minecraft connection to the address passed over the network passed. The network is
// typically "raknet". A Conn is returned which may be used to receive packets from and send packets to.
// If a connection is not established before the timeout ends, DialTimeout returns an error.
func (d Dialer) DialTimeout(network, address string, timeout time.Duration) (*Conn, error) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
return d.DialContext(ctx, network, address)
}
// DialContext dials a Minecraft connection to the address passed over the network passed. The network is
// typically "raknet". A Conn is returned which may be used to receive packets from and send packets to.
// If a connection is not established before the context passed is cancelled, DialContext returns an error.
func (d Dialer) DialContext(ctx context.Context, network, address string) (conn *Conn, err error) {
key, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
var chainData string
if d.TokenSource != nil {
chainData, err = authChain(ctx, d.TokenSource, key)
if err != nil {
return nil, &net.OpError{Op: "dial", Net: "minecraft", Err: err}
}
d.IdentityData = readChainIdentityData([]byte(chainData))
}
if d.ErrorLog == nil {
d.ErrorLog = log.New(os.Stderr, "", log.LstdFlags)
}
if d.Protocol == nil {
d.Protocol = DefaultProtocol
}
if d.FlushRate == 0 {
d.FlushRate = time.Second / 20
}
n, ok := networkByID(network)
if !ok {
return nil, fmt.Errorf("listen: no network under id: %v", network)
}
var pong []byte
var netConn net.Conn
if pong, err = n.PingContext(ctx, address); err == nil {
netConn, err = n.DialContext(ctx, addressWithPongPort(pong, address))
} else {
netConn, err = n.DialContext(ctx, address)
}
if err != nil {
return nil, err
}
conn = newConn(netConn, key, d.ErrorLog, d.Protocol, d.FlushRate, false)
conn.pool = conn.proto.Packets(false)
conn.identityData = d.IdentityData
conn.clientData = d.ClientData
conn.packetFunc = d.PacketFunc
conn.downloadResourcePack = d.DownloadResourcePack
conn.cacheEnabled = d.EnableClientCache
conn.disconnectOnInvalidPacket = d.DisconnectOnInvalidPackets
conn.disconnectOnUnknownPacket = d.DisconnectOnUnknownPackets
defaultIdentityData(&conn.identityData)
defaultClientData(address, conn.identityData.DisplayName, &conn.clientData)
var request []byte
if d.TokenSource == nil {
// We haven't logged into the user's XBL account. We create a login request with only one token
// holding the identity data set in the Dialer after making sure we clear data from the identity data
// that is only present when logged in.
if !d.KeepXBLIdentityData {
clearXBLIdentityData(&conn.identityData)
}
request = login.EncodeOffline(conn.identityData, conn.clientData, key)
} else {
// We login as an Android device and this will show up in the 'titleId' field in the JWT chain, which
// we can't edit. We just enforce Android data for logging in.
setAndroidData(&conn.clientData)
request = login.Encode(chainData, conn.clientData, key)
identityData, _, _, _ := login.Parse(request)
// If we got the identity data from Minecraft auth, we need to make sure we set it in the Conn too, as
// we are not aware of the identity data ourselves yet.
conn.identityData = identityData
}
l, c := make(chan struct{}), make(chan struct{})
go listenConn(conn, d.ErrorLog, l, c)
conn.expect(packet.IDNetworkSettings, packet.IDPlayStatus)
if err := conn.WritePacket(&packet.RequestNetworkSettings{ClientProtocol: d.Protocol.ID()}); err != nil {
return nil, err
}
_ = conn.Flush()
select {
case <-conn.close:
return nil, conn.closeErr("dial")
case <-ctx.Done():
return nil, conn.wrap(ctx.Err(), "dial")
case <-l:
// We've received our network settings, so we can now send our login request.
conn.expect(packet.IDServerToClientHandshake, packet.IDPlayStatus)
if err := conn.WritePacket(&packet.Login{ConnectionRequest: request, ClientProtocol: d.Protocol.ID()}); err != nil {
return nil, err
}
_ = conn.Flush()
select {
case <-conn.close:
return nil, conn.closeErr("dial")
case <-ctx.Done():
return nil, conn.wrap(ctx.Err(), "dial")
case <-c:
// We've connected successfully. We return the connection and no error.
return conn, nil
}
}
}
// readChainIdentityData reads a login.IdentityData from the Mojang chain
// obtained through authentication.
func readChainIdentityData(chainData []byte) login.IdentityData {
chain := struct{ Chain []string }{}
if err := json.Unmarshal(chainData, &chain); err != nil {
panic("invalid chain data from authentication: " + err.Error())
}
data := chain.Chain[1]
claims := struct {
ExtraData login.IdentityData `json:"extraData"`
}{}
tok, err := jwt.ParseSigned(data)
if err != nil {
panic("invalid chain data from authentication: " + err.Error())
}
if err := tok.UnsafeClaimsWithoutVerification(&claims); err != nil {
panic("invalid chain data from authentication: " + err.Error())
}
if claims.ExtraData.Identity == "" {
panic("chain data contained no data")
}
return claims.ExtraData
}
// listenConn listens on the connection until it is closed on another goroutine. The channel passed will
// receive a value once the connection is logged in.
func listenConn(conn *Conn, logger *log.Logger, l, c chan struct{}) {
defer func() {
_ = conn.Close()
}()
for {
// We finally arrived at the packet decoding loop. We constantly decode packets that arrive
// and push them to the Conn so that they may be processed.
packets, err := conn.dec.Decode()
if err != nil {
if !raknet.ErrConnectionClosed(err) {
logger.Printf("error reading from dialer connection: %v\n", err)
}
return
}
for _, data := range packets {
loggedInBefore, readyToLoginBefore := conn.loggedIn, conn.readyToLogin
if err := conn.receive(data); err != nil {
logger.Printf("error: %v", err)
return
}
if !readyToLoginBefore && conn.readyToLogin {
// This is the signal that the connection is ready to login, so we put a value in the channel so that
// it may be detected.
l <- struct{}{}
}
if !loggedInBefore && conn.loggedIn {
// This is the signal that the connection was considered logged in, so we put a value in the channel so
// that it may be detected.
c <- struct{}{}
}
}
}
}
// authChain requests the Minecraft auth JWT chain using the credentials passed. If successful, an encoded
// chain ready to be put in a login request is returned.
func authChain(ctx context.Context, src oauth2.TokenSource, key *ecdsa.PrivateKey) (string, error) {
// Obtain the Live token, and using that the XSTS token.
liveToken, err := src.Token()
if err != nil {
return "", fmt.Errorf("error obtaining Live Connect token: %v", err)
}
xsts, err := auth.RequestXBLToken(ctx, liveToken, "https://multiplayer.minecraft.net/")
if err != nil {
return "", fmt.Errorf("error obtaining XBOX Live token: %v", err)
}
// Obtain the raw chain data using the
chain, err := auth.RequestMinecraftChain(ctx, xsts, key)
if err != nil {
return "", fmt.Errorf("error obtaining Minecraft auth chain: %v", err)
}
return chain, nil
}
//go:embed skin_resource_patch.json
var skinResourcePatch []byte
//go:embed skin_geometry.json
var skinGeometry []byte
// defaultClientData edits the ClientData passed to have defaults set to all fields that were left unchanged.
func defaultClientData(address, username string, d *login.ClientData) {
rand2.Seed(time.Now().Unix())
d.ServerAddress = address
d.ThirdPartyName = username
if d.DeviceOS == 0 {
d.DeviceOS = protocol.DeviceAndroid
}
if d.GameVersion == "" {
d.GameVersion = protocol.CurrentVersion
}
if d.ClientRandomID == 0 {
d.ClientRandomID = rand2.Int63()
}
if d.DeviceID == "" {
d.DeviceID = uuid.New().String()
}
if d.LanguageCode == "" {
d.LanguageCode = "en_GB"
}
if d.AnimatedImageData == nil {
d.AnimatedImageData = make([]login.SkinAnimation, 0)
}
if d.PersonaPieces == nil {
d.PersonaPieces = make([]login.PersonaPiece, 0)
}
if d.PieceTintColours == nil {
d.PieceTintColours = make([]login.PersonaPieceTintColour, 0)
}
if d.SelfSignedID == "" {
d.SelfSignedID = uuid.New().String()
}
if d.SkinID == "" {
d.SkinID = uuid.New().String()
}
if d.SkinData == "" {
d.SkinData = base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{0, 0, 0, 255}, 32*64))
d.SkinImageHeight = 32
d.SkinImageWidth = 64
}
if d.SkinResourcePatch == "" {
d.SkinResourcePatch = base64.StdEncoding.EncodeToString(skinResourcePatch)
}
if d.SkinGeometry == "" {
d.SkinGeometry = base64.StdEncoding.EncodeToString(skinGeometry)
}
}
// setAndroidData ensures the login.ClientData passed matches settings you would see on an Android device.
func setAndroidData(data *login.ClientData) {
data.DeviceOS = protocol.DeviceAndroid
data.GameVersion = protocol.CurrentVersion
}
// clearXBLIdentityData clears data from the login.IdentityData that is only set when a player is logged into
// XBOX Live.
func clearXBLIdentityData(data *login.IdentityData) {
data.XUID = ""
data.TitleID = ""
}
// defaultIdentityData edits the IdentityData passed to have defaults set to all fields that were left
// unchanged.
func defaultIdentityData(data *login.IdentityData) {
if data.Identity == "" {
data.Identity = uuid.New().String()
}
if data.DisplayName == "" {
data.DisplayName = "Steve"
}
}
// splitPong splits the pong data passed by ;, taking into account escaping these.
func splitPong(s string) []string {
var runes []rune
var tokens []string
inEscape := false
for _, r := range s {
switch {
case r == '\\':
inEscape = true
case r == ';':
tokens = append(tokens, string(runes))
runes = runes[:0]
case inEscape:
inEscape = false
fallthrough
default:
runes = append(runes, r)
}
}
return append(tokens, string(runes))
}
// addressWithPongPort parses the redirect IPv4 port from the pong and returns the address passed with the port
// found if present, or the original address if not.
func addressWithPongPort(pong []byte, address string) string {
frag := splitPong(string(pong))
if len(frag) > 10 {
portStr := frag[10]
port, err := strconv.Atoi(portStr)
// Vanilla (realms, in particular) will sometimes send port 19132 when you ping a port that isn't 19132 already,
// but we should ignore that.
if err != nil || port == 19132 {
return address
}
// Remove the port from the address.
addressParts := strings.Split(address, ":")
address = strings.Join(strings.Split(address, ":")[:len(addressParts)-1], ":")
return address + ":" + portStr
}
return address
}