diff options
author | HimbeerserverDE <himbeerserverde@gmail.com> | 2021-08-27 18:59:27 +0200 |
---|---|---|
committer | HimbeerserverDE <himbeerserverde@gmail.com> | 2021-08-27 18:59:27 +0200 |
commit | b21345fba99d38c5e5ade695e32fc9e23ea48e98 (patch) | |
tree | 3d90d8abd9d356109d8690c5babd481abb363b55 | |
parent | ca7c62308867f27a607fd17d72f926fcda05bb32 (diff) |
Basic authentication server and content multiplexer
-rw-r--r-- | .gitignore | 6 | ||||
-rw-r--r-- | auth.go | 21 | ||||
-rw-r--r-- | auth_sqlite3.go | 146 | ||||
-rw-r--r-- | client_conn.go | 352 | ||||
-rw-r--r-- | config.go | 62 | ||||
-rw-r--r-- | connect.go | 28 | ||||
-rw-r--r-- | content.go | 380 | ||||
-rw-r--r-- | go.mod | 9 | ||||
-rw-r--r-- | go.sum | 6 | ||||
-rw-r--r-- | listen.go | 47 | ||||
-rw-r--r-- | main.go | 94 | ||||
-rw-r--r-- | players.go | 6 | ||||
-rw-r--r-- | server_conn.go | 142 |
13 files changed, 1299 insertions, 0 deletions
@@ -13,3 +13,9 @@ # Dependency directories (remove the comment below to include it) # vendor/ + +# Configuration files +*.json + +# Database files +*.sqlite @@ -0,0 +1,21 @@ +package main + +import "time" + +var authIface authBackend + +type user struct { + name string + salt []byte + verifier []byte + timestamp time.Time +} + +type authBackend interface { + Exists(name string) bool + Passwd(name string) (salt, verifier []byte, err error) + SetPasswd(name string, salt, verifier []byte) error + Timestamp(name string) (time.Time, error) + Import(data []user) + Export() ([]user, error) +} diff --git a/auth_sqlite3.go b/auth_sqlite3.go new file mode 100644 index 0000000..02a3de4 --- /dev/null +++ b/auth_sqlite3.go @@ -0,0 +1,146 @@ +package main + +import ( + "database/sql" + "time" + + _ "github.com/mattn/go-sqlite3" +) + +type authSQLite3 struct { + db *sql.DB +} + +func (a authSQLite3) Exists(name string) bool { + if err := a.init(); err != nil { + return false + } + defer a.close() + + var name2 string + err := a.db.QueryRow(`SELECT name FROM user WHERE name = ?;`, name).Scan(&name2) + return err == nil +} + +func (a authSQLite3) Passwd(name string) (salt, verifier []byte, err error) { + if err = a.init(); err != nil { + return + } + defer a.close() + + a.db.QueryRow(`SELECT salt, verifier FROM user WHERE name = ?;`, name).Scan(&salt, &verifier) + a.updateTimestamp(name) + return +} + +func (a authSQLite3) SetPasswd(name string, salt, verifier []byte) error { + if err := a.init(); err != nil { + return err + } + defer a.close() + + _, err := a.db.Exec(`REPLACE INTO user (name, salt, verifier) VALUES (?, ?, ?);`, name, salt, verifier) + if err != nil { + return err + } + + a.updateTimestamp(name) + return nil +} + +func (a authSQLite3) Timestamp(name string) (time.Time, error) { + if err := a.init(); err != nil { + return time.Time{}, err + } + defer a.close() + + var tstr string + err := a.db.QueryRow(`SELECT timestamp FROM user WHERE name = ?;`, name).Scan(&tstr) + if err != nil { + return time.Time{}, err + } + + return time.Parse("2006-01-02 15:04:05", tstr) +} + +func (a authSQLite3) Import(in []user) { + if err := a.init(); err != nil { + return + } + defer a.close() + + a.db.Exec(`DELETE FROM user;`) + for _, u := range in { + a.SetPasswd(u.name, u.salt, u.verifier) + a.db.Query(`UPDATE user SET timestamp = ? WHERE name = ?;`, u.timestamp.Format("2006-01-02 15:04:05"), u.name) + } +} + +func (a authSQLite3) Export() ([]user, error) { + if err := a.init(); err != nil { + return nil, err + } + defer a.close() + + rows, err := a.db.Query(`SELECT * FROM user;`) + if err != nil { + return nil, err + } + defer rows.Close() + + var names []string + for rows.Next() { + var name string + if err := rows.Scan(&name); err != nil { + return nil, err + } + + names = append(names, name) + } + + var out []user + for _, name := range names { + var u user + u.timestamp, err = a.Timestamp(name) + if err != nil { + return nil, err + } + + u.salt, u.verifier, err = a.Passwd(name) + if err != nil { + return nil, err + } + + out = append(out, u) + } + + return out, nil +} + +func (a authSQLite3) updateTimestamp(name string) { + a.db.Exec(`UPDATE user SET timestamp = datetime("now") WHERE name = ?;`, name) +} + +func (a *authSQLite3) init() error { + var err error + a.db, err = sql.Open("sqlite3", "auth.sqlite") + if err != nil { + return err + } + + init := `CREATE TABLE IF NOT EXISTS user ( + name VARCHAR(20) PRIMARY KEY NOT NULL, + salt BLOB NOT NULL, + verifier BLOB NOT NULL, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP);` + + if _, err := a.db.Exec(init); err != nil { + return err + } + + return nil +} + +func (a authSQLite3) close() error { + return a.db.Close() +} 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) + } + } +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..6bbe840 --- /dev/null +++ b/config.go @@ -0,0 +1,62 @@ +package main + +import ( + "encoding/json" + "log" + "os" +) + +const latestSerializeVer = 0x1c +const latestProtoVer = 39 +const maxPlayerNameLen = 20 +const playerNameChars = "^[a-zA-Z0-9-_]+$" +const bytesPerMediaBunch = 5000 + +const defaultSendInterval = 0.09 +const defaultUserLimit = 10 +const defaultAuthBackend = "sqlite3" +const defaultBindAddr = ":40000" + +var conf Config + +type Config struct { + RequirePasswd bool + SendInterval float32 + UserLimit int + AuthBackend string + BindAddr string + Servers []struct { + Name string + Addr string + } +} + +func loadConfig() error { + oldConf := conf + + conf.SendInterval = defaultSendInterval + conf.UserLimit = defaultUserLimit + conf.AuthBackend = defaultAuthBackend + conf.BindAddr = defaultBindAddr + + f, err := os.OpenFile("config.json", os.O_RDWR|os.O_CREATE, 0666) + if err != nil { + conf = oldConf + return err + } + defer f.Close() + + if fi, _ := f.Stat(); fi.Size() == 0 { + f.WriteString("{\n\n}\n") + f.Seek(0, os.SEEK_SET) + } + + decoder := json.NewDecoder(f) + if err := decoder.Decode(&conf); err != nil { + conf = oldConf + return err + } + + log.Print("{←|⇶} load config") + return nil +} diff --git a/connect.go b/connect.go new file mode 100644 index 0000000..7ecf30a --- /dev/null +++ b/connect.go @@ -0,0 +1,28 @@ +package main + +import ( + "net" + + "github.com/anon55555/mt" +) + +func connect(conn net.Conn) *serverConn { + sc := &serverConn{ + Peer: mt.Connect(conn), + } + + go handleSrv(sc) + return sc +} + +func connectContent(conn net.Conn, name, userName string) *contentConn { + cc := &contentConn{ + Peer: mt.Connect(conn), + doneCh: make(chan struct{}), + name: name, + userName: userName, + } + + go handleContent(cc) + return cc +} diff --git a/content.go b/content.go new file mode 100644 index 0000000..2e01e46 --- /dev/null +++ b/content.go @@ -0,0 +1,380 @@ +package main + +import ( + "errors" + "fmt" + "log" + "net" + "sync" + "time" + + "github.com/HimbeerserverDE/srp" + "github.com/anon55555/mt" +) + +type mediaFile struct { + name string + base64SHA1 string + data []byte +} + +type contentConn struct { + mt.Peer + + state clientState + name, userName string + doneCh chan struct{} + + auth struct { + method mt.AuthMethods + salt, srpA, a, srpK []byte + } + + itemDefs []mt.ItemDef + aliases []struct{ Alias, Orig string } + + nodeDefs []mt.NodeDef + + media []mediaFile +} + +func (cc *contentConn) done() <-chan struct{} { return cc.doneCh } + +func (cc *contentConn) log(dir, msg string) { + log.Printf("{←|⇶} %s {%s} %s", dir, cc.name, msg) +} + +func handleContent(cc *contentConn) { + defer close(cc.doneCh) + + go func() { + for cc.state == csCreated { + cc.SendCmd(&mt.ToSrvInit{ + SerializeVer: latestSerializeVer, + MinProtoVer: latestProtoVer, + MaxProtoVer: latestProtoVer, + PlayerName: cc.userName, + }) + time.Sleep(500 * time.Millisecond) + } + }() + + for { + pkt, err := cc.Recv() + if err != nil { + if errors.Is(err, net.ErrClosed) { + cc.log("<->", "disconnect") + break + } + + cc.log("-->", err.Error()) + continue + } + + switch cmd := pkt.Cmd.(type) { + case *mt.ToCltHello: + if cc.auth.method != 0 { + cc.log("<--", "unexpected authentication") + cc.Close() + break + } + + cc.state++ + + if cmd.AuthMethods&mt.FirstSRP == mt.FirstSRP { + cc.auth.method = mt.FirstSRP + } else { + cc.auth.method = mt.SRP + } + + if cmd.SerializeVer != latestSerializeVer { + cc.log("<--", "invalid serializeVer") + break + } + + switch cc.auth.method { + case mt.SRP: + cc.auth.srpA, cc.auth.a, err = srp.InitiateHandshake() + if err != nil { + cc.log("-->", err.Error()) + break + } + + cc.SendCmd(&mt.ToSrvSRPBytesA{ + A: cc.auth.srpA, + NoSHA1: true, + }) + case mt.FirstSRP: + salt, verifier, err := srp.NewClient([]byte(cc.userName), []byte{}) + if err != nil { + cc.log("-->", err.Error()) + break + } + + cc.SendCmd(&mt.ToSrvFirstSRP{ + Salt: salt, + Verifier: verifier, + EmptyPasswd: true, + }) + default: + cc.log("<->", "invalid auth method") + cc.Close() + } + case *mt.ToCltSRPBytesSaltB: + if cc.auth.method != mt.SRP { + cc.log("<--", "multiple authentication attempts") + break + } + + cc.auth.srpK, err = srp.CompleteHandshake(cc.auth.srpA, cc.auth.a, []byte(cc.userName), []byte{}, cmd.Salt, cmd.B) + if err != nil { + cc.log("-->", err.Error()) + break + } + + M := srp.ClientProof([]byte(cc.userName), cmd.Salt, cc.auth.srpA, cmd.B, cc.auth.srpK) + if M == nil { + cc.log("<--", "SRP safety check fail") + break + } + + cc.SendCmd(&mt.ToSrvSRPBytesM{ + M: M, + }) + case *mt.ToCltDisco: + cc.log("<--", fmt.Sprintf("deny access %+v", cmd)) + case *mt.ToCltAcceptAuth: + cc.auth.method = 0 + cc.SendCmd(&mt.ToSrvInit2{}) + case *mt.ToCltItemDefs: + for _, def := range cmd.Defs { + cc.itemDefs = append(cc.itemDefs, def) + } + cc.aliases = cmd.Aliases + case *mt.ToCltNodeDefs: + for _, def := range cmd.Defs { + cc.nodeDefs = append(cc.nodeDefs, def) + } + case *mt.ToCltAnnounceMedia: + // ToDo: OOB media support + var filenames []string + + for _, f := range cmd.Files { + cc.media = append(cc.media, mediaFile{ + name: f.Name, + base64SHA1: f.Base64SHA1, + }) + + filenames = append(filenames, f.Name) + } + + cc.SendCmd(&mt.ToSrvReqMedia{Filenames: filenames}) + case *mt.ToCltMedia: + for _, f := range cmd.Files { + for _, af := range cc.media { + if af.name == f.Name { + af.data = f.Data + break + } + } + } + + if cmd.I == cmd.N-1 { + cc.Close() + } + } + } +} + +func (cc *clientConn) sendMedia(filenames []string) { + var bunches [][]struct { + Name string + Data []byte + } + bunches = append(bunches, []struct { + Name string + Data []byte + }{}) + + var bunchSize int + for _, filename := range filenames { + var known bool + for _, f := range cc.media { + if f.name == filename { + mfile := struct { + Name string + Data []byte + }{ + Name: f.name, + Data: f.data, + } + bunches[len(bunches)-1] = append(bunches[len(bunches)-1], mfile) + + bunchSize += len(f.data) + if bunchSize >= bytesPerMediaBunch { + bunches = append(bunches, []struct { + Name string + Data []byte + }{}) + bunchSize = 0 + } + + known = true + break + } + } + + if !known { + cc.log("-->", "request unknown media file") + continue + } + } + + for i := uint16(0); i < uint16(len(bunches)); i++ { + cc.SendCmd(&mt.ToCltMedia{ + N: uint16(len(bunches)), + I: i, + Files: bunches[i], + }) + } +} + +type param0Map map[string]map[mt.Content]mt.Content +type param0SrvMap map[mt.Content]struct { + name string + param0 mt.Content +} + +func muxItemDefs(conns []*contentConn) ([]mt.ItemDef, []struct{ Alias, Orig string }) { + var itemDefs []mt.ItemDef + var aliases []struct{ Alias, Orig string } + var wg sync.WaitGroup + + for _, cc := range conns { + wg.Add(1) + go func() { + <-cc.done() + for _, def := range cc.itemDefs { + def.Name = cc.name + "_" + def.Name + itemDefs = append(itemDefs, def) + // ToDo: Hand mux + } + + for _, alias := range cc.aliases { + aliases = append(aliases, struct{ Alias, Orig string }{ + Alias: cc.name + "_" + alias.Alias, + Orig: cc.name + "_" + alias.Orig, + }) + } + + wg.Done() + }() + } + + wg.Wait() + return itemDefs, aliases +} + +func muxNodeDefs(conns []*contentConn) (nodeDefs []mt.NodeDef, p0Map param0Map, p0SrvMap param0SrvMap) { + var wg sync.WaitGroup + var param0 mt.Content + + p0Map = make(param0Map) + p0SrvMap = make(param0SrvMap) + + for _, cc := range conns { + wg.Add(1) + go func() { + <-cc.done() + for _, def := range cc.nodeDefs { + if p0Map[cc.name] == nil { + p0Map[cc.name] = make(map[mt.Content]mt.Content) + } + + p0Map[cc.name][def.Param0] = param0 + p0SrvMap[param0] = struct { + name string + param0 mt.Content + }{ + name: cc.name, + param0: def.Param0, + } + + def.Param0 = param0 + def.Name = cc.name + "_" + def.Name + nodeDefs = append(nodeDefs, def) + + param0++ + if param0 >= mt.Unknown || param0 <= mt.Ignore { + param0 = mt.Ignore + 1 + } + } + + wg.Done() + }() + } + + wg.Wait() + return +} + +func muxMedia(conns []*contentConn) []mediaFile { + var media []mediaFile + var wg sync.WaitGroup + + for _, cc := range conns { + wg.Add(1) + go func() { + <-cc.done() + for _, f := range cc.media { + f.name = cc.name + "_" + f.name + media = append(media, f) + } + + wg.Done() + }() + } + + wg.Wait() + return media +} + +func muxContent(userName string) (itemDefs []mt.ItemDef, aliases []struct{ Alias, Orig string }, nodeDefs []mt.NodeDef, p0Map param0Map, p0SrvMap param0SrvMap, media []mediaFile, err error) { + var conns []*contentConn + for _, srv := range conf.Servers { + var addr *net.UDPAddr + addr, err = net.ResolveUDPAddr("udp", srv.Addr) + if err != nil { + return + } + + var conn *net.UDPConn + conn, err = net.DialUDP("udp", nil, addr) + if err != nil { + return + } + + conns = append(conns, connectContent(conn, srv.Name, userName)) + } + + var wg sync.WaitGroup + wg.Add(3) + + go func() { + defer wg.Done() + itemDefs, aliases = muxItemDefs(conns) + }() + + go func() { + defer wg.Done() + nodeDefs, p0Map, p0SrvMap = muxNodeDefs(conns) + }() + + go func() { + defer wg.Done() + media = muxMedia(conns) + }() + + wg.Wait() + return +} @@ -0,0 +1,9 @@ +module github.com/HimbeerserverDE/MTConnector + +go 1.16 + +require ( + github.com/HimbeerserverDE/srp v0.0.0-20210331172529-2b5dbec6b82b + github.com/anon55555/mt v0.0.0-20210623155243-152b6697e62c + github.com/mattn/go-sqlite3 v1.14.8 +) @@ -0,0 +1,6 @@ +github.com/HimbeerserverDE/srp v0.0.0-20210331172529-2b5dbec6b82b h1:xqNC1S76U5U+eFyzr5Ld+8aPOLaDFCw6f1uddjui+h8= +github.com/HimbeerserverDE/srp v0.0.0-20210331172529-2b5dbec6b82b/go.mod h1:pxNH8S2nh4n2DWE0ToX5GnnDr/uEAuaAhJsCpkDLIWw= +github.com/anon55555/mt v0.0.0-20210623155243-152b6697e62c h1:+T4M4zm8QMMnH56jopsKcCBxItSsb6jtulB3ZKFYETU= +github.com/anon55555/mt v0.0.0-20210623155243-152b6697e62c/go.mod h1:jH4ER+ahjl7H6TczzK+q4V9sXY++U2Geh6/vt3r4Xvs= +github.com/mattn/go-sqlite3 v1.14.8 h1:gDp86IdQsN/xWjIEmr9MF6o9mpksUgh0fu+9ByFxzIU= +github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= diff --git a/listen.go b/listen.go new file mode 100644 index 0000000..c1266b5 --- /dev/null +++ b/listen.go @@ -0,0 +1,47 @@ +package main + +import ( + "fmt" + "net" + + "github.com/anon55555/mt" +) + +type listener struct { + mtListener mt.Listener +} + +func listen(pc net.PacketConn) *listener { + return &listener{ + mtListener: mt.Listen(pc), + } +} + +func (l *listener) close() error { + return l.mtListener.Close() +} + +func (l *listener) addr() net.Addr { return l.mtListener.Addr() } + +func (l *listener) accept() (*clientConn, error) { + p, err := l.mtListener.Accept() + if err != nil { + return nil, err + } + + cc := &clientConn{ + Peer: p, + initCh: make(chan struct{}), + } + + cc.log("-->", "connect") + go handleClt(cc) + + select { + case <-cc.Closed(): + return nil, fmt.Errorf("%s is closed", cc.RemoteAddr()) + default: + } + + return cc, nil +} @@ -0,0 +1,94 @@ +package main + +import ( + "errors" + "log" + "net" + "os" + "os/signal" + "sync" + "syscall" + + "github.com/anon55555/mt" +) + +func main() { + if err := loadConfig(); err != nil { + log.Fatal("{←|⇶} ", err) + } + + var err error + switch conf.AuthBackend { + case "sqlite3": + authIface = authSQLite3{} + default: + log.Fatal("{←|⇶} invalid auth backend") + } + + addr, err := net.ResolveUDPAddr("udp", conf.BindAddr) + if err != nil { + log.Fatal("{←|⇶} ", err) + } + + pc, err := net.ListenUDP("udp", addr) + if err != nil { + log.Fatal("{←|⇶} ", err) + } + + l := listen(pc) + defer l.close() + + log.Print("{←|⇶} listening on ", l.addr()) + + clts := make(map[*clientConn]struct{}) + var mu sync.Mutex + + go func() { + sig := make(chan os.Signal) + signal.Notify(sig, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP) + <-sig + + mu.Lock() + defer mu.Unlock() + + for cc := range clts { + ack, _ := cc.SendCmd(&mt.ToCltDisco{Reason: mt.Shutdown}) + <-ack + cc.Close() + } + + os.Exit(0) + }() + + for { + cc, err := l.accept() + if err != nil { + if errors.Is(err, net.ErrClosed) { + break + } + + log.Print("{←|⇶} ", err) + continue + } + + mu.Lock() + clts[cc] = struct{}{} + mu.Unlock() + + go func() { + <-cc.Closed() + + mu.Lock() + defer mu.Unlock() + + delete(clts, cc) + }() + + go func() { + <-cc.init() + cc.log("<->", "handshake completed") + // ToDo: establish serverConn + // and start handler goroutines + }() + } +} diff --git a/players.go b/players.go new file mode 100644 index 0000000..183a405 --- /dev/null +++ b/players.go @@ -0,0 +1,6 @@ +package main + +import "sync" + +var players = make(map[string]struct{}) +var playersMu sync.RWMutex diff --git a/server_conn.go b/server_conn.go new file mode 100644 index 0000000..d2d39b1 --- /dev/null +++ b/server_conn.go @@ -0,0 +1,142 @@ +package main + +import ( + "errors" + "fmt" + "log" + "net" + "time" + + "github.com/HimbeerserverDE/srp" + "github.com/anon55555/mt" +) + +type serverConn struct { + mt.Peer + clt *clientConn + + state clientState + name string + initCh chan struct{} + + auth struct { + method mt.AuthMethods + salt, srpA, a, srpK []byte + } +} + +func (sc *serverConn) client() *clientConn { return sc.clt } + +func (sc *serverConn) init() <-chan struct{} { return sc.initCh } + +func (sc *serverConn) log(dir, msg string) { + log.Printf("{←|⇶} %s {%s} %s", dir, sc.name, msg) +} + +func handleSrv(sc *serverConn) { + if sc.client() == nil { + sc.log("-->", "no associated client") + } + + go func() { + for sc.state == csCreated { + 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) { + sc.log("<->", "disconnect") + break + } + + sc.log("-->", err.Error()) + continue + } + + switch cmd := pkt.Cmd.(type) { + case *mt.ToCltHello: + if sc.auth.method != 0 { + sc.log("<--", "unexpected authentication") + sc.Close() + break + } + + sc.state++ + + if cmd.AuthMethods&mt.FirstSRP == mt.FirstSRP { + 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.Error()) + break + } + + sc.SendCmd(&mt.ToSrvSRPBytesA{ + A: sc.auth.srpA, + NoSHA1: true, + }) + case mt.FirstSRP: + salt, verifier, err := srp.NewClient([]byte(sc.client().name), []byte{}) + if err != nil { + sc.log("-->", err.Error()) + 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(sc.client().name), []byte{}, cmd.Salt, cmd.B) + if err != nil { + sc.log("-->", err.Error()) + break + } + + M := srp.ClientProof([]byte(sc.client().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("<--", fmt.Sprintf("deny access %+v", cmd)) + case *mt.ToCltAcceptAuth: + sc.auth.method = 0 + sc.SendCmd(&mt.ToSrvInit2{Lang: sc.client().lang}) + } + } +} |