226 lines
5.5 KiB
Go
226 lines
5.5 KiB
Go
package client
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"errors"
|
|
|
|
"github.com/admin-else/strom/mc/api"
|
|
"github.com/admin-else/strom/mc/crypto"
|
|
"github.com/admin-else/strom/mc/event"
|
|
"github.com/admin-else/strom/mc/proto"
|
|
"github.com/admin-else/strom/mc/proto_base"
|
|
"github.com/admin-else/strom/mc/proto_generated/v1_21_8"
|
|
"github.com/admin-else/strom/mc/proto_generated/v26_2"
|
|
"github.com/admin-else/strom/mc/text"
|
|
)
|
|
|
|
type LoginClient struct {
|
|
*proto.Conn
|
|
Account *api.Account
|
|
ServerHost string
|
|
ServerPort uint16
|
|
|
|
GivenAccount *v1_21_8.LoginToClientPacketSuccess
|
|
}
|
|
|
|
var (
|
|
NoTokenErr = errors.New("the account has no token so we cant join online servers")
|
|
BadPublicKeyTypeErr = errors.New("the public key is not rsa")
|
|
FailedToLoginErr = errors.New("failed to login")
|
|
)
|
|
|
|
type KickedDuringLoginErr struct {
|
|
text.Component
|
|
}
|
|
|
|
func (k KickedDuringLoginErr) Error() string {
|
|
return k.String()
|
|
}
|
|
|
|
func (s *LoginClient) OnDefault(event event.Unhandled) (err error) {
|
|
if _, ok := event.Val.(proto_base.EncodeDecodeAble); !ok {
|
|
return
|
|
}
|
|
s.Log.Warn("unexpected packet during login client", "packet", event.Val)
|
|
return
|
|
}
|
|
|
|
func (s *LoginClient) OnClose(_ event.Close) (err error) {
|
|
return
|
|
}
|
|
|
|
func (s *LoginClient) OnCompress(compress *v1_21_8.LoginToClientPacketCompress) (err error) {
|
|
s.SetCompressionThreshold(compress.Threshold)
|
|
return
|
|
}
|
|
|
|
func (s *LoginClient) OnEncrypt(packet *v1_21_8.LoginToClientPacketEncryptionBegin) (err error) {
|
|
sharedSecret := make([]byte, 16)
|
|
_, _ = rand.Read(sharedSecret) //never fails
|
|
|
|
if packet.ShouldAuthenticate {
|
|
if s.Account.Ygg == "" {
|
|
err = NoTokenErr
|
|
return
|
|
}
|
|
serverId := crypto.AuthDigest([]byte(packet.ServerId), sharedSecret, packet.PublicKey)
|
|
err = s.Account.JoinServer(serverId)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
pubAny, err := x509.ParsePKIXPublicKey(packet.PublicKey)
|
|
if err != nil {
|
|
return
|
|
}
|
|
pub, ok := pubAny.(*rsa.PublicKey)
|
|
if !ok {
|
|
err = BadPublicKeyTypeErr
|
|
return
|
|
}
|
|
verifyTokenEnc, err := rsa.EncryptPKCS1v15(rand.Reader, pub, packet.VerifyToken)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
sharedSecretEnc, err := rsa.EncryptPKCS1v15(rand.Reader, pub, sharedSecret)
|
|
if err != nil {
|
|
return
|
|
}
|
|
err = s.Send(&v1_21_8.LoginToServerPacketEncryptionBegin{SharedSecret: sharedSecretEnc, VerifyToken: verifyTokenEnc})
|
|
if err != nil {
|
|
return
|
|
}
|
|
err = s.SetSecret(sharedSecret)
|
|
return
|
|
|
|
}
|
|
|
|
func (s *LoginClient) OnDisconnect(packet *v1_21_8.LoginToClientPacketDisconnect) (err error) {
|
|
var reason text.Component
|
|
err = json.Unmarshal([]byte(packet.Reason), &reason)
|
|
if err != nil {
|
|
return
|
|
}
|
|
err = KickedDuringLoginErr{reason}
|
|
return
|
|
}
|
|
|
|
func (s *LoginClient) OnSuccess(success *v1_21_8.LoginToClientPacketSuccess) (err error) {
|
|
s.GivenAccount = success
|
|
err = s.Send(&v1_21_8.LoginToServerPacketLoginAcknowledged{})
|
|
if err != nil {
|
|
return
|
|
}
|
|
s.SetState(proto_base.Configuration)
|
|
err = event.HandlerDoneErr{}
|
|
return
|
|
}
|
|
|
|
func (s *LoginClient) OnSuccess26_2(success *v26_2.LoginToClientPacketSuccess) (err error) {
|
|
return s.OnSuccess(&v1_21_8.LoginToClientPacketSuccess{
|
|
Uuid: success.Uuid,
|
|
Username: success.Username,
|
|
Properties: success.Properties,
|
|
})
|
|
}
|
|
|
|
func (s *LoginClient) OnStart() (err error) {
|
|
return
|
|
}
|
|
|
|
// LoginRaw performs the login handshake on an already-established connection without DNS resolution or configuration handling.
|
|
func LoginRaw(c *proto.Conn, account *api.Account) (err error) {
|
|
lc := &LoginClient{
|
|
Conn: c,
|
|
Account: account,
|
|
}
|
|
lc.RegisterCritical(lc.OnDefault)
|
|
lc.RegisterCritical(lc.OnClose)
|
|
lc.RegisterCriticalUntilLatest(lc.OnCompress)
|
|
lc.RegisterCriticalUntilLatest(lc.OnEncrypt)
|
|
lc.RegisterCriticalUntilLatest(lc.OnDisconnect)
|
|
lc.RegisterCriticalUntil("26.1", lc.OnSuccess)
|
|
lc.RegisterCriticalUntilLatest(lc.OnSuccess26_2)
|
|
|
|
p, err := MakeHandshakePacket(lc.Conn, proto_base.Login)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
err = lc.Send(p)
|
|
if err != nil {
|
|
return
|
|
}
|
|
lc.SetState(proto_base.Login)
|
|
err = lc.Send(&v1_21_8.LoginToServerPacketLoginStart{Username: lc.Account.Name, PlayerUUID: lc.Account.Uuid})
|
|
|
|
err = lc.StartConn()
|
|
if err != nil {
|
|
err = errors.Join(FailedToLoginErr, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
// WithoutDns returns a login setting that disables SRV DNS resolution during login.
|
|
func WithoutDns() func(ls *loginSettings) {
|
|
return func(ls *loginSettings) {
|
|
ls.NoDns = true
|
|
}
|
|
}
|
|
|
|
// WithContext returns a login setting that sets the context used for the connection dial and DNS resolution.
|
|
func WithContext(ctx context.Context) func(ls *loginSettings) {
|
|
return func(ls *loginSettings) {
|
|
ls.Ctx = ctx
|
|
}
|
|
}
|
|
|
|
// WithVersion returns a login setting that forces a specific Minecraft version string for the connection.
|
|
func WithVersion(version string) func(ls *loginSettings) {
|
|
return func(ls *loginSettings) {
|
|
ls.Version = version
|
|
}
|
|
}
|
|
|
|
type loginSettings struct {
|
|
NoDns bool
|
|
Ctx context.Context
|
|
Version string
|
|
IgnoreConfig bool
|
|
}
|
|
|
|
// Login resolves the address with SRV DNS, connects, performs the login handshake, and handles the configuration phase, returning an authenticated connection ready for play.
|
|
func Login(connectTo string, account *api.Account, settings ...func(ls *loginSettings)) (c *proto.Conn, err error) {
|
|
ls := &loginSettings{}
|
|
|
|
for _, setting := range settings {
|
|
setting(ls)
|
|
}
|
|
|
|
if !ls.NoDns {
|
|
connectTo, _, _, err = DoDNSChecked(ls.Ctx, connectTo, nil)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
c, err = Connect(ls.Ctx, connectTo, ls.Version)
|
|
if err != nil {
|
|
return
|
|
}
|
|
err = LoginRaw(c, account)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if !ls.IgnoreConfig {
|
|
err = IgnoreConfig(c)
|
|
}
|
|
return
|
|
}
|