Add server infrastructure with login, status, and kick handlers

This commit is contained in:
adman
2026-02-24 23:28:25 +01:00
parent bd83b9689e
commit 29b1b92113
7 changed files with 460 additions and 0 deletions
+39
View File
@@ -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
}
+179
View File
@@ -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
}
+4
View File
@@ -0,0 +1,4 @@
package server
type SchematicServer interface {
}
+74
View File
@@ -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
}