mt-multiserver-proxy/server_conn.go

557 lines
12 KiB
Go

package proxy
import (
"errors"
"log"
"net"
"strings"
"sync"
"time"
"github.com/HimbeerserverDE/srp"
"github.com/anon55555/mt"
"github.com/anon55555/mt/rudp"
)
// A ServerConn is a connection to a minetest server.
type ServerConn struct {
mt.Peer
clt *ClientConn
mu sync.RWMutex
logger *log.Logger
cstate clientState
cstateMu sync.RWMutex
name string
initCh chan struct{}
auth struct {
method mt.AuthMethods
salt, srpA, a, srpK []byte
}
inv mt.Inv
detachedInvs []string
aos map[mt.AOID]struct{}
particleSpawners map[mt.ParticleSpawnerID]struct{}
sounds map[mt.SoundID]struct{}
huds map[mt.HUDID]mt.HUDType
playerList map[string]struct{}
}
func (sc *ServerConn) client() *ClientConn {
sc.mu.RLock()
defer sc.mu.RUnlock()
return sc.clt
}
func (sc *ServerConn) state() clientState {
sc.cstateMu.RLock()
defer sc.cstateMu.RUnlock()
return sc.cstate
}
func (sc *ServerConn) setState(state clientState) {
sc.cstateMu.Lock()
defer sc.cstateMu.Unlock()
sc.cstate = state
}
// Init returns a channel that is closed
// when the ServerConn enters the csActive state.
func (sc *ServerConn) Init() <-chan struct{} { return sc.initCh }
// Log logs an interaction with the ServerConn.
// dir indicates the direction of the interaction.
func (sc *ServerConn) Log(dir string, v ...interface{}) {
sc.logger.Println(append([]interface{}{dir}, v...)...)
}
func handleSrv(sc *ServerConn) {
go func() {
init := make(chan struct{})
defer close(init)
go func(init <-chan struct{}) {
select {
case <-init:
case <-time.After(10 * time.Second):
sc.Log("->", "timeout")
sc.Close()
}
}(init)
for sc.state() == csCreated && sc.client() != nil {
sc.SendCmd(&mt.ToSrvInit{
SerializeVer: latestSerializeVer,
MinProtoVer: latestProtoVer,
MaxProtoVer: latestProtoVer,
PlayerName: sc.client().Name(),
})
time.Sleep(500 * time.Millisecond)
}
}()
for {
pkt, err := sc.Recv()
if err != nil {
if errors.Is(err, net.ErrClosed) {
if errors.Is(sc.WhyClosed(), rudp.ErrTimedOut) {
sc.Log("<->", "timeout")
} else {
sc.Log("<->", "disconnect")
}
if sc.client() != nil {
ack, _ := sc.client().SendCmd(&mt.ToCltDisco{
Reason: mt.Custom,
Custom: "Server connection closed unexpectedly.",
})
select {
case <-sc.client().Closed():
case <-ack:
sc.client().Close()
sc.client().mu.Lock()
sc.client().srv = nil
sc.client().mu.Unlock()
sc.mu.Lock()
sc.clt = nil
sc.mu.Unlock()
}
}
break
}
sc.Log("<-", err)
continue
}
clt := sc.client()
if clt == nil {
sc.Log("<-", "no client")
continue
}
switch cmd := pkt.Cmd.(type) {
case *mt.ToCltHello:
if sc.auth.method != 0 {
sc.Log("<-", "unexpected authentication")
sc.Close()
break
}
sc.setState(sc.state() + 1)
if cmd.AuthMethods&mt.FirstSRP != 0 {
sc.auth.method = mt.FirstSRP
} else {
sc.auth.method = mt.SRP
}
if cmd.SerializeVer != latestSerializeVer {
sc.Log("<-", "invalid serializeVer")
break
}
switch sc.auth.method {
case mt.SRP:
sc.auth.srpA, sc.auth.a, err = srp.InitiateHandshake()
if err != nil {
sc.Log("->", err)
break
}
sc.SendCmd(&mt.ToSrvSRPBytesA{
A: sc.auth.srpA,
NoSHA1: true,
})
case mt.FirstSRP:
salt, verifier, err := srp.NewClient([]byte(clt.name), []byte{})
if err != nil {
sc.Log("->", err)
break
}
sc.SendCmd(&mt.ToSrvFirstSRP{
Salt: salt,
Verifier: verifier,
EmptyPasswd: true,
})
default:
sc.Log("<->", "invalid auth method")
sc.Close()
}
case *mt.ToCltSRPBytesSaltB:
if sc.auth.method != mt.SRP {
sc.Log("<-", "multiple authentication attempts")
break
}
sc.auth.srpK, err = srp.CompleteHandshake(sc.auth.srpA, sc.auth.a, []byte(clt.name), []byte{}, cmd.Salt, cmd.B)
if err != nil {
sc.Log("->", err)
break
}
M := srp.ClientProof([]byte(clt.name), cmd.Salt, sc.auth.srpA, cmd.B, sc.auth.srpK)
if M == nil {
sc.Log("<-", "SRP safety check fail")
break
}
sc.SendCmd(&mt.ToSrvSRPBytesM{
M: M,
})
case *mt.ToCltDisco:
sc.Log("<-", "deny access", cmd)
ack, _ := clt.SendCmd(cmd)
select {
case <-clt.Closed():
case <-ack:
clt.Close()
sc.mu.Lock()
sc.clt = nil
sc.mu.Unlock()
}
case *mt.ToCltAcceptAuth:
sc.auth = struct {
method mt.AuthMethods
salt, srpA, a, srpK []byte
}{}
sc.SendCmd(&mt.ToSrvInit2{Lang: clt.lang})
case *mt.ToCltDenySudoMode:
sc.Log("<-", "deny sudo")
case *mt.ToCltAcceptSudoMode:
sc.Log("<-", "accept sudo")
sc.setState(sc.state() + 1)
case *mt.ToCltAnnounceMedia:
sc.SendCmd(&mt.ToSrvReqMedia{})
sc.SendCmd(&mt.ToSrvCltReady{
Major: clt.major,
Minor: clt.minor,
Patch: clt.patch,
Reserved: clt.reservedVer,
Version: clt.versionStr,
Formspec: clt.formspecVer,
})
sc.Log("<->", "handshake completed")
sc.setState(sc.state() + 1)
close(sc.initCh)
case *mt.ToCltInv:
var oldInv mt.Inv
copy(oldInv, sc.inv)
sc.inv.Deserialize(strings.NewReader(cmd.Inv))
sc.prependInv(sc.inv)
handStack := mt.Stack{
Item: mt.Item{
Name: sc.name + "_hand",
},
Count: 1,
}
hand := sc.inv.List("hand")
if hand == nil {
sc.inv = append(sc.inv, mt.NamedInvList{
Name: "hand",
InvList: mt.InvList{
Width: 0,
Stacks: []mt.Stack{handStack},
},
})
} else if len(hand.Stacks) == 0 {
hand.Width = 0
hand.Stacks = []mt.Stack{handStack}
}
b := &strings.Builder{}
sc.inv.SerializeKeep(b, oldInv)
clt.SendCmd(&mt.ToCltInv{Inv: b.String()})
case *mt.ToCltAOMsgs:
for k := range cmd.Msgs {
sc.swapAOID(&cmd.Msgs[k].ID)
sc.handleAOMsg(cmd.Msgs[k].Msg)
}
clt.SendCmd(cmd)
case *mt.ToCltAORmAdd:
resp := &mt.ToCltAORmAdd{}
for _, ao := range cmd.Remove {
delete(sc.aos, ao)
resp.Remove = append(resp.Remove, ao)
}
for _, ao := range cmd.Add {
if ao.InitData.Name == clt.name {
clt.currentCAO = ao.ID
if clt.playerCAO == 0 {
clt.playerCAO = ao.ID
for _, msg := range ao.InitData.Msgs {
sc.handleAOMsg(msg)
}
resp.Add = append(resp.Add, ao)
} else {
var msgs []mt.IDAOMsg
for _, msg := range ao.InitData.Msgs {
msgs = append(msgs, mt.IDAOMsg{
ID: ao.ID,
Msg: msg,
})
}
clt.SendCmd(&mt.ToCltAOMsgs{Msgs: msgs})
}
} else {
sc.swapAOID(&ao.ID)
for _, msg := range ao.InitData.Msgs {
sc.handleAOMsg(msg)
}
resp.Add = append(resp.Add, ao)
sc.aos[ao.ID] = struct{}{}
}
}
clt.SendCmd(resp)
case *mt.ToCltCSMRestrictionFlags:
if !Conf().DropCSMRF {
cmd.Flags &= ^mt.NoCSMs
clt.SendCmd(cmd)
}
case *mt.ToCltDetachedInv:
var inv mt.Inv
inv.Deserialize(strings.NewReader(cmd.Inv))
sc.prependInv(inv)
b := &strings.Builder{}
inv.Serialize(b)
if cmd.Keep {
sc.detachedInvs = append(sc.detachedInvs, cmd.Name)
} else {
for i, name := range sc.detachedInvs {
if name == cmd.Name {
sc.detachedInvs = append(sc.detachedInvs[:i], sc.detachedInvs[i+1:]...)
break
}
}
}
clt.SendCmd(&mt.ToCltDetachedInv{
Name: cmd.Name,
Keep: cmd.Keep,
Len: cmd.Len,
Inv: b.String(),
})
case *mt.ToCltMediaPush:
var exit bool
for _, f := range clt.media {
if f.name == cmd.Filename {
exit = true
break
}
}
if exit {
break
}
prepend(sc.name, &cmd.Filename)
clt.SendCmd(cmd)
case *mt.ToCltSkyParams:
for i := range cmd.Textures {
prependTexture(sc.name, &cmd.Textures[i])
}
clt.SendCmd(cmd)
case *mt.ToCltSunParams:
prependTexture(sc.name, &cmd.Texture)
prependTexture(sc.name, &cmd.ToneMap)
prependTexture(sc.name, &cmd.Rise)
clt.SendCmd(cmd)
case *mt.ToCltMoonParams:
prependTexture(sc.name, &cmd.Texture)
prependTexture(sc.name, &cmd.ToneMap)
clt.SendCmd(cmd)
case *mt.ToCltSetHotbarParam:
prependTexture(sc.name, &cmd.Img)
clt.SendCmd(cmd)
case *mt.ToCltUpdatePlayerList:
if !clt.playerListInit {
clt.playerListInit = true
} else if cmd.Type == mt.InitPlayers {
cmd.Type = mt.AddPlayers
}
if cmd.Type <= mt.AddPlayers {
for _, player := range cmd.Players {
sc.playerList[player] = struct{}{}
}
} else if cmd.Type == mt.RemovePlayers {
for _, player := range cmd.Players {
delete(sc.playerList, player)
}
}
clt.SendCmd(cmd)
case *mt.ToCltSpawnParticle:
prependTexture(sc.name, &cmd.Texture)
sc.globalParam0(&cmd.NodeParam0)
clt.SendCmd(cmd)
case *mt.ToCltBlkData:
for i := range cmd.Blk.Param0 {
sc.globalParam0(&cmd.Blk.Param0[i])
}
for k := range cmd.Blk.NodeMetas {
for j, field := range cmd.Blk.NodeMetas[k].Fields {
if field.Name == "formspec" {
sc.prependFormspec(&cmd.Blk.NodeMetas[k].Fields[j].Value)
break
}
}
sc.prependInv(cmd.Blk.NodeMetas[k].Inv)
}
clt.SendCmd(cmd)
case *mt.ToCltAddNode:
sc.globalParam0(&cmd.Node.Param0)
clt.SendCmd(cmd)
case *mt.ToCltAddParticleSpawner:
prependTexture(sc.name, &cmd.Texture)
sc.swapAOID(&cmd.AttachedAOID)
sc.globalParam0(&cmd.NodeParam0)
sc.particleSpawners[cmd.ID] = struct{}{}
clt.SendCmd(cmd)
case *mt.ToCltDelParticleSpawner:
delete(sc.particleSpawners, cmd.ID)
clt.SendCmd(cmd)
case *mt.ToCltPlaySound:
prepend(sc.name, &cmd.Name)
sc.swapAOID(&cmd.SrcAOID)
if cmd.Loop {
sc.sounds[cmd.ID] = struct{}{}
}
clt.SendCmd(cmd)
case *mt.ToCltFadeSound:
delete(sc.sounds, cmd.ID)
clt.SendCmd(cmd)
case *mt.ToCltStopSound:
delete(sc.sounds, cmd.ID)
clt.SendCmd(cmd)
case *mt.ToCltAddHUD:
sc.prependHUD(cmd.Type, cmd)
sc.huds[cmd.ID] = cmd.Type
clt.SendCmd(cmd)
case *mt.ToCltChangeHUD:
sc.prependHUD(sc.huds[cmd.ID], cmd)
clt.SendCmd(cmd)
case *mt.ToCltRmHUD:
delete(sc.huds, cmd.ID)
clt.SendCmd(cmd)
case *mt.ToCltShowFormspec:
sc.prependFormspec(&cmd.Formspec)
clt.SendCmd(cmd)
case *mt.ToCltFormspecPrepend:
sc.prependFormspec(&cmd.Prepend)
clt.SendCmd(cmd)
case *mt.ToCltInvFormspec:
sc.prependFormspec(&cmd.Formspec)
clt.SendCmd(cmd)
case *mt.ToCltMinimapModes:
for i := range cmd.Modes {
prependTexture(sc.name, &cmd.Modes[i].Texture)
}
clt.SendCmd(cmd)
case *mt.ToCltNodeMetasChanged:
for k := range cmd.Changed {
for i, field := range cmd.Changed[k].Fields {
if field.Name == "formspec" {
sc.prependFormspec(&cmd.Changed[k].Fields[i].Value)
break
}
}
sc.prependInv(cmd.Changed[k].Inv)
}
clt.SendCmd(cmd)
case *mt.ToCltAddPlayerVel:
clt.SendCmd(cmd)
case *mt.ToCltBreath:
clt.SendCmd(cmd)
case *mt.ToCltChatMsg:
clt.SendCmd(cmd)
case *mt.ToCltCloudParams:
clt.SendCmd(cmd)
case *mt.ToCltDeathScreen:
clt.SendCmd(cmd)
case *mt.ToCltEyeOffset:
clt.SendCmd(cmd)
case *mt.ToCltFOV:
clt.SendCmd(cmd)
case *mt.ToCltHP:
clt.SendCmd(cmd)
case *mt.ToCltHUDFlags:
clt.SendCmd(cmd)
case *mt.ToCltLocalPlayerAnim:
clt.SendCmd(cmd)
case *mt.ToCltModChanMsg:
clt.SendCmd(cmd)
case *mt.ToCltModChanSig:
var exit bool
switch cmd.Signal {
case mt.JoinOK:
if _, ok := clt.modChs[cmd.Channel]; ok {
exit = true
break
}
clt.modChs[cmd.Channel] = struct{}{}
case mt.JoinFail:
fallthrough
case mt.LeaveOK:
delete(clt.modChs, cmd.Channel)
}
if exit {
break
}
clt.SendCmd(cmd)
case *mt.ToCltMovePlayer:
clt.SendCmd(cmd)
case *mt.ToCltMovement:
clt.SendCmd(cmd)
case *mt.ToCltOverrideDayNightRatio:
clt.SendCmd(cmd)
case *mt.ToCltPrivs:
clt.SendCmd(cmd)
case *mt.ToCltRemoveNode:
clt.SendCmd(cmd)
case *mt.ToCltStarParams:
clt.SendCmd(cmd)
case *mt.ToCltTimeOfDay:
clt.SendCmd(cmd)
}
}
}