mt-multiserver-proxy/process.go

873 lines
18 KiB
Go

package proxy
import (
"crypto/subtle"
"fmt"
"net"
"strings"
"time"
"github.com/HimbeerserverDE/srp"
"github.com/anon55555/mt"
)
func (cc *ClientConn) process(pkt mt.Pkt) {
srv := cc.server()
forward := func(pkt mt.Pkt) {
if srv == nil {
cc.Log("->", "no server")
return
}
srv.Send(pkt)
}
switch cmd := pkt.Cmd.(type) {
case *mt.ToSrvNil:
return
case *mt.ToSrvInit:
if cc.state() > csCreated {
cc.Log("->", "duplicate init")
return
}
cc.setState(csInit)
if cmd.SerializeVer < serializeVer {
cc.Log("<-", "invalid serializeVer", cmd.SerializeVer)
ack, _ := cc.SendCmd(&mt.ToCltKick{Reason: mt.UnsupportedVer})
select {
case <-cc.Closed():
case <-ack:
cc.Close()
}
return
}
if cmd.MaxProtoVer < protoVer {
cc.Log("<-", "invalid protoVer", cmd.MaxProtoVer)
ack, _ := cc.SendCmd(&mt.ToCltKick{Reason: mt.UnsupportedVer})
select {
case <-cc.Closed():
case <-ack:
cc.Close()
}
return
}
if len(cmd.PlayerName) == 0 || len(cmd.PlayerName) > maxPlayerNameLen {
cc.Log("<-", "invalid player name length")
ack, _ := cc.SendCmd(&mt.ToCltKick{Reason: mt.BadName})
select {
case <-cc.Closed():
case <-ack:
cc.Close()
}
return
}
if !playerNameChars.MatchString(cmd.PlayerName) {
cc.Log("<-", "invalid player name")
ack, _ := cc.SendCmd(&mt.ToCltKick{Reason: mt.BadNameChars})
select {
case <-cc.Closed():
case <-ack:
cc.Close()
}
return
}
cc.name = cmd.PlayerName
cc.logger.SetPrefix(fmt.Sprintf("[%s %s] ", cc.RemoteAddr(), cc.Name()))
if authIface.Banned(cc.RemoteAddr().(*net.UDPAddr)) {
cc.Log("<-", "banned")
cc.Kick("Banned by proxy.")
return
}
playersMu.Lock()
_, ok := players[cc.Name()]
if ok {
cc.Log("<-", "already connected")
ack, _ := cc.SendCmd(&mt.ToCltKick{Reason: mt.AlreadyConnected})
select {
case <-cc.Closed():
case <-ack:
cc.Close()
}
playersMu.Unlock()
return
}
players[cc.Name()] = struct{}{}
playersMu.Unlock()
if cc.Name() == "singleplayer" {
cc.Log("<-", "name is singleplayer")
ack, _ := cc.SendCmd(&mt.ToCltKick{Reason: mt.BadName})
select {
case <-cc.Closed():
case <-ack:
cc.Close()
}
return
}
// user limit
if len(players) >= Conf().UserLimit {
cc.Log("<-", "player limit reached")
ack, _ := cc.SendCmd(&mt.ToCltKick{Reason: mt.TooManyClts})
select {
case <-cc.Closed():
case <-ack:
cc.Close()
}
return
}
// reply
if authIface.Exists(cc.Name()) {
cc.auth.method = mt.SRP
} else {
cc.auth.method = mt.FirstSRP
}
cc.SendCmd(&mt.ToCltHello{
SerializeVer: serializeVer,
ProtoVer: protoVer,
AuthMethods: cc.auth.method,
Username: cc.Name(),
})
return
case *mt.ToSrvFirstSRP:
if cc.state() == csInit {
if cc.auth.method != mt.FirstSRP {
cc.Log("->", "unauthorized password change")
ack, _ := cc.SendCmd(&mt.ToCltKick{Reason: mt.UnexpectedData})
select {
case <-cc.Closed():
case <-ack:
cc.Close()
}
return
}
cc.auth = struct {
method mt.AuthMethods
salt, srpA, srpB, srpM, srpK []byte
}{}
if cmd.EmptyPasswd && Conf().RequirePasswd {
cc.Log("<-", "empty password disallowed")
ack, _ := cc.SendCmd(&mt.ToCltKick{Reason: mt.EmptyPasswd})
select {
case <-cc.Closed():
case <-ack:
cc.Close()
}
return
}
if err := authIface.SetPasswd(cc.Name(), cmd.Salt, cmd.Verifier); err != nil {
cc.Log("<-", "set password fail")
ack, _ := cc.SendCmd(&mt.ToCltKick{Reason: mt.SrvErr})
select {
case <-cc.Closed():
case <-ack:
cc.Close()
}
return
}
cc.Log("->", "set password")
cc.SendCmd(&mt.ToCltAcceptAuth{
PlayerPos: mt.Pos{0, 5, 0},
MapSeed: 0,
SendInterval: Conf().SendInterval,
SudoAuthMethods: mt.SRP,
})
} else {
if cc.state() < csSudo {
cc.Log("->", "unauthorized sudo action")
return
}
cc.setState(csActive)
if err := authIface.SetPasswd(cc.Name(), cmd.Salt, cmd.Verifier); err != nil {
cc.Log("<-", "change password fail")
cc.SendChatMsg("Password change failed or unavailable.")
return
}
cc.Log("->", "change password")
cc.SendChatMsg("Password change successful.")
}
return
case *mt.ToSrvSRPBytesA:
wantSudo := cc.state() == csActive
if cc.state() != csInit && cc.state() != csActive {
cc.Log("->", "unexpected authentication")
return
}
if !wantSudo && cc.auth.method != mt.SRP {
cc.Log("<-", "multiple authentication attempts")
if wantSudo {
cc.SendCmd(&mt.ToCltDenySudoMode{})
return
}
ack, _ := cc.SendCmd(&mt.ToCltKick{Reason: mt.UnexpectedData})
select {
case <-cc.Closed():
case <-ack:
cc.Close()
}
return
}
if !cmd.NoSHA1 {
cc.Log("<-", "unsupported SHA1 auth")
return
}
cc.auth.method = mt.SRP
salt, verifier, err := authIface.Passwd(cc.Name())
if err != nil {
cc.Log("<-", "SRP data retrieval fail")
ack, _ := cc.SendCmd(&mt.ToCltKick{Reason: mt.SrvErr})
select {
case <-cc.Closed():
case <-ack:
cc.Close()
}
return
}
cc.auth.salt = salt
cc.auth.srpA = cmd.A
cc.auth.srpB, _, cc.auth.srpK, err = srp.Handshake(cc.auth.srpA, verifier)
if err != nil || cc.auth.srpB == nil {
cc.Log("<-", "SRP safety check fail")
ack, _ := cc.SendCmd(&mt.ToCltKick{Reason: mt.UnexpectedData})
select {
case <-cc.Closed():
case <-ack:
cc.Close()
}
return
}
cc.SendCmd(&mt.ToCltSRPBytesSaltB{
Salt: cc.auth.salt,
B: cc.auth.srpB,
})
return
case *mt.ToSrvSRPBytesM:
wantSudo := cc.state() == csActive
if cc.state() != csInit && cc.state() != csActive {
cc.Log("->", "unexpected authentication")
return
}
if cc.auth.method != mt.SRP {
cc.Log("<-", "multiple authentication attempts")
if wantSudo {
cc.SendCmd(&mt.ToCltDenySudoMode{})
return
}
ack, _ := cc.SendCmd(&mt.ToCltKick{Reason: mt.UnexpectedData})
select {
case <-cc.Closed():
case <-ack:
cc.Close()
}
return
}
M := srp.ClientProof([]byte(cc.Name()), cc.auth.salt, cc.auth.srpA, cc.auth.srpB, cc.auth.srpK)
if subtle.ConstantTimeCompare(cmd.M, M) == 1 {
cc.auth = struct {
method mt.AuthMethods
salt, srpA, srpB, srpM, srpK []byte
}{}
if wantSudo {
cc.setState(csSudo)
cc.SendCmd(&mt.ToCltAcceptSudoMode{})
} else {
cc.SendCmd(&mt.ToCltAcceptAuth{
PlayerPos: mt.Pos{0, 5, 0},
MapSeed: 0,
SendInterval: Conf().SendInterval,
SudoAuthMethods: mt.SRP,
})
}
} else {
if wantSudo {
cc.Log("<-", "invalid password (sudo)")
cc.SendCmd(&mt.ToCltDenySudoMode{})
return
}
cc.Log("<-", "invalid password")
ack, _ := cc.SendCmd(&mt.ToCltKick{Reason: mt.WrongPasswd})
select {
case <-cc.Closed():
case <-ack:
cc.Close()
}
return
}
return
case *mt.ToSrvInit2:
var remotes []string
var err error
cc.itemDefs, cc.aliases, cc.nodeDefs, cc.p0Map, cc.p0SrvMap, cc.media, remotes, err = muxContent(cc.Name())
if err != nil {
cc.Log("<-", err.Error())
cc.Kick("Content multiplexing failed.")
return
}
cc.SendCmd(&mt.ToCltItemDefs{
Defs: cc.itemDefs,
Aliases: cc.aliases,
})
cc.SendCmd(&mt.ToCltNodeDefs{Defs: cc.nodeDefs})
cc.itemDefs = []mt.ItemDef{}
cc.nodeDefs = []mt.NodeDef{}
var files []struct{ Name, Base64SHA1 string }
for _, f := range cc.media {
files = append(files, struct{ Name, Base64SHA1 string }{
Name: f.name,
Base64SHA1: f.base64SHA1,
})
}
cc.SendCmd(&mt.ToCltAnnounceMedia{
Files: files,
URL: strings.Join(remotes, ","),
})
cc.lang = cmd.Lang
var csmrf mt.CSMRestrictionFlags
if Conf().CSMRF.NoCSMs {
csmrf |= mt.NoCSMs
}
if !Conf().CSMRF.ChatMsgs {
csmrf |= mt.NoChatMsgs
}
if !Conf().CSMRF.ItemDefs {
csmrf |= mt.NoItemDefs
}
if !Conf().CSMRF.NodeDefs {
csmrf |= mt.NoNodeDefs
}
if !Conf().CSMRF.NoLimitMapRange {
csmrf |= mt.LimitMapRange
}
if !Conf().CSMRF.PlayerList {
csmrf |= mt.NoPlayerList
}
cc.SendCmd(&mt.ToCltCSMRestrictionFlags{
Flags: csmrf,
MapRange: Conf().MapRange,
})
return
case *mt.ToSrvReqMedia:
cc.sendMedia(cmd.Filenames)
return
case *mt.ToSrvCltReady:
cc.major = cmd.Major
cc.minor = cmd.Minor
cc.patch = cmd.Patch
cc.reservedVer = cmd.Reserved
cc.versionStr = cmd.Version
cc.formspecVer = cmd.Formspec
cc.setState(csActive)
close(cc.initCh)
return
case *mt.ToSrvInteract:
if srv == nil {
cc.Log("->", "no server")
return
}
if _, ok := cmd.Pointed.(*mt.PointedAO); ok {
srv.swapAOID(&cmd.Pointed.(*mt.PointedAO).ID)
}
if handleInteraction(cmd, cc) { // if return true: already handled
return
}
case *mt.ToSrvChatMsg:
done := make(chan struct{})
go func(done chan<- struct{}) {
result, isCmd := onChatMsg(cc, cmd)
if !isCmd {
forward(pkt)
} else if result != "" {
cc.SendChatMsg(result)
}
close(done)
}(done)
go func(done <-chan struct{}) {
select {
case <-done:
case <-time.After(ChatCmdTimeout):
cmdName := strings.Split(cmd.Msg, " ")[0]
cc.SendChatMsg("Command", cmdName, "is taking suspiciously long to execute.")
}
}(done)
return
}
forward(pkt)
}
func (sc *ServerConn) process(pkt mt.Pkt) {
clt := sc.client()
if clt == nil {
sc.Log("<-", "no client")
return
}
switch cmd := pkt.Cmd.(type) {
case *mt.ToCltHello:
if sc.auth.method != 0 {
sc.Log("<-", "unexpected authentication")
sc.Close()
return
}
sc.setState(csActive)
if cmd.AuthMethods&mt.FirstSRP != 0 {
sc.auth.method = mt.FirstSRP
} else {
sc.auth.method = mt.SRP
}
if cmd.SerializeVer != serializeVer {
sc.Log("<-", "invalid serializeVer")
return
}
switch sc.auth.method {
case mt.SRP:
var err error
sc.auth.srpA, sc.auth.a, err = srp.InitiateHandshake()
if err != nil {
sc.Log("->", err)
return
}
sc.SendCmd(&mt.ToSrvSRPBytesA{
A: sc.auth.srpA,
NoSHA1: true,
})
case mt.FirstSRP:
id := strings.ToLower(clt.Name())
salt, verifier, err := srp.NewClient([]byte(id), []byte{})
if err != nil {
sc.Log("->", err)
return
}
sc.SendCmd(&mt.ToSrvFirstSRP{
Salt: salt,
Verifier: verifier,
EmptyPasswd: true,
})
default:
sc.Log("<->", "invalid auth method")
sc.Close()
}
return
case *mt.ToCltSRPBytesSaltB:
if sc.auth.method != mt.SRP {
sc.Log("<-", "multiple authentication attempts")
return
}
id := strings.ToLower(clt.Name())
var err error
sc.auth.srpK, err = srp.CompleteHandshake(sc.auth.srpA, sc.auth.a, []byte(id), []byte{}, cmd.Salt, cmd.B)
if err != nil {
sc.Log("->", err)
return
}
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")
return
}
sc.SendCmd(&mt.ToSrvSRPBytesM{
M: M,
})
return
case *mt.ToCltKick:
sc.Log("<-", "deny access", cmd)
if cmd.Reason == mt.Shutdown || cmd.Reason == mt.Crash || cmd.Reason == mt.SrvErr || cmd.Reason == mt.TooManyClts || cmd.Reason == mt.UnsupportedVer {
clt.SendChatMsg(cmd.String())
for _, srvName := range FallbackServers(sc.name) {
if err := clt.Hop(srvName); err != nil {
clt.Log("<-", err)
break
}
}
return
}
ack, _ := clt.SendCmd(cmd)
select {
case <-clt.Closed():
case <-ack:
clt.Close()
sc.mu.Lock()
sc.clt = nil
sc.mu.Unlock()
}
return
case *mt.ToCltAcceptAuth:
sc.auth = struct {
method mt.AuthMethods
salt, srpA, a, srpK []byte
}{}
sc.SendCmd(&mt.ToSrvInit2{Lang: clt.lang})
return
case *mt.ToCltDenySudoMode:
sc.Log("<-", "deny sudo")
return
case *mt.ToCltAcceptSudoMode:
sc.Log("<-", "accept sudo")
sc.setState(csSudo)
return
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(csActive)
close(sc.initCh)
return
case *mt.ToCltMedia:
return
case *mt.ToCltItemDefs:
return
case *mt.ToCltNodeDefs:
return
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.mediaPool + "_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()})
return
case *mt.ToCltAOMsgs:
for k := range cmd.Msgs {
sc.swapAOID(&cmd.Msgs[k].ID)
sc.handleAOMsg(cmd.Msgs[k].Msg)
}
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)
return
case *mt.ToCltCSMRestrictionFlags:
if Conf().DropCSMRF {
return
}
cmd.Flags &= ^mt.NoCSMs
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(),
})
return
case *mt.ToCltMediaPush:
prepend(sc.mediaPool, &cmd.Filename)
var exit bool
for _, f := range clt.media {
if f.name == cmd.Filename {
exit = true
break
}
}
if exit {
break
}
if cmd.ShouldCache {
cacheMedia(mediaFile{
name: cmd.Filename,
base64SHA1: b64.EncodeToString(cmd.SHA1[:]),
data: cmd.Data,
})
}
case *mt.ToCltSkyParams:
for i := range cmd.Textures {
prependTexture(sc.mediaPool, &cmd.Textures[i])
}
case *mt.ToCltSunParams:
prependTexture(sc.mediaPool, &cmd.Texture)
prependTexture(sc.mediaPool, &cmd.ToneMap)
prependTexture(sc.mediaPool, &cmd.Rise)
case *mt.ToCltMoonParams:
prependTexture(sc.mediaPool, &cmd.Texture)
prependTexture(sc.mediaPool, &cmd.ToneMap)
case *mt.ToCltSetHotbarParam:
prependTexture(sc.mediaPool, &cmd.Img)
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)
}
}
case *mt.ToCltSpawnParticle:
prependTexture(sc.mediaPool, &cmd.Texture)
sc.globalParam0(&cmd.NodeParam0)
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)
}
case *mt.ToCltAddNode:
sc.globalParam0(&cmd.Node.Param0)
case *mt.ToCltAddParticleSpawner:
prependTexture(sc.mediaPool, &cmd.Texture)
sc.swapAOID(&cmd.AttachedAOID)
sc.globalParam0(&cmd.NodeParam0)
sc.particleSpawners[cmd.ID] = struct{}{}
case *mt.ToCltDelParticleSpawner:
delete(sc.particleSpawners, cmd.ID)
case *mt.ToCltPlaySound:
prepend(sc.mediaPool, &cmd.Name)
sc.swapAOID(&cmd.SrcAOID)
if cmd.Loop {
sc.sounds[cmd.ID] = struct{}{}
}
case *mt.ToCltFadeSound:
delete(sc.sounds, cmd.ID)
case *mt.ToCltStopSound:
delete(sc.sounds, cmd.ID)
case *mt.ToCltAddHUD:
sc.prependHUD(cmd.Type, cmd)
sc.huds[cmd.ID] = cmd.Type
case *mt.ToCltChangeHUD:
sc.prependHUD(sc.huds[cmd.ID], cmd)
case *mt.ToCltRmHUD:
delete(sc.huds, cmd.ID)
case *mt.ToCltShowFormspec:
sc.prependFormspec(&cmd.Formspec)
case *mt.ToCltFormspecPrepend:
sc.prependFormspec(&cmd.Prepend)
case *mt.ToCltInvFormspec:
sc.prependFormspec(&cmd.Formspec)
case *mt.ToCltMinimapModes:
for i := range cmd.Modes {
prependTexture(sc.mediaPool, &cmd.Modes[i].Texture)
}
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)
}
case *mt.ToCltModChanSig:
switch cmd.Signal {
case mt.JoinOK:
if _, ok := clt.modChs[cmd.Channel]; ok {
return
}
clt.modChs[cmd.Channel] = struct{}{}
case mt.JoinFail:
fallthrough
case mt.LeaveOK:
delete(clt.modChs, cmd.Channel)
}
}
clt.Send(pkt)
}