aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHimbeerserverDE <himbeerserverde@gmail.com>2021-08-27 18:59:27 +0200
committerHimbeerserverDE <himbeerserverde@gmail.com>2021-08-27 18:59:27 +0200
commitb21345fba99d38c5e5ade695e32fc9e23ea48e98 (patch)
tree3d90d8abd9d356109d8690c5babd481abb363b55
parentca7c62308867f27a607fd17d72f926fcda05bb32 (diff)
Basic authentication server and content multiplexer
-rw-r--r--.gitignore6
-rw-r--r--auth.go21
-rw-r--r--auth_sqlite3.go146
-rw-r--r--client_conn.go352
-rw-r--r--config.go62
-rw-r--r--connect.go28
-rw-r--r--content.go380
-rw-r--r--go.mod9
-rw-r--r--go.sum6
-rw-r--r--listen.go47
-rw-r--r--main.go94
-rw-r--r--players.go6
-rw-r--r--server_conn.go142
13 files changed, 1299 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
index 66fd13c..2067b3f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,9 @@
# Dependency directories (remove the comment below to include it)
# vendor/
+
+# Configuration files
+*.json
+
+# Database files
+*.sqlite
diff --git a/auth.go b/auth.go
new file mode 100644
index 0000000..2f7b0c4
--- /dev/null
+++ b/auth.go
@@ -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
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..c82b722
--- /dev/null
+++ b/go.mod
@@ -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
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..0c59bb2
--- /dev/null
+++ b/go.sum
@@ -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
+}
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..74ba45b
--- /dev/null
+++ b/main.go
@@ -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})
+ }
+ }
+}