From d7df6b25ef94ea0f5d06aba8aab457c154fa58af Mon Sep 17 00:00:00 2001 From: adman Date: Fri, 19 Dec 2025 17:26:58 +0100 Subject: [PATCH] yess --- api/account.go | 8 ++ bot/secure_chat.go | 37 ------- bot/server.go | 28 ----- client/client.go | 23 ++++ {bot => client}/modules/config_ignorer.go | 9 +- {bot => client}/modules/login.go | 31 ++++-- {bot => client}/modules/status.go | 9 +- {bot => client}/modules/timer.go | 0 crypto/offline.go | 26 +++++ error_utils/error_utils.go | 8 ++ {bot => event}/event_bus.go | 26 +---- {bot => proto}/connection.go | 91 ++++++---------- {proto_base => proto}/packet_test.go | 2 +- proto_base/{proto_types.go => base_types.go} | 29 +++++ proto_generated/version_switcher.go | 3 +- proto_generator/main.go | 2 +- server/server.go | 67 ++++++++++++ server/server_modules/login_server.go | 107 +++++++++++++++++++ server/server_modules/status.go | 59 ++++++++++ text/text.go | 3 + 20 files changed, 401 insertions(+), 167 deletions(-) delete mode 100644 bot/secure_chat.go delete mode 100644 bot/server.go create mode 100644 client/client.go rename {bot => client}/modules/config_ignorer.go (87%) rename {bot => client}/modules/login.go (82%) rename {bot => client}/modules/status.go (85%) rename {bot => client}/modules/timer.go (100%) create mode 100644 crypto/offline.go create mode 100644 error_utils/error_utils.go rename {bot => event}/event_bus.go (61%) rename {bot => proto}/connection.go (70%) rename {proto_base => proto}/packet_test.go (99%) rename proto_base/{proto_types.go => base_types.go} (86%) create mode 100644 server/server.go create mode 100644 server/server_modules/login_server.go create mode 100644 server/server_modules/status.go create mode 100644 text/text.go diff --git a/api/account.go b/api/account.go index 1c56742..d86bd7a 100644 --- a/api/account.go +++ b/api/account.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "github.com/admin-else/strom/crypto" "github.com/google/uuid" ) @@ -105,3 +106,10 @@ func (a *Account) JoinServer(serverId string) (err error) { } return } + +func NewOfflineAccount(name string) *Account { + return &Account{ + Name: name, + Uuid: crypto.FromOfflinePlayer(name), + } +} diff --git a/bot/secure_chat.go b/bot/secure_chat.go deleted file mode 100644 index e0f5b7b..0000000 --- a/bot/secure_chat.go +++ /dev/null @@ -1,37 +0,0 @@ -package bot - -import ( - "crypto/rsa" - "crypto/x509" - "encoding/pem" - - "github.com/admin-else/strom/api" - "github.com/google/uuid" -) - -type SecureChatSession struct { - Account *api.Account - PlayerKeys api.PlayerKeys - Private *rsa.PrivateKey - SessionId uuid.UUID -} - -func NewSecureChat(a *api.Account) (s *SecureChatSession, err error) { - s = &SecureChatSession{Account: a} - s.PlayerKeys, err = a.FetchKeys() - - if err != nil { - return - } - block, _ := pem.Decode([]byte(s.PlayerKeys.KeyPair.PrivateKey)) - - k, err := x509.ParsePKCS8PrivateKey(block.Bytes) - if err != nil { - return - } - - s.Private = k.(*rsa.PrivateKey) - - s.SessionId, err = uuid.NewRandom() - return -} diff --git a/bot/server.go b/bot/server.go deleted file mode 100644 index 4d2abca..0000000 --- a/bot/server.go +++ /dev/null @@ -1,28 +0,0 @@ -package bot - -import "net" - -func ServeClient(cNet net.Conn, factory func(c *Conn) (h HandlerInst, err error)) (err error) { - c := Servee(cNet) - h, err := factory(c) - if err != nil { - return - } - err = c.Start(h) - return -} - -func StartServer(listenAddr string, factory func(c *Conn) (h HandlerInst, err error)) (err error) { - l, err := net.Listen("tcp", listenAddr) - if err != nil { - return - } - var cNet net.Conn - for { - cNet, err = l.Accept() - if err != nil { - return - } - go ServeClient(cNet, factory) - } -} diff --git a/client/client.go b/client/client.go new file mode 100644 index 0000000..3c70611 --- /dev/null +++ b/client/client.go @@ -0,0 +1,23 @@ +package client + +import ( + "net" + + "github.com/admin-else/strom/proto" + "github.com/admin-else/strom/proto_base" +) + +func Connect(addr string) (ret *proto.Conn, err error) { + ret = &proto.Conn{} + ret.Version = "1.21.8" + ret.State = proto_base.Handshaking + ret.CompressionThreshold = -1 + ret.Actor = proto_base.Client + ret.Conn, err = net.Dial("tcp", addr) + if err != nil { + return + } + ret.R = ret.Conn + ret.W = ret.Conn + return +} diff --git a/bot/modules/config_ignorer.go b/client/modules/config_ignorer.go similarity index 87% rename from bot/modules/config_ignorer.go rename to client/modules/config_ignorer.go index 431504f..18a468a 100644 --- a/bot/modules/config_ignorer.go +++ b/client/modules/config_ignorer.go @@ -4,13 +4,14 @@ import ( "errors" "fmt" - "github.com/admin-else/strom/bot" + "github.com/admin-else/strom/event" + "github.com/admin-else/strom/proto" "github.com/admin-else/strom/proto_base" "github.com/admin-else/strom/proto_generated/v1_21_8" ) type ConfigIgnorer struct { - *bot.Conn + *proto.Conn } func (c *ConfigIgnorer) Default(event any) (err error) { @@ -28,7 +29,7 @@ func (c *ConfigIgnorer) OnFinish(_ v1_21_8.ConfigurationToClientPacketFinishConf return } c.State = proto_base.Play - err = bot.HandlerDone + err = event.HandlerDone return } @@ -42,7 +43,7 @@ func (c *ConfigIgnorer) OnKeepAlive(packet v1_21_8.ConfigurationToClientPacketKe return } -func IgnoreConfig(c *bot.Conn) (err error) { +func IgnoreConfig(c *proto.Conn) (err error) { err = c.Start(&ConfigIgnorer{c}) if err != nil { err = errors.Join(err, errors.New("failed to ignore config")) diff --git a/bot/modules/login.go b/client/modules/login.go similarity index 82% rename from bot/modules/login.go rename to client/modules/login.go index c5b6df2..41f7789 100644 --- a/bot/modules/login.go +++ b/client/modules/login.go @@ -12,8 +12,10 @@ import ( "strconv" "github.com/admin-else/strom/api" - "github.com/admin-else/strom/bot" + "github.com/admin-else/strom/client" "github.com/admin-else/strom/crypto" + "github.com/admin-else/strom/event" + "github.com/admin-else/strom/proto" "github.com/admin-else/strom/proto_base" "github.com/admin-else/strom/data" @@ -21,8 +23,8 @@ import ( ) type LoginClient struct { - *bot.Conn - Account api.Account + *proto.Conn + Account *api.Account GivenAccount v1_21_8.LoginToClientPacketSuccess } @@ -32,11 +34,11 @@ func (s *LoginClient) Default(event any) (err error) { return } -func (s *LoginClient) OnCycle(_ bot.OnLoopCycle) (err error) { +func (s *LoginClient) OnCycle(_ event.OnLoopCycle) (err error) { return } -func (s *LoginClient) OnStart(_ bot.OnStart) (err error) { +func (s *LoginClient) OnStart(_ event.OnStart) (err error) { host, portStr, err := net.SplitHostPort(s.RemoteAddr().String()) if err != nil { return @@ -131,11 +133,26 @@ func (s *LoginClient) OnSuccess(success v1_21_8.LoginToClientPacketSuccess) (err return } s.State = proto_base.Configuration - err = bot.HandlerDone + err = event.HandlerDone return } -func Login(c *bot.Conn, account api.Account) (err error) { +func Login(c *proto.Conn, account *api.Account) (err error) { + err = c.Start(&LoginClient{ + Conn: c, + Account: account, + }) + if err != nil { + err = errors.Join(err, errors.New("failed to login")) + } + return +} + +func ConnectAndLogin(connectTo string, account *api.Account) (c *proto.Conn, err error) { + c, err = client.Connect(connectTo) + if err != nil { + return + } err = c.Start(&LoginClient{ Conn: c, Account: account, diff --git a/bot/modules/status.go b/client/modules/status.go similarity index 85% rename from bot/modules/status.go rename to client/modules/status.go index f0c3dde..90f2426 100644 --- a/bot/modules/status.go +++ b/client/modules/status.go @@ -5,7 +5,8 @@ import ( "strconv" "strings" - "github.com/admin-else/strom/bot" + "github.com/admin-else/strom/event" + "github.com/admin-else/strom/proto" "github.com/admin-else/strom/proto_base" "github.com/admin-else/strom/data" @@ -13,7 +14,7 @@ import ( ) type StatusClient struct { - *bot.Conn + *proto.Conn Status string } @@ -22,7 +23,7 @@ func (s *StatusClient) Default(event any) (err error) { return } -func (s *StatusClient) OnStart(_ bot.OnStart) (err error) { +func (s *StatusClient) OnStart(_ event.OnStart) (err error) { parts := strings.Split(s.RemoteAddr().String(), ":") versionData, err := data.LookUpProtocolVersionByName(s.Version) if err != nil { @@ -48,6 +49,6 @@ func (s *StatusClient) OnStart(_ bot.OnStart) (err error) { func (s *StatusClient) OnStatus(p v1_21_8.StatusToClientPacketServerInfo) (err error) { s.Status = p.Response - err = bot.HandlerDone + err = event.HandlerDone return } diff --git a/bot/modules/timer.go b/client/modules/timer.go similarity index 100% rename from bot/modules/timer.go rename to client/modules/timer.go diff --git a/crypto/offline.go b/crypto/offline.go new file mode 100644 index 0000000..c1a7719 --- /dev/null +++ b/crypto/offline.go @@ -0,0 +1,26 @@ +package crypto + +import ( + "crypto/md5" + + "github.com/google/uuid" +) + +// UUIDv3 Variation of google.uuid.NewHash +// We need this because we want a 14-byte namespace and Google forces 16 +func UUIDv3(space []byte, data []byte) uuid.UUID { + h := md5.New() + h.Reset() + h.Write(space) //nolint:errcheck + h.Write(data) //nolint:errcheck + s := h.Sum(nil) + var u uuid.UUID + copy(u[:], s) + u[6] = (u[6] & 0x0f) | uint8((3&0xf)<<4) // Version 3 + u[8] = (u[8] & 0x3f) | 0x80 // RFC 4122 variant + return u +} + +func FromOfflinePlayer(displayName string) uuid.UUID { + return UUIDv3([]byte("OfflinePlayer:"), []byte(displayName)) +} diff --git a/error_utils/error_utils.go b/error_utils/error_utils.go new file mode 100644 index 0000000..e4359cd --- /dev/null +++ b/error_utils/error_utils.go @@ -0,0 +1,8 @@ +package error_utils + +func Unwrap[T any](v T, err error) T { + if err != nil { + panic(err) + } + return v +} diff --git a/bot/event_bus.go b/event/event_bus.go similarity index 61% rename from bot/event_bus.go rename to event/event_bus.go index 49d7001..e608054 100644 --- a/bot/event_bus.go +++ b/event/event_bus.go @@ -1,4 +1,4 @@ -package bot +package event import ( "errors" @@ -32,7 +32,7 @@ func FindHandlers(inst HandlerInst) (handlers map[reflect.Type]reflect.Value) { return } -func FireEvent(inst HandlerInst, event any, handlers map[reflect.Type]reflect.Value) (err error) { +func Fire(inst HandlerInst, event any, handlers map[reflect.Type]reflect.Value) (err error) { if handler, ok := handlers[reflect.TypeOf(event)]; ok { errV := handler.Call([]reflect.Value{reflect.ValueOf(event)})[0] if !errV.IsNil() { @@ -43,25 +43,3 @@ func FireEvent(inst HandlerInst, event any, handlers map[reflect.Type]reflect.Va } return } - -func (c *Conn) Start(inst HandlerInst) (err error) { - _ = *c // exit early on nil connection - handlers := FindHandlers(inst) - err = FireEvent(inst, OnStart{}, handlers) - for err == nil { - var event any - event, err = c.Receive() - if err != nil { - return - } - err = FireEvent(inst, event, handlers) - if err != nil { - break - } - err = FireEvent(inst, OnLoopCycle{}, handlers) - } - if errors.Is(err, HandlerDone) { - err = nil - } - return -} diff --git a/bot/connection.go b/proto/connection.go similarity index 70% rename from bot/connection.go rename to proto/connection.go index 8a7ab09..d8c0cc7 100644 --- a/bot/connection.go +++ b/proto/connection.go @@ -1,4 +1,4 @@ -package bot +package proto import ( "bytes" @@ -7,56 +7,29 @@ import ( "io" "net" + "github.com/admin-else/strom/event" "github.com/admin-else/strom/proto_base" "github.com/admin-else/strom/proto_generated" ) var BadPacketTypeError = errors.New("bad packet type") -// UnCodablePacket represents a packet that could not be decoded due to querser not supporting all packets. +// UnCodablePacket represents a packet that could not be decoded due to proto_generated not supporting all packets. type UnCodablePacket struct { Err error Partial any Data []byte } -type Actor int - -const ( - Server Actor = iota - Client -) - -func (a Actor) SendDirection() proto_base.Direction { - switch a { - case Server: - return proto_base.ToClient - case Client: - return proto_base.ToServer - default: - panic("invalid actor") - } -} - -func (a Actor) ReceiveDirection() proto_base.Direction { - switch a { - case Server: - return proto_base.ToServer - case Client: - return proto_base.ToClient - default: - panic("invalid actor") - } -} - type Conn struct { net.Conn - R io.Reader - W io.Writer - CompressionThreshold int32 - State proto_base.State - Actor Actor - Version string + R io.Reader + W io.Writer + CompressionThreshold int32 + State proto_base.State + Actor proto_base.Actor + Version string + DontDecodePacketsWithoutHandlers bool } func (c *Conn) Send(packet any) (err error) { @@ -131,6 +104,9 @@ func (c *Conn) Receive() (packet any, err error) { } if packetLen == 0 { packetBytes, err = io.ReadAll(rawPacketBuffer) + if err != nil { + return + } } else { var zReader io.ReadCloser zReader, err = zlib.NewReader(rawPacketBuffer) @@ -155,29 +131,24 @@ func (c *Conn) Receive() (packet any, err error) { return } -func Connect(addr string) (ret *Conn, err error) { - ret = &Conn{} - ret.Version = "1.21.8" - ret.State = proto_base.Handshaking - ret.CompressionThreshold = -1 - ret.Actor = Client - ret.Conn, err = net.Dial("tcp", addr) - if err != nil { - return +func (c *Conn) Start(inst event.HandlerInst) (err error) { + _ = *c // exit early on nil connection + handlers := event.FindHandlers(inst) + err = event.Fire(inst, event.OnStart{}, handlers) + for err == nil { + var packet any + packet, err = c.Receive() + if err != nil { + return + } + err = event.Fire(inst, packet, handlers) + if err != nil { + break + } + err = event.Fire(inst, event.OnLoopCycle{}, handlers) + } + if errors.Is(err, event.HandlerDone) { + err = nil } - ret.R = ret.Conn - ret.W = ret.Conn - return -} - -func Servee(c net.Conn) (ret *Conn) { - ret = &Conn{} - ret.Version = "1.21.8" - ret.State = proto_base.Handshaking - ret.CompressionThreshold = -1 - ret.Actor = Server - ret.Conn = c - ret.R = c - ret.W = c return } diff --git a/proto_base/packet_test.go b/proto/packet_test.go similarity index 99% rename from proto_base/packet_test.go rename to proto/packet_test.go index 5a70e96..6f99e0d 100644 --- a/proto_base/packet_test.go +++ b/proto/packet_test.go @@ -1,4 +1,4 @@ -package proto_base_test +package proto_test import ( "bytes" diff --git a/proto_base/proto_types.go b/proto_base/base_types.go similarity index 86% rename from proto_base/proto_types.go rename to proto_base/base_types.go index 2662189..486454f 100644 --- a/proto_base/proto_types.go +++ b/proto_base/base_types.go @@ -23,6 +23,35 @@ const ( Play ) +type Actor int + +const ( + Server Actor = iota + Client +) + +func (a Actor) SendDirection() Direction { + switch a { + case Server: + return ToClient + case Client: + return ToServer + default: + panic("invalid actor") + } +} + +func (a Actor) ReceiveDirection() Direction { + switch a { + case Server: + return ToServer + case Client: + return ToClient + default: + panic("invalid actor") + } +} + type PacketIdentifier struct { Direction Direction State State diff --git a/proto_generated/version_switcher.go b/proto_generated/version_switcher.go index 5f264ff..a1ebdbc 100644 --- a/proto_generated/version_switcher.go +++ b/proto_generated/version_switcher.go @@ -1,9 +1,10 @@ package proto_generated import ( + "io" + "github.com/admin-else/strom/proto_base" "github.com/admin-else/strom/proto_generated/v1_21_8" - "io" ) func PacketIdentifierToType(v string, d proto_base.Direction, s proto_base.State, i string) (t any) { diff --git a/proto_generator/main.go b/proto_generator/main.go index e66bb71..a679f97 100644 --- a/proto_generator/main.go +++ b/proto_generator/main.go @@ -321,7 +321,7 @@ func generateVersion(v string) (err error) { if err != nil { return } - f, err := os.Create("proto_generated/v" + vUnderscore + "/proto.go") + f, err := os.Create("proto_generated/v" + vUnderscore + "/proto_base.go") if err != nil { return } diff --git a/server/server.go b/server/server.go new file mode 100644 index 0000000..369ce6c --- /dev/null +++ b/server/server.go @@ -0,0 +1,67 @@ +package server + +import ( + "log" + "net" + + "github.com/admin-else/strom/event" + "github.com/admin-else/strom/proto" + "github.com/admin-else/strom/proto_base" +) + +func Servee(c net.Conn) (ret *proto.Conn) { + ret = &proto.Conn{} + ret.Version = "1.21.8" + ret.State = proto_base.Handshaking + ret.CompressionThreshold = -1 + ret.Actor = proto_base.Server + ret.Conn = c + ret.R = c + ret.W = c + return +} + +func ServeClient(cNet net.Conn, factory func(c *proto.Conn) (h event.HandlerInst, err error)) (err error) { + c := Servee(cNet) + h, err := factory(c) + if err != nil { + return + } + err = c.Start(h) + return +} + +func StartServerWithFactory(listenAddr string, factory func(c *proto.Conn) (h event.HandlerInst, err error)) (err error) { + l, err := net.Listen("tcp", listenAddr) + if err != nil { + return + } + var cNet net.Conn + for { + cNet, err = l.Accept() + if err != nil { + return + } + go ServeClient(cNet, factory) + } +} + +func StartServerWithOnConn(listenAddr string, onConn func(c *proto.Conn) (err error)) (err error) { + l, err := net.Listen("tcp", listenAddr) + if err != nil { + return + } + var cNet net.Conn + for { + cNet, err = l.Accept() + if err != nil { + return + } + go func() { + connErr := onConn(Servee(cNet)) + if connErr != nil { + log.Println(connErr) + } + }() + } +} diff --git a/server/server_modules/login_server.go b/server/server_modules/login_server.go new file mode 100644 index 0000000..24e1c58 --- /dev/null +++ b/server/server_modules/login_server.go @@ -0,0 +1,107 @@ +package server_modules + +import ( + "fmt" + + "github.com/admin-else/strom/api" + "github.com/admin-else/strom/event" + "github.com/admin-else/strom/proto" + "github.com/admin-else/strom/proto_base" + "github.com/admin-else/strom/proto_generated/v1_21_8" + "github.com/google/uuid" +) + +var UnexpectedStatusRequest = UnexpectedNextStateError{proto_base.Status} + +type NameAndUUID struct { + Name string + UUID uuid.UUID +} + +type UnexpectedNextStateError struct { + NextState proto_base.State +} + +func (u UnexpectedNextStateError) Error() string { + return fmt.Sprintf("unexpected next state: %v", u.NextState) +} + +type LoginServer struct { + *proto.Conn + //OnlineMode bool TODO: implement this + Requested NameAndUUID + Given *NameAndUUID + CompressionThreshold int32 +} + +func (l *LoginServer) OnHandshake(packet v1_21_8.HandshakingToServerPacketSetProtocol) (err error) { + l.State = proto_base.State(packet.NextState) + if packet.NextState != proto_base.VarInt(proto_base.Login) { + err = UnexpectedNextStateError{proto_base.State(packet.NextState)} + return + } + return +} + +func (l *LoginServer) SetCompressionThreshold(threshold int32) (err error) { + err = l.Send(v1_21_8.LoginToClientPacketCompress{Threshold: proto_base.VarInt(threshold)}) + if err != nil { + return + } + l.CompressionThreshold = threshold + return +} + +func (l *LoginServer) OnLoginStart(packet v1_21_8.LoginToServerPacketLoginStart) (err error) { + l.Requested = NameAndUUID{packet.Username, packet.PlayerUUID} + //err = l.SetCompressionThreshold(l.CompressionThreshold) + //if err != nil { + // return + //} + if l.Given == nil { + l.Given = &l.Requested + } + err = l.Send(v1_21_8.LoginToClientPacketSuccess{ + Uuid: l.Given.UUID, + Username: l.Given.Name, + Properties: nil, + }) + return +} + +func (l *LoginServer) OnLoginAcknowledged(_ v1_21_8.LoginToServerPacketLoginAcknowledged) (err error) { + l.State = proto_base.Configuration + err = event.HandlerDone + return +} + +func (l *LoginServer) Default(event any) (err error) { + err = fmt.Errorf("unexpected event during login: %#v", event) + fmt.Println(err) + return +} + +func (l *LoginServer) OnStart(_ event.OnStart) (err error) { + return +} + +func (l *LoginServer) OnCycle(_ event.OnLoopCycle) (err error) { + return +} + +func ServeLogin(c *proto.Conn) (ret *LoginServer, err error) { + ret = &LoginServer{Conn: c} + ret.CompressionThreshold = 256 + err = ret.Start(ret) + return +} + +func ServeLoginWithOtherAccount(c *proto.Conn, a *api.Account) (ret *LoginServer, err error) { + ret = &LoginServer{Conn: c, Given: &NameAndUUID{ + Name: a.Name, + UUID: a.Uuid, + }} + ret.CompressionThreshold = 256 + err = ret.Start(ret) + return +} diff --git a/server/server_modules/status.go b/server/server_modules/status.go new file mode 100644 index 0000000..986fe09 --- /dev/null +++ b/server/server_modules/status.go @@ -0,0 +1,59 @@ +package server_modules + +import ( + "encoding/json" + + "github.com/admin-else/strom/proto" + "github.com/admin-else/strom/proto_generated/v1_21_8" +) + +// StatusResponse here is the structure I used: https://minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping#Status_Response +type StatusResponse struct { + Version struct { + Name string `json:"name"` + Protocol int `json:"protocol"` + } `json:"version"` + Players struct { + Max int `json:"max"` + Online int `json:"online"` + Sample []struct { + Name string `json:"name"` + ID string `json:"id"` + } `json:"sample"` + } `json:"players"` + Description struct { + Text string `json:"text"` + } `json:"description"` + Favicon string `json:"favicon"` + EnforcesSecureChat bool `json:"enforcesSecureChat"` +} + +type StatusServer struct { + *proto.Conn + Status string +} + +func (p *StatusServer) Default(_ any) (err error) { + return +} + +func (p *StatusServer) OnStatusRequest(_ v1_21_8.StatusToServerPacketPingStart) (err error) { + return p.Send(v1_21_8.StatusToClientPacketServerInfo{Response: p.Status}) +} + +func (p *StatusServer) OnStatusPing(packet v1_21_8.StatusToServerPacketPing) (err error) { + return p.Send(v1_21_8.StatusToClientPacketPing(packet)) +} + +func ServeStatus(c *proto.Conn, s StatusResponse) (err error) { + status, err := json.Marshal(s) + if err != nil { + return + } + server := &StatusServer{ + Conn: c, + Status: string(status), + } + err = server.Start(server) + return +} diff --git a/text/text.go b/text/text.go new file mode 100644 index 0000000..915d733 --- /dev/null +++ b/text/text.go @@ -0,0 +1,3 @@ +package text + +// https://minecraft.wiki/w/Text_component_format