]> git.r.bdr.sh - rbdr/mobius/blobdiff - hotline/server.go
Refactor user access bitmap handling
[rbdr/mobius] / hotline / server.go
index 1edfd4a9d03a65224bd66a52c2b5dba185621780..3a43c95b4aaf15e69c70a294e1905f798e842cd7 100644 (file)
@@ -66,6 +66,9 @@ type Server struct {
 
        flatNewsMux sync.Mutex
        FlatNews    []byte
 
        flatNewsMux sync.Mutex
        FlatNews    []byte
+
+       banListMU sync.Mutex
+       banList   map[string]*time.Time
 }
 
 type PrivateChat struct {
 }
 
 type PrivateChat struct {
@@ -184,6 +187,7 @@ func (s *Server) Serve(ctx context.Context, ln net.Listener) error {
                go func() {
                        s.Logger.Infow("Connection established", "RemoteAddr", conn.RemoteAddr())
 
                go func() {
                        s.Logger.Infow("Connection established", "RemoteAddr", conn.RemoteAddr())
 
+                       defer conn.Close()
                        if err := s.handleNewConnection(connCtx, conn, conn.RemoteAddr().String()); err != nil {
                                if err == io.EOF {
                                        s.Logger.Infow("Client disconnected", "RemoteAddr", conn.RemoteAddr())
                        if err := s.handleNewConnection(connCtx, conn, conn.RemoteAddr().String()); err != nil {
                                if err == io.EOF {
                                        s.Logger.Infow("Client disconnected", "RemoteAddr", conn.RemoteAddr())
@@ -215,6 +219,7 @@ func NewServer(configDir string, netPort int, logger *zap.SugaredLogger, FS File
                Stats:         &Stats{StartTime: time.Now()},
                ThreadedNews:  &ThreadedNews{},
                FS:            FS,
                Stats:         &Stats{StartTime: time.Now()},
                ThreadedNews:  &ThreadedNews{},
                FS:            FS,
+               banList:       make(map[string]*time.Time),
        }
 
        var err error
        }
 
        var err error
@@ -233,6 +238,9 @@ func NewServer(configDir string, netPort int, logger *zap.SugaredLogger, FS File
                return nil, err
        }
 
                return nil, err
        }
 
+       // try to load the ban list, but ignore errors as this file may not be present or may be empty
+       _ = server.loadBanList(filepath.Join(configDir, "Banlist.yaml"))
+
        if err := server.loadThreadedNews(filepath.Join(configDir, "ThreadedNews.yaml")); err != nil {
                return nil, err
        }
        if err := server.loadThreadedNews(filepath.Join(configDir, "ThreadedNews.yaml")); err != nil {
                return nil, err
        }
@@ -300,16 +308,16 @@ func (s *Server) keepaliveHandler() {
                        if c.IdleTime > userIdleSeconds && !c.Idle {
                                c.Idle = true
 
                        if c.IdleTime > userIdleSeconds && !c.Idle {
                                c.Idle = true
 
-                               flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(*c.Flags)))
+                               flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(c.Flags)))
                                flagBitmap.SetBit(flagBitmap, userFlagAway, 1)
                                flagBitmap.SetBit(flagBitmap, userFlagAway, 1)
-                               binary.BigEndian.PutUint16(*c.Flags, uint16(flagBitmap.Int64()))
+                               binary.BigEndian.PutUint16(c.Flags, uint16(flagBitmap.Int64()))
 
                                c.sendAll(
                                        tranNotifyChangeUser,
                                        NewField(fieldUserID, *c.ID),
 
                                c.sendAll(
                                        tranNotifyChangeUser,
                                        NewField(fieldUserID, *c.ID),
-                                       NewField(fieldUserFlags, *c.Flags),
+                                       NewField(fieldUserFlags, c.Flags),
                                        NewField(fieldUserName, c.UserName),
                                        NewField(fieldUserName, c.UserName),
-                                       NewField(fieldUserIconID, *c.Icon),
+                                       NewField(fieldUserIconID, c.Icon),
                                )
                        }
                }
                                )
                        }
                }
@@ -317,6 +325,22 @@ func (s *Server) keepaliveHandler() {
        }
 }
 
        }
 }
 
