diff options
Diffstat (limited to 'client_conn.go')
-rw-r--r-- | client_conn.go | 352 |
1 files changed, 352 insertions, 0 deletions
diff --git a/client_conn.go b/client_conn.go new file mode 100644 index 0000000..000f029 --- /dev/null +++ b/client_conn.go @@ -0,0 +1,352 @@ +package main + +import ( + "crypto/subtle" + "errors" + "log" + "net" + "regexp" + "time" + + "github.com/HimbeerserverDE/srp" + "github.com/anon55555/mt" +) + +type clientState uint8 + +const ( + csCreated clientState = iota + csInit + csActive + csSudo +) + +type clientConn struct { + mt.Peer + srv *serverConn + + state clientState + name string + initCh chan struct{} + + auth struct { + method mt.AuthMethods + salt, srpA, srpB, srpM, srpK []byte + } + + lang string + + itemDefs []mt.ItemDef + aliases []struct{ Alias, Orig string } + nodeDefs []mt.NodeDef + p0Map param0Map + p0SrvMap param0SrvMap + media []mediaFile +} + +func (cc *clientConn) server() *serverConn { return cc.srv } + +func (cc *clientConn) init() <-chan struct{} { return cc.initCh } + +func (cc *clientConn) log(dir, msg string) { + if cc.name != "" { + log.Printf("{%s, %s} %s {←|⇶} %s", cc.name, cc.RemoteAddr(), dir, msg) + } else { + log.Printf("{%s} %s {←|⇶} %s", cc.RemoteAddr(), dir, msg) + } +} + +func handleClt(cc *clientConn) { + for { + pkt, err := cc.Recv() + if err != nil { + if errors.Is(err, net.ErrClosed) { + cc.log("<->", "disconnect") + if cc.name != "" { + playersMu.Lock() + delete(players, cc.name) + playersMu.Unlock() + } + + break + } + + cc.log("-->", err.Error()) + continue + } + + switch cmd := pkt.Cmd.(type) { + case *mt.ToSrvInit: + if cc.state > csCreated { + cc.log("-->", "duplicate init") + break + } + + cc.state = csInit + + if cmd.SerializeVer != latestSerializeVer { + cc.log("<--", "invalid serializeVer") + ack, _ := cc.SendCmd(&mt.ToCltDisco{Reason: mt.UnsupportedVer}) + <-ack + cc.Close() + break + } + + if cmd.MaxProtoVer < latestProtoVer { + cc.log("<--", "invalid protoVer") + ack, _ := cc.SendCmd(&mt.ToCltDisco{Reason: mt.UnsupportedVer}) + <-ack + cc.Close() + break + } + + if len(cmd.PlayerName) == 0 || len(cmd.PlayerName) > maxPlayerNameLen { + cc.log("<--", "invalid player name length") + ack, _ := cc.SendCmd(&mt.ToCltDisco{Reason: mt.BadName}) + <-ack + cc.Close() + break + } + + if ok, _ := regexp.MatchString(playerNameChars, cmd.PlayerName); !ok { + cc.log("<--", "invalid player name") + ack, _ := cc.SendCmd(&mt.ToCltDisco{Reason: mt.BadNameChars}) + <-ack + cc.Close() + break + } + + cc.name = cmd.PlayerName + + playersMu.Lock() + _, ok := players[cc.name] + if ok { + cc.log("<--", "already connected") + ack, _ := cc.SendCmd(&mt.ToCltDisco{Reason: mt.AlreadyConnected}) + <-ack + cc.Close() + + playersMu.Unlock() + break + } + + players[cc.name] = struct{}{} + playersMu.Unlock() + + if cc.name == "singleplayer" { + cc.log("<--", "name is singleplayer") + ack, _ := cc.SendCmd(&mt.ToCltDisco{Reason: mt.BadName}) + <-ack + cc.Close() + break + } + + // user limit + if len(players) >= conf.UserLimit { + cc.log("<--", "player limit reached") + ack, _ := cc.SendCmd(&mt.ToCltDisco{Reason: mt.TooManyClts}) + <-ack + cc.Close() + break + } + + // reply + if authIface.Exists(cc.name) { + cc.auth.method = mt.SRP + } else { + cc.auth.method = mt.FirstSRP + } + + cc.SendCmd(&mt.ToCltHello{ + SerializeVer: latestSerializeVer, + ProtoVer: latestProtoVer, + AuthMethods: cc.auth.method, + Username: cc.name, + }) + case *mt.ToSrvFirstSRP: + if cc.state == csInit { + if cc.auth.method != mt.FirstSRP { + cc.log("-->", "unauthorized password change") + ack, _ := cc.SendCmd(&mt.ToCltDisco{Reason: mt.UnexpectedData}) + <-ack + cc.Close() + break + } + + cc.auth.method = 0 + + if cmd.EmptyPasswd && conf.RequirePasswd { + cc.log("<--", "empty password disallowed") + ack, _ := cc.SendCmd(&mt.ToCltDisco{Reason: mt.EmptyPasswd}) + <-ack + cc.Close() + break + } + + if err := authIface.SetPasswd(cc.name, cmd.Salt, cmd.Verifier); err != nil { + cc.log("<--", "set password fail") + ack, _ := cc.SendCmd(&mt.ToCltDisco{Reason: mt.SrvErr}) + <-ack + cc.Close() + break + } + + 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") + break + } + + cc.state-- + + if err := authIface.SetPasswd(cc.name, cmd.Salt, cmd.Verifier); err != nil { + cc.log("<--", "change password fail") + cc.SendCmd(&mt.ToCltChatMsg{ + Type: mt.SysMsg, + Text: "Password change failed or unavailable.", + Timestamp: time.Now().Unix(), + }) + break + } + + cc.log("-->", "change password") + cc.SendCmd(&mt.ToCltChatMsg{ + Type: mt.SysMsg, + Text: "Password change successful.", + Timestamp: time.Now().Unix(), + }) + } + case *mt.ToSrvSRPBytesA: + wantSudo := cc.state == csActive + + if cc.state != csInit && cc.state != csActive { + cc.log("-->", "unexpected authentication") + break + } + + if !wantSudo && cc.auth.method != mt.SRP { + cc.log("<--", "multiple authentication attempts") + if wantSudo { + cc.SendCmd(&mt.ToCltDenySudoMode{}) + break + } + + ack, _ := cc.SendCmd(&mt.ToCltDisco{Reason: mt.UnexpectedData}) + <-ack + cc.Close() + break + } + + if !cmd.NoSHA1 { + cc.log("<--", "unsupported SHA1 auth") + break + } + + 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.ToCltDisco{Reason: mt.SrvErr}) + <-ack + cc.Close() + break + } + + 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.ToCltDisco{Reason: mt.UnexpectedData}) + <-ack + cc.Close() + break + } + + cc.SendCmd(&mt.ToCltSRPBytesSaltB{ + Salt: cc.auth.salt, + B: cc.auth.srpB, + }) + case *mt.ToSrvSRPBytesM: + wantSudo := cc.state == csActive + + if cc.state != csInit && cc.state != csActive { + cc.log("-->", "unexpected authentication") + break + } + + if cc.auth.method != mt.SRP { + cc.log("<--", "multiple authentication attempts") + if wantSudo { + cc.SendCmd(&mt.ToCltDenySudoMode{}) + break + } + + ack, _ := cc.SendCmd(&mt.ToCltDisco{Reason: mt.UnexpectedData}) + <-ack + cc.Close() + break + } + + 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.method = 0 + + if wantSudo { + cc.state++ + 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{}) + break + } + + cc.log("<--", "invalid password") + ack, _ := cc.SendCmd(&mt.ToCltDisco{Reason: mt.WrongPasswd}) + <-ack + cc.Close() + break + } + case *mt.ToSrvInit2: + cc.itemDefs, cc.aliases, cc.nodeDefs, cc.p0Map, cc.p0SrvMap, cc.media, err = muxContent(cc.name) + 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}) + cc.lang = cmd.Lang + case *mt.ToSrvReqMedia: + cc.sendMedia(cmd.Filenames) + } + } +} |