From 29b1b921130951e5585b404a8492fb974cc2d06c Mon Sep 17 00:00:00 2001 From: adman Date: Tue, 24 Feb 2026 23:28:25 +0100 Subject: [PATCH] Add server infrastructure with login, status, and kick handlers --- nbt/file.go | 1 + server/kicker.go | 39 +++++++ server/login_server.go | 179 +++++++++++++++++++++++++++++ server/schematic.go | 4 + server/status.go | 74 ++++++++++++ tools/registry_data_hunter/main.go | 111 ++++++++++++++++++ tools/schematic_server/main.go | 52 +++++++++ 7 files changed, 460 insertions(+) create mode 100644 nbt/file.go create mode 100644 server/kicker.go create mode 100644 server/login_server.go create mode 100644 server/schematic.go create mode 100644 server/status.go create mode 100644 tools/registry_data_hunter/main.go create mode 100644 tools/schematic_server/main.go diff --git a/nbt/file.go b/nbt/file.go new file mode 100644 index 0000000..41ad9a7 --- /dev/null +++ b/nbt/file.go @@ -0,0 +1 @@ +package nbt diff --git a/server/kicker.go b/server/kicker.go new file mode 100644 index 0000000..e66e34a --- /dev/null +++ b/server/kicker.go @@ -0,0 +1,39 @@ +package server + +import ( + "errors" + + "github.com/admin-else/strom/nbt" + "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/admin-else/strom/text" +) + +var ( + NotClientErr = errors.New("not a servee") + CantKickInStateErr = errors.New("cant kick in this state") +) + +func Kick(c *proto.Conn, reason *text.Component) (err error) { + if c.Actor != proto_base.Servee { + err = NotClientErr + return + } + switch c.State() { + case proto_base.Handshaking, proto_base.Status: + err = CantKickInStateErr + case proto_base.Login: + var b []byte + b, err = reason.MarshalJSON() + if err != nil { + return + } + err = c.Send(&v1_21_8.LoginToClientPacketDisconnect{Reason: string(b)}) + case proto_base.Configuration: + err = c.Send(&v1_21_8.ConfigurationToClientPacketDisconnect{Reason: nbt.Anon{Value: reason.ToNBT()}}) + case proto_base.Play: + err = c.Send(&v1_21_8.PlayToClientPacketKickDisconnect{Reason: nbt.Anon{Value: reason.ToNBT()}}) + } + return +} diff --git a/server/login_server.go b/server/login_server.go new file mode 100644 index 0000000..d95f2d5 --- /dev/null +++ b/server/login_server.go @@ -0,0 +1,179 @@ +package server + +import ( + "encoding/json" + "errors" + "fmt" + "log/slog" + "slices" + + "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 StatusServedError = errors.New("status served") + +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 FIXME: implement this + ServerHost string + ServerPort uint16 + Requested NameAndUUID + Given *NameAndUUID + CompressionThreshold int32 //FIXME: implement this + Status []byte + CompatibleVersions []int32 +} + +func (l *LoginServer) OnHandshake(packet *v1_21_8.HandshakingToServerPacketSetProtocol) (err error) { + l.SetState(proto_base.State(packet.NextState)) + l.ServerHost = packet.ServerHost + l.ServerPort = packet.ServerPort + + if packet.NextState == int32(proto_base.Status) && l.Status != nil { + return + } + + if packet.NextState != int32(proto_base.Login) { + err = UnexpectedNextStateError{proto_base.State(packet.NextState)} + return + } + + if len(l.CompatibleVersions) != 0 && !slices.Contains(l.CompatibleVersions, packet.ProtocolVersion) { + err = fmt.Errorf("incompatible protocol version please use one of %v you can look up the real coresponding version at https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Protocol_version_numbers", l.CompatibleVersions) + return + } + + return +} + +func (l *LoginServer) OnStatusRequest(_ *v1_21_8.StatusToServerPacketPingStart) (err error) { + err = l.Send(&v1_21_8.StatusToClientPacketServerInfo{Response: string(l.Status)}) + return +} + +func (l *LoginServer) OnStatusPing(packet *v1_21_8.StatusToServerPacketPing) (err error) { + err = l.Send(&v1_21_8.StatusToClientPacketPing{Time: packet.Time}) + if err != nil { + return + } + return event.HandlerDoneErr{Return: StatusServedError} +} + +func (l *LoginServer) SetCompressionThreshold(threshold int32) (err error) { + err = l.Send(&v1_21_8.LoginToClientPacketCompress{Threshold: 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.SetState(proto_base.Configuration) + err = event.HandlerDoneErr{} + return +} + +func (l *LoginServer) OnDefault(event event.Unhandled) (err error) { + err = fmt.Errorf("unexpected event during login: %#v", event) + return +} + +func (l *LoginServer) OnStart(_ event.OnStart) (err error) { + return +} + +func (l *LoginServer) OnCycle(_ event.Tick) (err error) { + return +} + +type LoginServerSetting func(*LoginServer) + +func WithOtherAccount(a *api.Account) LoginServerSetting { + return func(s *LoginServer) { + if s.Given == nil { + return + } + s.Given = &NameAndUUID{ + Name: a.Name, + UUID: a.Uuid, + } + } +} + +func WithRawStatus(status []byte) LoginServerSetting { + return func(s *LoginServer) { + s.Status = status + } +} + +func WithStatus(response StatusResponse) LoginServerSetting { + var status []byte + status, err := json.Marshal(response) + if err != nil { + panic(err) + } + slog.Debug("With status", "status", status) + return WithRawStatus(status) +} + +func WithCompatibleVersionsRange(start, end int32) LoginServerSetting { + var versions []int32 + for i := start; i <= end; i++ { + versions = append(versions, i) + } + return WithCompatibleVersions(versions...) +} + +func WithCompatibleVersions(versions ...int32) LoginServerSetting { + return func(s *LoginServer) { + s.CompatibleVersions = append(versions) + } +} + +func ServeLogin(c *proto.Conn, settings ...LoginServerSetting) (ret *LoginServer, err error) { + ret = &LoginServer{Conn: c} + for _, s := range settings { + s(ret) + } + ret.CompressionThreshold = 256 + err = ret.Start(ret) + return +} diff --git a/server/schematic.go b/server/schematic.go new file mode 100644 index 0000000..20e49bc --- /dev/null +++ b/server/schematic.go @@ -0,0 +1,4 @@ +package server + +type SchematicServer interface { +} diff --git a/server/status.go b/server/status.go new file mode 100644 index 0000000..3040021 --- /dev/null +++ b/server/status.go @@ -0,0 +1,74 @@ +package server + +import ( + "encoding/json" + + "github.com/admin-else/strom/proto" + "github.com/admin-else/strom/proto_generated/v1_21_8" + "github.com/admin-else/strom/text" +) + +// These are separate types so a status definition looks less ugly + +type StatusResponseVersion struct { + Name *text.Component `json:"name"` + Protocol int32 `json:"protocol"` +} + +type StatusResponseSample struct { + Name *text.Component `json:"name"` + ID string `json:"id"` +} +type StatusResponsePlayers struct { + Max int `json:"max"` + Online int `json:"online"` + Sample []StatusResponseSample `json:"sample"` +} + +// StatusResponse here is the structure I used: https://minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping#Status_Response +type StatusResponse struct { + Version StatusResponseVersion `json:"version"` + Players StatusResponsePlayers `json:"players"` + Description *text.Component `json:"description"` + Favicon string `json:"favicon"` + EnforcesSecureChat bool `json:"enforcesSecureChat"` +} + +// MustMarshal is a utility for marshalling StatusResponse to JSON +func (s StatusResponse) MustMarshal() []byte { + b, err := json.Marshal(s) + if err != nil { + panic(err) + } + return b +} + +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{Time: packet.Time}) +} + +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/tools/registry_data_hunter/main.go b/tools/registry_data_hunter/main.go new file mode 100644 index 0000000..5b2f82b --- /dev/null +++ b/tools/registry_data_hunter/main.go @@ -0,0 +1,111 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "log/slog" + + "github.com/admin-else/strom/api" + "github.com/admin-else/strom/client" + "github.com/admin-else/strom/client/modules" + "github.com/admin-else/strom/data" + "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/admin-else/strom/server" + "github.com/admin-else/strom/text" +) + +func wontFail[T any](v T, err error) T { + if err != nil { + panic(err) + } + return v +} + +var ( + CompatibleProtocolVersion = wontFail(data.LookUpVersionByName("1.21.8")).Version + FinishedConfigErr = errors.New("finished configuration") +) + +var Status = wontFail(json.Marshal(server.StatusResponse{ + Version: server.StatusResponseVersion{ + Name: nil, + Protocol: CompatibleProtocolVersion, + }, + Description: text.Pretty("Registry data hunter"), +})) + +type Proxy struct { + Client, Servee *proto.Conn +} + +func (p *Proxy) OnAnything(e event.Anything) (err error) { + packet, ok := e.Val.(proto_base.EncodeDecodeAble) + if !ok { + return + } + packetInfo, ok := proto.LookupPacketInfoByType(packet) + if !ok { + return + } + slog.Debug("packet", "type", packetInfo.Name, "packet", fmt.Sprintf("%#v", packet)) + switch packetInfo.Direction { + case proto_base.ToServer: + err = p.Client.Send(packet) + case proto_base.ToClient: + err = p.Servee.Send(packet) + } + return +} + +func (p *Proxy) OnFinishConfiguration(_ *v1_21_8.ConfigurationToServerPacketFinishConfiguration) (err error) { + err = FinishedConfigErr + return +} + +func (p *Proxy) OnData(data *v1_21_8.ConfigurationToClientPacketRegistryData) (err error) { + return +} + +func handleClient(c *proto.Conn) (err error) { + acc := api.NewOfflineAccount("RegistryHunter") + + // ignore resource leak warning we own c conn so we should not close it here + _, err = server.ServeLogin(c, server.WithOtherAccount(acc), server.WithCompatibleVersions(CompatibleProtocolVersion), server.WithRawStatus(Status)) + if err != nil { + return + } + sockPuppet, err := client.Connect(":25566") + if err != nil { + return + } + defer sockPuppet.Close() + err = sockPuppet.Start(&modules.LoginClient{ + Conn: sockPuppet, + Account: acc, + }) + if err != nil { + return + } + errChan := make(chan error) + p := &Proxy{Client: sockPuppet, Servee: c} + go func() { + errChan <- p.Client.Start(p) + }() + go func() { + errChan <- p.Servee.Start(p) + }() + err = <-errChan + return +} + +func main() { + slog.SetLogLoggerLevel(slog.LevelDebug) + err := server.StartServerWithOnConn(":25565", handleClient) + if err != nil { + panic(err) + } +} diff --git a/tools/schematic_server/main.go b/tools/schematic_server/main.go new file mode 100644 index 0000000..7e64fcf --- /dev/null +++ b/tools/schematic_server/main.go @@ -0,0 +1,52 @@ +package main + +import ( + "log/slog" + + "github.com/admin-else/strom/event" + "github.com/admin-else/strom/proto" + "github.com/admin-else/strom/server" + "github.com/admin-else/strom/text" +) + +func wontFail[T any](v T, err error) T { + if err != nil { + panic(err) + } + return v +} + +var StatusResponse = server.StatusResponse{ + Version: server.StatusResponseVersion{ + Name: text.Pretty("STROM"), + Protocol: 772, + }, + Players: server.StatusResponsePlayers{}, + Description: text.Pretty("GO TO LIMBO"), + Favicon: "", + EnforcesSecureChat: false, +}.MustMarshal() + +type Server struct { + *proto.Conn +} + +func (s *Server) OnStart(_ event.OnStart) (err error) { + //goland:noinspection GoResourceLeak captures the conn object which should NOT be closed + _, err = server.ServeLogin(s.Conn, server.WithRawStatus(StatusResponse)) + if err != nil { + return + } + return +} + +func main() { + slog.SetLogLoggerLevel(slog.LevelDebug) + err := server.StartServerWithFactory(":25565", func(c *proto.Conn) (h any, err error) { + h = &Server{Conn: c} + return + }) + if err != nil { + panic(err) + } +}