yes222
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/admin-else/strom/mc/api"
|
"github.com/admin-else/strom/mc/api"
|
||||||
@@ -52,7 +53,7 @@ func Run(args []string) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
err = PleaseProvideArgumentErr
|
slog.Info("account", "name", a.Name, "uuid", a.Uuid)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = json.NewEncoder(os.Stdout).Encode(data)
|
err = json.NewEncoder(os.Stdout).Encode(data)
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package cmd_util
|
||||||
|
|
||||||
|
import "github.com/admin-else/strom/mc/api"
|
||||||
|
|
||||||
|
func Account(s string) (a *api.Account, err error) {
|
||||||
|
if len(s) > 16 {
|
||||||
|
return api.NewAccountFromYGG(s)
|
||||||
|
}
|
||||||
|
a = api.NewOfflineAccount(s)
|
||||||
|
return
|
||||||
|
}
|
||||||
+1
-1
@@ -77,7 +77,7 @@ func Help(args []string) (err error) {
|
|||||||
func mainE() (err error) {
|
func mainE() (err error) {
|
||||||
subcommands["help"] = Help
|
subcommands["help"] = Help
|
||||||
args := os.Args
|
args := os.Args
|
||||||
args = []string{"TEST", "messenger", "-addr", "localhost"}
|
//args = []string{"TEST", "messenger", "-addr", "localhost"}
|
||||||
|
|
||||||
if len(args) < 2 {
|
if len(args) < 2 {
|
||||||
err = ExpectedASubcommandErr
|
err = ExpectedASubcommandErr
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ package messenger
|
|||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
|
|
||||||
"github.com/admin-else/strom/mc/api"
|
"github.com/admin-else/strom/cmd/strom/cmd_util"
|
||||||
|
"github.com/admin-else/strom/mc/bot/chat"
|
||||||
|
"github.com/admin-else/strom/mc/bot/keepalive"
|
||||||
"github.com/admin-else/strom/mc/client"
|
"github.com/admin-else/strom/mc/client"
|
||||||
"github.com/admin-else/strom/mc/event"
|
"github.com/admin-else/strom/mc/event"
|
||||||
"github.com/admin-else/strom/mc/proto"
|
"github.com/admin-else/strom/mc/proto"
|
||||||
@@ -13,11 +15,12 @@ import (
|
|||||||
var (
|
var (
|
||||||
cmd = flag.NewFlagSet("status", flag.ContinueOnError)
|
cmd = flag.NewFlagSet("status", flag.ContinueOnError)
|
||||||
connectToFlag = cmd.String("addr", "", "server address to connect to")
|
connectToFlag = cmd.String("addr", "", "server address to connect to")
|
||||||
nameFlag = cmd.String("name", "strom_messenger", "the name to use")
|
accFlag = cmd.String("acc", "strom_messenger", "the name to use if longer will be treated as ygg token")
|
||||||
)
|
)
|
||||||
|
|
||||||
type Messenger struct {
|
type Messenger struct {
|
||||||
*proto.Conn
|
*proto.Conn
|
||||||
|
chat *chat.Module
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Messenger) OnChat(e *v1_21_11.PlayToClientPacketPlayerChat) (err error) {
|
func (m *Messenger) OnChat(e *v1_21_11.PlayToClientPacketPlayerChat) (err error) {
|
||||||
@@ -36,22 +39,38 @@ func (m *Messenger) OnChatSystem(e *v1_21_11.PlayToClientPacketSystemChat) (err
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Messenger) OnStdin(e event.Stdin) (err error) {
|
func (m *Messenger) OnStdin(e event.Stdin) (err error) {
|
||||||
return m.Send(&v1_21_11.PlayToServerPacketChatMessage{Message: e.Val})
|
return m.chat.SendMessage(e.Val)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Run(args []string) (err error) {
|
func Run(args []string) (err error) {
|
||||||
err = cmd.Parse(args)
|
err = cmd.Parse(args)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
c, err := client.Login(*connectToFlag, api.NewOfflineAccount(*nameFlag))
|
acc, err := cmd_util.Account(*accFlag)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c, err := client.Login(*connectToFlag, acc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer c.Close()
|
defer c.Close()
|
||||||
|
|
||||||
m := Messenger{Conn: c}
|
chatMod, err := chat.Start(c, acc)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m := &Messenger{
|
||||||
|
Conn: c,
|
||||||
|
chat: chatMod,
|
||||||
|
}
|
||||||
|
|
||||||
event.StartListingStdin(m.Loop)
|
event.StartListingStdin(m.Loop)
|
||||||
m.Register(m.OnStdin)
|
m.Register(m.OnStdin)
|
||||||
m.RegisterUntilLatest(m.OnChat, m.OnChatUnsigned, m.OnChatSystem)
|
m.RegisterUntilLatest(m.OnChat, m.OnChatUnsigned, m.OnChatSystem)
|
||||||
|
keepalive.Start(m.Conn)
|
||||||
|
|
||||||
return m.StartConn()
|
return m.StartConn()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,207 @@
|
|||||||
|
package chat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/admin-else/strom/mc/api"
|
||||||
|
"github.com/admin-else/strom/mc/proto"
|
||||||
|
"github.com/admin-else/strom/mc/proto_generated/v1_21_11"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Module struct {
|
||||||
|
*proto.Conn
|
||||||
|
|
||||||
|
Account *api.Account
|
||||||
|
|
||||||
|
// Chat signing state
|
||||||
|
canSign bool
|
||||||
|
privKey *rsa.PrivateKey
|
||||||
|
sessionUUID uuid.UUID
|
||||||
|
msgIndex int32
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start creates a chat module for the connection. If acc is an online account,
|
||||||
|
// it fetches the profile key pair from Mojang and enables signed chat.
|
||||||
|
func Start(c *proto.Conn, acc *api.Account) (*Module, error) {
|
||||||
|
m := &Module{Conn: c, Account: acc}
|
||||||
|
if acc != nil && acc.Ygg != "" {
|
||||||
|
if err := m.initSigning(acc); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) initSigning(acc *api.Account) (err error) {
|
||||||
|
keys, err := acc.FetchKeys()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("fetch profile keys: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
priv, err := parsePrivateKey(keys.KeyPair.PrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse private key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.privKey = priv
|
||||||
|
m.sessionUUID = uuid.New()
|
||||||
|
|
||||||
|
pubDer, err := x509.MarshalPKIXPublicKey(&priv.PublicKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshal public key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sigBytes, err := base64.StdEncoding.DecodeString(keys.PublicKeySignatureV2)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("decode key signature: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = m.Conn.Send(&v1_21_11.PlayToServerPacketChatSessionUpdate{
|
||||||
|
SessionUUID: m.sessionUUID,
|
||||||
|
ExpireTime: keys.ExpiresAt.UnixMilli(),
|
||||||
|
PublicKey: v1_21_11.ByteArray{Val: pubDer},
|
||||||
|
Signature: v1_21_11.ByteArray{Val: sigBytes},
|
||||||
|
}); err != nil {
|
||||||
|
return fmt.Errorf("send chat session update: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.canSign = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendMessage sends a chat message. If signing is enabled, the message is signed;
|
||||||
|
// otherwise it is sent unsigned.
|
||||||
|
func (m *Module) SendMessage(message string) (err error) {
|
||||||
|
if m.canSign {
|
||||||
|
return m.sendSigned(message)
|
||||||
|
}
|
||||||
|
return m.Conn.Send(&v1_21_11.PlayToServerPacketChatMessage{Message: message})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) sendSigned(message string) (err error) {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
saltBytes := make([]byte, 8)
|
||||||
|
if _, err = rand.Read(saltBytes); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
salt := int64(binary.BigEndian.Uint64(saltBytes))
|
||||||
|
|
||||||
|
timestamp := now.UnixMilli()
|
||||||
|
|
||||||
|
payload, err := m.buildSignaturePayload(message, salt, timestamp)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hashed := sha256.Sum256(payload)
|
||||||
|
sigBytes, err := rsa.SignPKCS1v15(rand.Reader, m.privKey, crypto.SHA256, hashed[:])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(sigBytes) != 256 {
|
||||||
|
return fmt.Errorf("unexpected signature length: %d", len(sigBytes))
|
||||||
|
}
|
||||||
|
var signature [256]byte
|
||||||
|
copy(signature[:], sigBytes)
|
||||||
|
|
||||||
|
m.msgIndex++
|
||||||
|
|
||||||
|
return m.Conn.Send(&v1_21_11.PlayToServerPacketChatMessage{
|
||||||
|
Message: message,
|
||||||
|
Timestamp: timestamp,
|
||||||
|
Salt: salt,
|
||||||
|
Signature: &signature,
|
||||||
|
Offset: 0,
|
||||||
|
Acknowledged: [3]byte{},
|
||||||
|
Checksum: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) buildSignaturePayload(content string, salt, timestampMillis int64) ([]byte, error) {
|
||||||
|
buf := make([]byte, 0, 64+len(content))
|
||||||
|
w := func(b []byte) { buf = append(buf, b...) }
|
||||||
|
|
||||||
|
// Header
|
||||||
|
w(int32Bytes(1))
|
||||||
|
|
||||||
|
// SignedMessageLink: sender UUID, session UUID, index
|
||||||
|
w(uuidBytes(m.Account.Uuid))
|
||||||
|
w(uuidBytes(m.sessionUUID))
|
||||||
|
w(int32Bytes(m.msgIndex))
|
||||||
|
|
||||||
|
// SignedMessageBody: salt, timestamp seconds, content
|
||||||
|
w(int64Bytes(uint64(salt)))
|
||||||
|
w(int64Bytes(uint64(timestampMillis / 1000)))
|
||||||
|
contentBytes := []byte(content)
|
||||||
|
w(int32Bytes(int32(len(contentBytes))))
|
||||||
|
w(contentBytes)
|
||||||
|
|
||||||
|
// LastSeenMessages: empty
|
||||||
|
w(int32Bytes(0))
|
||||||
|
|
||||||
|
return buf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePrivateKey(s string) (*rsa.PrivateKey, error) {
|
||||||
|
block, _ := pem.Decode([]byte(s))
|
||||||
|
if block != nil {
|
||||||
|
k, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
k, err = x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if key, ok := k.(*rsa.PrivateKey); ok {
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("not an RSA private key")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some implementations return raw base64 DER instead of PEM
|
||||||
|
raw, err := base64.StdEncoding.DecodeString(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
k, err := x509.ParsePKCS8PrivateKey(raw)
|
||||||
|
if err != nil {
|
||||||
|
k, err = x509.ParsePKCS1PrivateKey(raw)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
key, ok := k.(*rsa.PrivateKey)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("not an RSA private key")
|
||||||
|
}
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func int32Bytes(v int32) []byte {
|
||||||
|
b := make([]byte, 4)
|
||||||
|
binary.BigEndian.PutUint32(b, uint32(v))
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func int64Bytes(v uint64) []byte {
|
||||||
|
b := make([]byte, 8)
|
||||||
|
binary.BigEndian.PutUint64(b, v)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func uuidBytes(u uuid.UUID) []byte {
|
||||||
|
b := make([]byte, 16)
|
||||||
|
copy(b, u[:])
|
||||||
|
return b
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package keepalive
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/admin-else/strom/mc/proto"
|
||||||
|
"github.com/admin-else/strom/mc/proto_generated/v1_21_8"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mod struct {
|
||||||
|
*proto.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mod) onKeepAlive(p *v1_21_8.PlayToClientPacketKeepAlive) (err error) {
|
||||||
|
return m.Send(&v1_21_8.PlayToServerPacketKeepAlive{KeepAliveId: p.KeepAliveId})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Start(c *proto.Conn) {
|
||||||
|
m := &mod{Conn: c}
|
||||||
|
m.RegisterUntilLatest(m.onKeepAlive)
|
||||||
|
}
|
||||||
@@ -6,9 +6,9 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UUIDv3 Variation of google.uuid.NewHash
|
// uuidV3 Variation of google.uuid.NewHash
|
||||||
// We need this because we want a 14-byte namespace and Google forces 16
|
// We need this because we want a 14-byte namespace and Google forces 16
|
||||||
func UUIDv3(space []byte, data []byte) uuid.UUID {
|
func uuidV3(space []byte, data []byte) uuid.UUID {
|
||||||
h := md5.New()
|
h := md5.New()
|
||||||
h.Reset()
|
h.Reset()
|
||||||
h.Write(space) //nolint:errcheck
|
h.Write(space) //nolint:errcheck
|
||||||
@@ -22,5 +22,5 @@ func UUIDv3(space []byte, data []byte) uuid.UUID {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func FromOfflinePlayer(displayName string) uuid.UUID {
|
func FromOfflinePlayer(displayName string) uuid.UUID {
|
||||||
return UUIDv3([]byte("OfflinePlayer:"), []byte(displayName))
|
return uuidV3([]byte("OfflinePlayer:"), []byte(displayName))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user