diff --git a/net/packet/util.go b/net/packet/util.go index d86119b..92cf1b0 100644 --- a/net/packet/util.go +++ b/net/packet/util.go @@ -75,7 +75,7 @@ func (a Ary[T]) ReadFrom(r io.Reader) (n int64, err error) { return n, err } -func Array(ary interface{}) Field { +func Array(ary any) Field { return Ary[VarInt]{Ary: ary} } diff --git a/server/auth/auth.go b/server/auth/auth.go index 84c3e76..a1bd733 100644 --- a/server/auth/auth.go +++ b/server/auth/auth.go @@ -13,6 +13,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "io/ioutil" "net/http" "strings" @@ -215,6 +216,19 @@ type Property struct { Name, Value, Signature string } +func (p Property) WriteTo(w io.Writer) (n int64, err error) { + hasSignature := len(p.Signature) > 0 + return pk.Tuple{ + pk.String(p.Name), + pk.String(p.Value), + pk.Boolean(hasSignature), + pk.Opt{ + Has: hasSignature, + Field: pk.String(p.Signature), + }, + }.WriteTo(w) +} + //Texture includes player's skin and cape type Texture struct { TimeStamp int64 `json:"timestamp"` diff --git a/server/auth/pubkey.go b/server/auth/pubkey.go new file mode 100644 index 0000000..72836d7 --- /dev/null +++ b/server/auth/pubkey.go @@ -0,0 +1,69 @@ +package auth + +import ( + "crypto/rsa" + "crypto/x509" + "errors" + "io" + "time" + + pk "github.com/Tnze/go-mc/net/packet" +) + +type PublicKey struct { + ExpiresAt time.Time + PubKey *rsa.PublicKey + Signature []byte +} + +func (p *PublicKey) WriteTo(w io.Writer) (n int64, err error) { + pubKeyEncoded, err := x509.MarshalPKIXPublicKey(p.PubKey) + if err != nil { + return 0, err + } + return pk.Tuple{ + pk.Long(p.ExpiresAt.UnixMilli()), + pk.ByteArray(pubKeyEncoded), + pk.ByteArray(p.Signature), + }.WriteTo(w) +} + +func (p *PublicKey) ReadFrom(r io.Reader) (n int64, err error) { + var ( + ExpiresAt pk.Long + PubKey pk.ByteArray + Signature pk.ByteArray + ) + n, err = pk.Tuple{ + &ExpiresAt, + &PubKey, + &Signature, + }.ReadFrom(r) + if err != nil { + return n, err + } + p.ExpiresAt = time.UnixMilli(int64(ExpiresAt)) + pubKey, err := x509.ParsePKIXPublicKey(PubKey) + if err != nil { + return n, err + } + if key, ok := pubKey.(*rsa.PublicKey); !ok { + return n, errors.New("expect RSA public key") + } else { + p.PubKey = key + } + + p.Signature = Signature + return n, nil +} + +func (p *PublicKey) Verify() bool { + if p.ExpiresAt.Before(time.Now()) { + return false + } + encoded, err := x509.MarshalPKIXPublicKey(p.PubKey) + if err != nil { + return false + } + return VerifySignature(encoded, p.Signature) +} diff --git a/server/gameplay.go b/server/gameplay.go index 93dde1b..cd89e80 100644 --- a/server/gameplay.go +++ b/server/gameplay.go @@ -1,8 +1,8 @@ package server import ( - "crypto/rsa" _ "embed" + "github.com/Tnze/go-mc/server/auth" "github.com/google/uuid" "github.com/Tnze/go-mc/net" @@ -13,5 +13,5 @@ type GamePlay interface { // // Note: the connection will be closed after this function returned. // You don't need to close the connection, but to keep not returning while the player is playing. - AcceptPlayer(name string, id uuid.UUID, profilePubKey *rsa.PublicKey, protocol int32, conn *net.Conn) + AcceptPlayer(name string, id uuid.UUID, profilePubKey *auth.PublicKey, properties []auth.Property, protocol int32, conn *net.Conn) } diff --git a/server/login.go b/server/login.go index 600c476..8399519 100644 --- a/server/login.go +++ b/server/login.go @@ -1,27 +1,20 @@ package server import ( - "crypto/rsa" - "crypto/x509" - "errors" "fmt" - "github.com/google/uuid" - "io" - "time" - "unsafe" - "github.com/Tnze/go-mc/chat" "github.com/Tnze/go-mc/data/packetid" "github.com/Tnze/go-mc/net" pk "github.com/Tnze/go-mc/net/packet" "github.com/Tnze/go-mc/offline" "github.com/Tnze/go-mc/server/auth" + "github.com/google/uuid" ) // LoginHandler is used to handle player login process, that is, // from clientbound "LoginStart" packet to serverbound "LoginSuccess" packet. type LoginHandler interface { - AcceptLogin(conn *net.Conn, protocol int32) (name string, id uuid.UUID, profilePubKey *rsa.PublicKey, err error) + AcceptLogin(conn *net.Conn, protocol int32) (name string, id uuid.UUID, profilePubKey *auth.PublicKey, properties []auth.Property, err error) } // LoginChecker is the interface to check if a player is allowed to log in the server. @@ -31,6 +24,9 @@ type LoginChecker interface { CheckPlayer(name string, id uuid.UUID, protocol int32) (ok bool, reason chat.Message) } +// Make sure MojangLoginHandler implement LoginHandler +var _ LoginHandler = (*MojangLoginHandler)(nil) + // MojangLoginHandler is a standard LoginHandler that implement both online and offline login progress. // This implementation also support custom LoginChecker. // None of Custom login packet (also called LoginPluginRequest/Response) is support by this implementation. @@ -54,7 +50,7 @@ type MojangLoginHandler struct { } // AcceptLogin implement LoginHandler for MojangLoginHandler -func (d *MojangLoginHandler) AcceptLogin(conn *net.Conn, protocol int32) (name string, id uuid.UUID, profilePubKey *rsa.PublicKey, err error) { +func (d *MojangLoginHandler) AcceptLogin(conn *net.Conn, protocol int32) (name string, id uuid.UUID, profilePubKey *auth.PublicKey, properties []auth.Property, err error) { //login start var p pk.Packet err = conn.ReadPacket(&p) @@ -66,56 +62,41 @@ func (d *MojangLoginHandler) AcceptLogin(conn *net.Conn, protocol int32) (name s return } - var hasSignature pk.Boolean - var timestamp pk.Long - var profilePubKeyBytes pk.ByteArray - var signature pk.ByteArray + var hasPubKey pk.Boolean + var pubKey auth.PublicKey err = p.Scan( (*pk.String)(&name), - &hasSignature, pk.Opt{ - Has: &hasSignature, - Field: pk.Tuple{×tamp, &profilePubKeyBytes, &signature}, + &hasPubKey, pk.Opt{ + Has: &hasPubKey, + Field: &pubKey, }, ) //decode username as pk.String if err != nil { return } - if hasSignature { - if time.UnixMilli(int64(timestamp)).Before(time.Now()) || - !auth.VerifySignature(profilePubKeyBytes, signature) { + if hasPubKey { + if !pubKey.Verify() { err = LoginFailErr{reason: chat.TranslateMsg("multiplayer.disconnect.invalid_public_key_signature")} return } + profilePubKey = &pubKey } else if d.EnforceSecureProfile { err = LoginFailErr{reason: chat.TranslateMsg("multiplayer.disconnect.missing_public_key")} return } - var properties []property //auth if d.OnlineMode { var resp *auth.Resp //Auth, Encrypt - var key any - key, err = x509.ParsePKIXPublicKey(profilePubKeyBytes) - if err != nil { - return - } - if rsaPubKey, ok := key.(*rsa.PublicKey); !ok { - err = errors.New("expect RSA public key") - return - } else { - profilePubKey = rsaPubKey - } - - resp, err = auth.Encrypt(conn, name, profilePubKey) + resp, err = auth.Encrypt(conn, name, profilePubKey.PubKey) if err != nil { return } name = resp.Name id = resp.ID - properties = *(*[]property)(unsafe.Pointer(&resp.Properties)) + properties = resp.Properties } else { // offline-mode UUID id = offline.NameToUUID(name) @@ -156,21 +137,6 @@ type GameProfile struct { Name string } -type property auth.Property - -func (p property) WriteTo(w io.Writer) (n int64, err error) { - hasSignature := len(p.Signature) > 0 - return pk.Tuple{ - pk.String(p.Name), - pk.String(p.Value), - pk.Boolean(hasSignature), - pk.Opt{ - Has: hasSignature, - Field: pk.String(p.Signature), - }, - }.WriteTo(w) -} - type wrongPacketErr struct { expect, get int32 } diff --git a/server/playerlist.go b/server/playerlist.go index ddf4afc..b1711c2 100644 --- a/server/playerlist.go +++ b/server/playerlist.go @@ -30,7 +30,6 @@ func NewPlayerList(maxPlayers int) *PlayerList { } } -// ClientJoin implement Component for PlayerList func (p *PlayerList) ClientJoin(client PlayerListClient, player PlayerSample) { p.playersLock.Lock() defer p.playersLock.Unlock() @@ -49,6 +48,7 @@ func (p *PlayerList) ClientLeft(client PlayerListClient) { delete(p.players, client) } +// CheckPlayer implements LoginChecker for PlayerList func (p *PlayerList) CheckPlayer(string, uuid.UUID, int32) (ok bool, reason chat.Message) { p.playersLock.Lock() defer p.playersLock.Unlock() @@ -87,3 +87,17 @@ func (p *PlayerList) PlayerSamples() (sample []PlayerSample) { } return } + +func (p *PlayerList) Len() int { + p.playersLock.Lock() + defer p.playersLock.Unlock() + return len(p.players) +} + +func (p *PlayerList) Range(f func(PlayerListClient, PlayerSample)) { + p.playersLock.Lock() + defer p.playersLock.Unlock() + for client, player := range p.players { + f(client, player) + } +} diff --git a/server/server.go b/server/server.go index acd81b1..5cd9652 100644 --- a/server/server.go +++ b/server/server.go @@ -70,7 +70,7 @@ func (s *Server) acceptConn(conn *net.Conn) { case 1: // list ping s.acceptListPing(conn) case 2: // login - name, id, profilePubKey, err := s.AcceptLogin(conn, protocol) + name, id, profilePubKey, properties, err := s.AcceptLogin(conn, protocol) if err != nil { var loginErr *LoginFailErr if errors.As(err, &loginErr) { @@ -84,6 +84,6 @@ func (s *Server) acceptConn(conn *net.Conn) { } return } - s.AcceptPlayer(name, id, profilePubKey, protocol, conn) + s.AcceptPlayer(name, id, profilePubKey, properties, protocol, conn) } }