+func (s *Server) writeBanList() error {
+       s.banListMU.Lock()
+       defer s.banListMU.Unlock()
+
+       out, err := yaml.Marshal(s.banList)
+       if err != nil {
+               return err
+       }
+       err = ioutil.WriteFile(
+               filepath.Join(s.ConfigDir, "Banlist.yaml"),
+               out,
+               0666,
+       )
+       return err
+}
+
 func (s *Server) writeThreadedNews() error {
        s.mux.Lock()
        defer s.mux.Unlock()
 func (s *Server) writeThreadedNews() error {
        s.mux.Lock()
        defer s.mux.Unlock()
@@ -339,12 +363,12 @@ func (s *Server) NewClientConn(conn io.ReadWriteCloser, remoteAddr string) *Clie
 
        clientConn := &ClientConn{
                ID:         &[]byte{0, 0},
 
        clientConn := &ClientConn{
                ID:         &[]byte{0, 0},
-               Icon:       &[]byte{0, 0},
-               Flags:      &[]byte{0, 0},
+               Icon:       []byte{0, 0},
+               Flags:      []byte{0, 0},
                UserName:   []byte{},
                Connection: conn,
                Server:     s,
                UserName:   []byte{},
                Connection: conn,
                Server:     s,
-               Version:    &[]byte{},
+               Version:    []byte{},
                AutoReply:  []byte{},
                transfers:  map[int]map[[4]byte]*FileTransfer{},
                Agreed:     false,
                AutoReply:  []byte{},
                transfers:  map[int]map[[4]byte]*FileTransfer{},
                Agreed:     false,
@@ -368,7 +392,7 @@ func (s *Server) NewClientConn(conn io.ReadWriteCloser, remoteAddr string) *Clie
 }
 
 // NewUser creates a new user account entry in the server map and config file
 }
 
 // NewUser creates a new user account entry in the server map and config file
-func (s *Server) NewUser(login, name, password string, access []byte) error {
+func (s *Server) NewUser(login, name, password string, access accessBitmap) error {
        s.mux.Lock()
        defer s.mux.Unlock()
 
        s.mux.Lock()
        defer s.mux.Unlock()
 
@@ -376,7 +400,7 @@ func (s *Server) NewUser(login, name, password string, access []byte) error {
                Login:    login,
                Name:     name,
                Password: hashAndSalt([]byte(password)),
                Login:    login,
                Name:     name,
                Password: hashAndSalt([]byte(password)),
-               Access:   &access,
+               Access:   access,
        }
        out, err := yaml.Marshal(&account)
        if err != nil {
        }
        out, err := yaml.Marshal(&account)
        if err != nil {
@@ -387,7 +411,7 @@ func (s *Server) NewUser(login, name, password string, access []byte) error {
        return s.FS.WriteFile(filepath.Join(s.ConfigDir, "Users", login+".yaml"), out, 0666)
 }
 
        return s.FS.WriteFile(filepath.Join(s.ConfigDir, "Users", login+".yaml"), out, 0666)
 }
 
-func (s *Server) UpdateUser(login, newLogin, name, password string, access []byte) error {
+func (s *Server) UpdateUser(login, newLogin, name, password string, access accessBitmap) error {
        s.mux.Lock()
        defer s.mux.Unlock()
 
        s.mux.Lock()
        defer s.mux.Unlock()
 
@@ -402,7 +426,7 @@ func (s *Server) UpdateUser(login, newLogin, name, password string, access []byt
        }
 
        account := s.Accounts[newLogin]
        }
 
        account := s.Accounts[newLogin]
-       account.Access = &access
+       account.Access = access
        account.Name = name
        account.Password = password
 
        account.Name = name
        account.Password = password
 
@@ -439,8 +463,8 @@ func (s *Server) connectedUsers() []Field {
                }
                user := User{
                        ID:    *c.ID,
                }
                user := User{
                        ID:    *c.ID,
-                       Icon:  *c.Icon,
-                       Flags: *c.Flags,
+                       Icon:  c.Icon,
+                       Flags: c.Flags,
                        Name:  string(c.UserName),
                }
                connectedUsers = append(connectedUsers, NewField(fieldUsernameWithInfo, user.Payload()))
                        Name:  string(c.UserName),
                }
                connectedUsers = append(connectedUsers, NewField(fieldUsernameWithInfo, user.Payload()))
@@ -448,6 +472,16 @@ func (s *Server) connectedUsers() []Field {
        return connectedUsers
 }
 
        return connectedUsers
 }
 
+func (s *Server) loadBanList(path string) error {
+       fh, err := os.Open(path)
+       if err != nil {
+               return err
+       }
+       decoder := yaml.NewDecoder(fh)
+
+       return decoder.Decode(s.banList)
+}
+
 // loadThreadedNews loads the threaded news data from disk
 func (s *Server) loadThreadedNews(threadedNewsPath string) error {
        fh, err := os.Open(threadedNewsPath)
 // loadThreadedNews loads the threaded news data from disk
 func (s *Server) loadThreadedNews(threadedNewsPath string) error {
        fh, err := os.Open(threadedNewsPath)
@@ -535,11 +569,36 @@ func (s *Server) handleNewConnection(ctx context.Context, rwc io.ReadWriteCloser
        }
 
        c := s.NewClientConn(rwc, remoteAddr)
        }
 
        c := s.NewClientConn(rwc, remoteAddr)
+
+       // check if remoteAddr is present in the ban list
+       if banUntil, ok := s.banList[strings.Split(remoteAddr, ":")[0]]; ok {
+               // permaban
+               if banUntil == nil {
+                       s.outbox <- *NewTransaction(
+                               tranServerMsg,
+                               c.ID,
+                               NewField(fieldData, []byte("You are permanently banned on this server")),
+                               NewField(fieldChatOptions, []byte{0, 0}),
+                       )
+                       time.Sleep(1 * time.Second)
+                       return nil
+               } else if time.Now().Before(*banUntil) {
+                       s.outbox <- *NewTransaction(
+                               tranServerMsg,
+                               c.ID,
+                               NewField(fieldData, []byte("You are temporarily banned on this server")),
+                               NewField(fieldChatOptions, []byte{0, 0}),
+                       )
+                       time.Sleep(1 * time.Second)
+                       return nil
+               }
+
+       }
        defer c.Disconnect()
 
        encodedLogin := clientLogin.GetField(fieldUserLogin).Data
        encodedPassword := clientLogin.GetField(fieldUserPassword).Data
        defer c.Disconnect()
 
        encodedLogin := clientLogin.GetField(fieldUserLogin).Data
        encodedPassword := clientLogin.GetField(fieldUserPassword).Data
-       *c.Version = clientLogin.GetField(fieldVersion).Data
+       c.Version = clientLogin.GetField(fieldVersion).Data
 
        var login string
        for _, char := range encodedLogin {
 
        var login string
        for _, char := range encodedLogin {
@@ -562,23 +621,27 @@ func (s *Server) handleNewConnection(ctx context.Context, rwc io.ReadWriteCloser
                        return err
                }
 
                        return err
                }
 
-               c.logger.Infow("Login failed", "clientVersion", fmt.Sprintf("%x", *c.Version))
+               c.logger.Infow("Login failed", "clientVersion", fmt.Sprintf("%x", c.Version))
 
                return nil
        }
 
 
                return nil
        }
 
-       if clientLogin.GetField(fieldUserName).Data != nil {
-               c.UserName = clientLogin.GetField(fieldUserName).Data
-       }
-
        if clientLogin.GetField(fieldUserIconID).Data != nil {
        if clientLogin.GetField(fieldUserIconID).Data != nil {
-               *c.Icon = clientLogin.GetField(fieldUserIconID).Data
+               c.Icon = clientLogin.GetField(fieldUserIconID).Data
        }
 
        c.Account = c.Server.Accounts[login]
 
        }
 
        c.Account = c.Server.Accounts[login]
 
+       if clientLogin.GetField(fieldUserName).Data != nil {
+               if c.Authorize(accessAnyName) {
+                       c.UserName = clientLogin.GetField(fieldUserName).Data
+               } else {
+                       c.UserName = []byte(c.Account.Name)
+               }
+       }
+
        if c.Authorize(accessDisconUser) {
        if c.Authorize(accessDisconUser) {
-               *c.Flags = []byte{0, 2}
+               c.Flags = []byte{0, 2}
        }
 
        s.outbox <- c.NewReply(clientLogin,
        }
 
        s.outbox <- c.NewReply(clientLogin,
@@ -588,24 +651,33 @@ func (s *Server) handleNewConnection(ctx context.Context, rwc io.ReadWriteCloser
        )
 
        // Send user access privs so client UI knows how to behave
        )
 
        // Send user access privs so client UI knows how to behave
-       c.Server.outbox <- *NewTransaction(tranUserAccess, c.ID, NewField(fieldUserAccess, *c.Account.Access))
-
-       // Show agreement to client
-       c.Server.outbox <- *NewTransaction(tranShowAgreement, c.ID, NewField(fieldData, s.Agreement))
+       c.Server.outbox <- *NewTransaction(tranUserAccess, c.ID, NewField(fieldUserAccess, c.Account.Access[:]))
+
+       // Accounts with accessNoAgreement do not receive the server agreement on login.  The behavior is different between
+       // client versions.  For 1.2.3 client, we do not send tranShowAgreement.  For other client versions, we send
+       // tranShowAgreement but with the NoServerAgreement field set to 1.
+       if c.Authorize(accessNoAgreement) {
+               // If client version is nil, then the client uses the 1.2.3 login behavior
+               if c.Version != nil {
+                       c.Server.outbox <- *NewTransaction(tranShowAgreement, c.ID, NewField(fieldNoServerAgreement, []byte{1}))
+               }
+       } else {
+               c.Server.outbox <- *NewTransaction(tranShowAgreement, c.ID, NewField(fieldData, s.Agreement))
+       }
 
        // Used simplified hotline v1.2.3 login flow for clients that do not send login info in tranAgreed
 
        // Used simplified hotline v1.2.3 login flow for clients that do not send login info in tranAgreed
-       if *c.Version == nil || bytes.Equal(*c.Version, nostalgiaVersion) {
+       if c.Version == nil || bytes.Equal(c.Version, nostalgiaVersion) {
                c.Agreed = true
                c.logger = c.logger.With("name", string(c.UserName))
                c.Agreed = true
                c.logger = c.logger.With("name", string(c.UserName))
-               c.logger.Infow("Login successful", "clientVersion", fmt.Sprintf("%x", *c.Version))
+               c.logger.Infow("Login successful", "clientVersion", fmt.Sprintf("%x", c.Version))
 
                for _, t := range c.notifyOthers(
                        *NewTransaction(
                                tranNotifyChangeUser, nil,
                                NewField(fieldUserName, c.UserName),
                                NewField(fieldUserID, *c.ID),
 
                for _, t := range c.notifyOthers(
                        *NewTransaction(
                                tranNotifyChangeUser, nil,
                                NewField(fieldUserName, c.UserName),
                                NewField(fieldUserID, *c.ID),
-                               NewField(fieldUserIconID, *c.Icon),
-                               NewField(fieldUserFlags, *c.Flags),
+                               NewField(fieldUserIconID, c.Icon),
+                               NewField(fieldUserFlags, c.Flags),
                        ),
                ) {
                        c.Server.outbox <- t
                        ),
                ) {
                        c.Server.outbox <- t