Add server infrastructure with login, status, and kick handlers
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
package nbt
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
type SchematicServer interface {
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user