]> git.r.bdr.sh - rbdr/mobius/blobdiff - hotline/transaction_handlers.go
Disconnect banned users earlier in the login flow
[rbdr/mobius] / hotline / transaction_handlers.go
index 26eee12ef38a47931be98d034389f8269e48a50f..d99da53519a393c559fd6ce005157f7f34e0f8db 100644 (file)
@@ -6,7 +6,6 @@ import (
        "errors"
        "fmt"
        "gopkg.in/yaml.v3"
-       "io/ioutil"
        "math/big"
        "os"
        "path"
@@ -238,7 +237,7 @@ var TransactionHandlers = map[uint16]TransactionType{
 }
 
 func HandleChatSend(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
-       if !authorize(cc.Account.Access, accessSendChat) {
+       if !cc.Authorize(accessSendChat) {
                res = append(res, cc.NewErrReply(t, "You are not allowed to participate in chat."))
                return res, err
        }
@@ -249,14 +248,16 @@ func HandleChatSend(cc *ClientConn, t *Transaction) (res []Transaction, err erro
 
        // By holding the option key, Hotline chat allows users to send /me formatted messages like:
        // *** Halcyon does stuff
-       // This is indicated by the presence of the optional field fieldChatOptions in the transaction payload
-       if t.GetField(fieldChatOptions).Data != nil {
+       // This is indicated by the presence of the optional field fieldChatOptions set to a value of 1.
+       // Most clients do not send this option for normal chat messages.
+       if t.GetField(fieldChatOptions).Data != nil && bytes.Equal(t.GetField(fieldChatOptions).Data, []byte{0, 1}) {
                formattedMsg = fmt.Sprintf("\r*** %s %s", cc.UserName, t.GetField(fieldData).Data)
        }
 
+       // The ChatID field is used to identify messages as belonging to a private chat.
+       // All clients *except* Frogblast omit this field for public chat, but Frogblast sends a value of 00 00 00 00.
        chatID := t.GetField(fieldChatID).Data
-       // a non-nil chatID indicates the message belongs to a private chat
-       if chatID != nil {
+       if chatID != nil && !bytes.Equal([]byte{0, 0, 0, 0}, chatID) {
                chatInt := binary.BigEndian.Uint32(chatID)
                privChat := cc.Server.PrivateChats[chatInt]
 
@@ -276,7 +277,7 @@ func HandleChatSend(cc *ClientConn, t *Transaction) (res []Transaction, err erro
 
        for _, c := range sortedClients(cc.Server.Clients) {
                // Filter out clients that do not have the read chat permission
-               if authorize(c.Account.Access, accessReadChat) {
+               if c.Authorize(accessReadChat) {
                        res = append(res, *NewTransaction(tranChatMsg, c.ID, NewField(fieldData, []byte(formattedMsg))))
                }
        }
@@ -286,6 +287,7 @@ func HandleChatSend(cc *ClientConn, t *Transaction) (res []Transaction, err erro
 
 // HandleSendInstantMsg sends instant message to the user on the current server.
 // Fields used in the request:
+//
 //     103     User ID
 //     113     Options
 //             One of the following values:
@@ -299,6 +301,11 @@ func HandleChatSend(cc *ClientConn, t *Transaction) (res []Transaction, err erro
 // Fields used in the reply:
 // None
 func HandleSendInstantMsg(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
+       if !cc.Authorize(accessSendPrivMsg) {
+               res = append(res, cc.NewErrReply(t, "You are not allowed to send private messages."))
+               return res, err
+       }
+
        msg := t.GetField(fieldData)
        ID := t.GetField(fieldUserID)
 
@@ -317,14 +324,29 @@ func HandleSendInstantMsg(cc *ClientConn, t *Transaction) (res []Transaction, er
                reply.Fields = append(reply.Fields, NewField(fieldQuotingMsg, t.GetField(fieldQuotingMsg).Data))
        }
 
-       res = append(res, *reply)
-
        id, _ := byteToInt(ID.Data)
        otherClient, ok := cc.Server.Clients[uint16(id)]
        if !ok {
                return res, errors.New("invalid client ID")
        }
 
+       // Check if target user has "Refuse private messages" flag
+       flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(otherClient.Flags)))
+       if flagBitmap.Bit(userFLagRefusePChat) == 1 {
+               res = append(res,
+                       *NewTransaction(
+                               tranServerMsg,
+                               cc.ID,
+                               NewField(fieldData, []byte(string(otherClient.UserName)+" does not accept private messages.")),
+                               NewField(fieldUserName, otherClient.UserName),
+                               NewField(fieldUserID, *otherClient.ID),
+                               NewField(fieldOptions, []byte{0, 2}),
+                       ),
+               )
+       } else {
+               res = append(res, *reply)
+       }
+
        // Respond with auto reply if other client has it enabled
        if len(otherClient.AutoReply) > 0 {
                res = append(res,
@@ -399,12 +421,12 @@ func HandleSetFileInfo(cc *ClientConn, t *Transaction) (res []Transaction, err e
        if t.GetField(fieldFileComment).Data != nil {
                switch mode := fi.Mode(); {
                case mode.IsDir():
-                       if !authorize(cc.Account.Access, accessSetFolderComment) {
+                       if !cc.Authorize(accessSetFolderComment) {
                                res = append(res, cc.NewErrReply(t, "You are not allowed to set comments for folders."))
                                return res, err
                        }
                case mode.IsRegular():
-                       if !authorize(cc.Account.Access, accessSetFileComment) {
+                       if !cc.Authorize(accessSetFileComment) {
                                res = append(res, cc.NewErrReply(t, "You are not allowed to set comments for files."))
                                return res, err
                        }
@@ -433,7 +455,7 @@ func HandleSetFileInfo(cc *ClientConn, t *Transaction) (res []Transaction, err e
        if fileNewName != nil {
                switch mode := fi.Mode(); {
                case mode.IsDir():
-                       if !authorize(cc.Account.Access, accessRenameFolder) {
+                       if !cc.Authorize(accessRenameFolder) {
                                res = append(res, cc.NewErrReply(t, "You are not allowed to rename folders."))
                                return res, err
                        }
@@ -443,7 +465,7 @@ func HandleSetFileInfo(cc *ClientConn, t *Transaction) (res []Transaction, err e
                                return res, err
                        }
                case mode.IsRegular():
-                       if !authorize(cc.Account.Access, accessRenameFile) {
+                       if !cc.Authorize(accessRenameFile) {
                                res = append(res, cc.NewErrReply(t, "You are not allowed to rename files."))
                                return res, err
                        }
@@ -458,7 +480,7 @@ func HandleSetFileInfo(cc *ClientConn, t *Transaction) (res []Transaction, err e
                                return res, err
                        }
                        if err != nil {
-                               panic(err)
+                               return res, err
                        }
                }
        }
@@ -494,12 +516,12 @@ func HandleDeleteFile(cc *ClientConn, t *Transaction) (res []Transaction, err er
 
        switch mode := fi.Mode(); {
        case mode.IsDir():
-               if !authorize(cc.Account.Access, accessDeleteFolder) {
+               if !cc.Authorize(accessDeleteFolder) {
                        res = append(res, cc.NewErrReply(t, "You are not allowed to delete folders."))
                        return res, err
                }
        case mode.IsRegular():
-               if !authorize(cc.Account.Access, accessDeleteFile) {
+               if !cc.Authorize(accessDeleteFile) {
                        res = append(res, cc.NewErrReply(t, "You are not allowed to delete files."))
                        return res, err
                }
@@ -544,12 +566,12 @@ func HandleMoveFile(cc *ClientConn, t *Transaction) (res []Transaction, err erro
        }
        switch mode := fi.Mode(); {
        case mode.IsDir():
-               if !authorize(cc.Account.Access, accessMoveFolder) {
+               if !cc.Authorize(accessMoveFolder) {
                        res = append(res, cc.NewErrReply(t, "You are not allowed to move folders."))
                        return res, err
                }
        case mode.IsRegular():
-               if !authorize(cc.Account.Access, accessMoveFile) {
+               if !cc.Authorize(accessMoveFile) {
                        res = append(res, cc.NewErrReply(t, "You are not allowed to move files."))
                        return res, err
                }
@@ -564,7 +586,7 @@ func HandleMoveFile(cc *ClientConn, t *Transaction) (res []Transaction, err erro
 }
 
 func HandleNewFolder(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
-       if !authorize(cc.Account.Access, accessCreateFolder) {
+       if !cc.Authorize(accessCreateFolder) {
                res = append(res, cc.NewErrReply(t, "You are not allowed to create folders."))
                return res, err
        }
@@ -577,7 +599,7 @@ func HandleNewFolder(cc *ClientConn, t *Transaction) (res []Transaction, err err
        // fieldFilePath is only present for nested paths
        if t.GetField(fieldFilePath).Data != nil {
                var newFp FilePath
-               err := newFp.UnmarshalBinary(t.GetField(fieldFilePath).Data)
+               _, err := newFp.Write(t.GetField(fieldFilePath).Data)
                if err != nil {
                        return nil, err
                }
@@ -607,7 +629,7 @@ func HandleNewFolder(cc *ClientConn, t *Transaction) (res []Transaction, err err
 }
 
 func HandleSetUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
-       if !authorize(cc.Account.Access, accessModifyUser) {
+       if !cc.Authorize(accessModifyUser) {
                res = append(res, cc.NewErrReply(t, "You are not allowed to modify accounts."))
                return res, err
        }
@@ -618,8 +640,8 @@ func HandleSetUser(cc *ClientConn, t *Transaction) (res []Transaction, err error
        newAccessLvl := t.GetField(fieldUserAccess).Data
 
        account := cc.Server.Accounts[login]
-       account.Access = &newAccessLvl
        account.Name = userName
+       copy(account.Access[:], newAccessLvl)
 
        // If the password field is cleared in the Hotline edit user UI, the SetUser transaction does
        // not include fieldUserPassword
@@ -634,7 +656,7 @@ func HandleSetUser(cc *ClientConn, t *Transaction) (res []Transaction, err error
        if err != nil {
                return res, err
        }
-       if err := os.WriteFile(cc.Server.ConfigDir+"Users/"+login+".yaml", out, 0666); err != nil {
+       if err := os.WriteFile(filepath.Join(cc.Server.ConfigDir, "Users", login+".yaml"), out, 0666); err != nil {
                return res, err
        }
 
@@ -645,22 +667,22 @@ func HandleSetUser(cc *ClientConn, t *Transaction) (res []Transaction, err error
                        newT := NewTransaction(tranUserAccess, c.ID, NewField(fieldUserAccess, newAccessLvl))
                        res = append(res, *newT)
 
-                       flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(*c.Flags)))
-                       if authorize(c.Account.Access, accessDisconUser) {
+                       flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(c.Flags)))
+                       if c.Authorize(accessDisconUser) {
                                flagBitmap.SetBit(flagBitmap, userFlagAdmin, 1)
                        } else {
                                flagBitmap.SetBit(flagBitmap, userFlagAdmin, 0)
                        }
-                       binary.BigEndian.PutUint16(*c.Flags, uint16(flagBitmap.Int64()))
+                       binary.BigEndian.PutUint16(c.Flags, uint16(flagBitmap.Int64()))
 
                        c.Account.Access = account.Access
 
                        cc.sendAll(
                                tranNotifyChangeUser,
                                NewField(fieldUserID, *c.ID),
-                               NewField(fieldUserFlags, *c.Flags),
+                               NewField(fieldUserFlags, c.Flags),
                                NewField(fieldUserName, c.UserName),
-                               NewField(fieldUserIconID, *c.Icon),
+                               NewField(fieldUserIconID, c.Icon),
                        )
                }
        }
@@ -670,7 +692,7 @@ func HandleSetUser(cc *ClientConn, t *Transaction) (res []Transaction, err error
 }
 
 func HandleGetUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
-       if !authorize(cc.Account.Access, accessOpenUser) {
+       if !cc.Authorize(accessOpenUser) {
                res = append(res, cc.NewErrReply(t, "You are not allowed to view accounts."))
                return res, err
        }
@@ -685,21 +707,26 @@ func HandleGetUser(cc *ClientConn, t *Transaction) (res []Transaction, err error
                NewField(fieldUserName, []byte(account.Name)),
                NewField(fieldUserLogin, negateString(t.GetField(fieldUserLogin).Data)),
                NewField(fieldUserPassword, []byte(account.Password)),
-               NewField(fieldUserAccess, *account.Access),
+               NewField(fieldUserAccess, account.Access[:]),
        ))
        return res, err
 }
 
 func HandleListUsers(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
-       if !authorize(cc.Account.Access, accessOpenUser) {
+       if !cc.Authorize(accessOpenUser) {
                res = append(res, cc.NewErrReply(t, "You are not allowed to view accounts."))
                return res, err
        }
 
        var userFields []Field
        for _, acc := range cc.Server.Accounts {
-               userField := acc.MarshalBinary()
-               userFields = append(userFields, NewField(fieldData, userField))
+               b := make([]byte, 0, 100)
+               n, err := acc.Read(b)
+               if err != nil {
+                       return res, err
+               }
+
+               userFields = append(userFields, NewField(fieldData, b[:n]))
        }
 
        res = append(res, cc.NewReply(t, userFields...))
@@ -726,7 +753,7 @@ func HandleUpdateUser(cc *ClientConn, t *Transaction) (res []Transaction, err er
                        login := DecodeUserString(getField(fieldData, &subFields).Data)
                        cc.logger.Infow("DeleteUser", "login", login)
 
-                       if !authorize(cc.Account.Access, accessDeleteUser) {
+                       if !cc.Authorize(accessDeleteUser) {
                                res = append(res, cc.NewErrReply(t, "You are not allowed to delete accounts."))
                                return res, err
                        }
@@ -744,7 +771,7 @@ func HandleUpdateUser(cc *ClientConn, t *Transaction) (res []Transaction, err er
                        cc.logger.Infow("UpdateUser", "login", login)
 
                        // account dataFile, so this is an update action
-                       if !authorize(cc.Account.Access, accessModifyUser) {
+                       if !cc.Authorize(accessModifyUser) {
                                res = append(res, cc.NewErrReply(t, "You are not allowed to modify accounts."))
                                return res, err
                        }
@@ -757,7 +784,7 @@ func HandleUpdateUser(cc *ClientConn, t *Transaction) (res []Transaction, err er
                        }
 
                        if getField(fieldUserAccess, &subFields) != nil {
-                               acc.Access = &getField(fieldUserAccess, &subFields).Data
+                               copy(acc.Access[:], getField(fieldUserAccess, &subFields).Data)
                        }
 
                        err = cc.Server.UpdateUser(
@@ -765,7 +792,7 @@ func HandleUpdateUser(cc *ClientConn, t *Transaction) (res []Transaction, err er
                                DecodeUserString(getField(fieldUserLogin, &subFields).Data),
                                string(getField(fieldUserName, &subFields).Data),
                                acc.Password,
-                               *acc.Access,
+                               acc.Access,
                        )
                        if err != nil {
                                return res, err
@@ -773,17 +800,24 @@ func HandleUpdateUser(cc *ClientConn, t *Transaction) (res []Transaction, err er
                } else {
                        cc.logger.Infow("CreateUser", "login", login)
 
-                       if !authorize(cc.Account.Access, accessCreateUser) {
+                       if !cc.Authorize(accessCreateUser) {
                                res = append(res, cc.NewErrReply(t, "You are not allowed to create new accounts."))
                                return res, err
                        }
 
-                       err := cc.Server.NewUser(
-                               login,
-                               string(getField(fieldUserName, &subFields).Data),
-                               string(getField(fieldUserPassword, &subFields).Data),
-                               getField(fieldUserAccess, &subFields).Data,
-                       )
+                       newAccess := accessBitmap{}
+                       copy(newAccess[:], getField(fieldUserAccess, &subFields).Data[:])
+
+                       // Prevent account from creating new account with greater permission
+                       for i := 0; i < 64; i++ {
+                               if newAccess.IsSet(i) {
+                                       if !cc.Authorize(i) {
+                                               return append(res, cc.NewErrReply(t, "Cannot create account with more access than yourself.")), err
+                                       }
+                               }
+                       }
+
+                       err := cc.Server.NewUser(login, string(getField(fieldUserName, &subFields).Data), string(getField(fieldUserPassword, &subFields).Data), newAccess)
                        if err != nil {
                                return []Transaction{}, err
                        }
@@ -796,7 +830,7 @@ func HandleUpdateUser(cc *ClientConn, t *Transaction) (res []Transaction, err er
 
 // HandleNewUser creates a new user account
 func HandleNewUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
-       if !authorize(cc.Account.Access, accessCreateUser) {
+       if !cc.Authorize(accessCreateUser) {
                res = append(res, cc.NewErrReply(t, "You are not allowed to create new accounts."))
                return res, err
        }
@@ -809,12 +843,20 @@ func HandleNewUser(cc *ClientConn, t *Transaction) (res []Transaction, err error
                return res, err
        }
 
-       if err := cc.Server.NewUser(
-               login,
-               string(t.GetField(fieldUserName).Data),
-               string(t.GetField(fieldUserPassword).Data),
-               t.GetField(fieldUserAccess).Data,
-       ); err != nil {
+       newAccess := accessBitmap{}
+       copy(newAccess[:], t.GetField(fieldUserAccess).Data[:])
+
+       // Prevent account from creating new account with greater permission
+       for i := 0; i < 64; i++ {
+               if newAccess.IsSet(i) {
+                       if !cc.Authorize(i) {
+                               res = append(res, cc.NewErrReply(t, "Cannot create account with more access than yourself."))
+                               return res, err
+                       }
+               }
+       }
+
+       if err := cc.Server.NewUser(login, string(t.GetField(fieldUserName).Data), string(t.GetField(fieldUserPassword).Data), newAccess); err != nil {
                return []Transaction{}, err
        }
 
@@ -823,7 +865,7 @@ func HandleNewUser(cc *ClientConn, t *Transaction) (res []Transaction, err error
 }
 
 func HandleDeleteUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
-       if !authorize(cc.Account.Access, accessDeleteUser) {
+       if !cc.Authorize(accessDeleteUser) {
                res = append(res, cc.NewErrReply(t, "You are not allowed to delete accounts."))
                return res, err
        }
@@ -841,7 +883,7 @@ func HandleDeleteUser(cc *ClientConn, t *Transaction) (res []Transaction, err er
 
 // HandleUserBroadcast sends an Administrator Message to all connected clients of the server
 func HandleUserBroadcast(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
-       if !authorize(cc.Account.Access, accessBroadcast) {
+       if !cc.Authorize(accessBroadcast) {
                res = append(res, cc.NewErrReply(t, "You are not allowed to send broadcast messages."))
                return res, err
        }
@@ -856,17 +898,6 @@ func HandleUserBroadcast(cc *ClientConn, t *Transaction) (res []Transaction, err
        return res, err
 }
 
-func byteToInt(bytes []byte) (int, error) {
-       switch len(bytes) {
-       case 2:
-               return int(binary.BigEndian.Uint16(bytes)), nil
-       case 4:
-               return int(binary.BigEndian.Uint32(bytes)), nil
-       }
-
-       return 0, errors.New("unknown byte length")
-}
-
 // HandleGetClientInfoText returns user information for the specific user.
 //
 // Fields used in the request:
@@ -876,7 +907,7 @@ func byteToInt(bytes []byte) (int, error) {
 // 102 User name
 // 101 Data            User info text string
 func HandleGetClientInfoText(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
-       if !authorize(cc.Account.Access, accessGetClientInfo) {
+       if !cc.Authorize(accessGetClientInfo) {
                res = append(res, cc.NewErrReply(t, "You are not allowed to get client info."))
                return res, err
        }
@@ -902,28 +933,34 @@ func HandleGetUserNameList(cc *ClientConn, t *Transaction) (res []Transaction, e
 }
 
 func HandleTranAgreed(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
-       cc.Agreed = true
-       cc.UserName = t.GetField(fieldUserName).Data
-       *cc.Icon = t.GetField(fieldUserIconID).Data
+       if t.GetField(fieldUserName).Data != nil {
+               if cc.Authorize(accessAnyName) {
+                       cc.UserName = t.GetField(fieldUserName).Data
+               } else {
+                       cc.UserName = []byte(cc.Account.Name)
+               }
+       }
+
+       cc.Icon = t.GetField(fieldUserIconID).Data
 
        cc.logger = cc.logger.With("name", string(cc.UserName))
-       cc.logger.Infow("Login successful", "clientVersion", fmt.Sprintf("%x", *cc.Version))
+       cc.logger.Infow("Login successful", "clientVersion", fmt.Sprintf("%v", func() int { i, _ := byteToInt(cc.Version); return i }()))
 
        options := t.GetField(fieldOptions).Data
        optBitmap := big.NewInt(int64(binary.BigEndian.Uint16(options)))
 
-       flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(*cc.Flags)))
+       flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(cc.Flags)))
 
        // Check refuse private PM option
        if optBitmap.Bit(refusePM) == 1 {
                flagBitmap.SetBit(flagBitmap, userFlagRefusePM, 1)
-               binary.BigEndian.PutUint16(*cc.Flags, uint16(flagBitmap.Int64()))
+               binary.BigEndian.PutUint16(cc.Flags, uint16(flagBitmap.Int64()))
        }
 
        // Check refuse private chat option
        if optBitmap.Bit(refuseChat) == 1 {
                flagBitmap.SetBit(flagBitmap, userFLagRefusePChat, 1)
-               binary.BigEndian.PutUint16(*cc.Flags, uint16(flagBitmap.Int64()))
+               binary.BigEndian.PutUint16(cc.Flags, uint16(flagBitmap.Int64()))
        }
 
        // Check auto response
@@ -933,20 +970,19 @@ func HandleTranAgreed(cc *ClientConn, t *Transaction) (res []Transaction, err er
                cc.AutoReply = []byte{}
        }
 
-       for _, t := range cc.notifyOthers(
+       trans := cc.notifyOthers(
                *NewTransaction(
                        tranNotifyChangeUser, nil,
                        NewField(fieldUserName, cc.UserName),
                        NewField(fieldUserID, *cc.ID),
-                       NewField(fieldUserIconID, *cc.Icon),
-                       NewField(fieldUserFlags, *cc.Flags),
+                       NewField(fieldUserIconID, cc.Icon),
+                       NewField(fieldUserFlags, cc.Flags),
                ),
-       ) {
-               cc.Server.outbox <- t
-       }
+       )
+       res = append(res, trans...)
 
        if cc.Server.Config.BannerFile != "" {
-               cc.Server.outbox <- *NewTransaction(tranServerBanner, cc.ID, NewField(fieldBannerType, []byte("JPEG")))
+               res = append(res, *NewTransaction(tranServerBanner, cc.ID, NewField(fieldBannerType, []byte("JPEG"))))
        }
 
        res = append(res, cc.NewReply(t))
@@ -954,20 +990,11 @@ func HandleTranAgreed(cc *ClientConn, t *Transaction) (res []Transaction, err er
        return res, err
 }
 
-const defaultNewsDateFormat = "Jan02 15:04" // Jun23 20:49
-//  "Mon, 02 Jan 2006 15:04:05 MST"
-
-const defaultNewsTemplate = `From %s (%s):
-
-%s
-
-__________________________________________________________`
-
 // HandleTranOldPostNews updates the flat news
 // Fields used in this request:
 // 101 Data
 func HandleTranOldPostNews(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
-       if !authorize(cc.Account.Access, accessNewsPostArt) {
+       if !cc.Authorize(accessNewsPostArt) {
                res = append(res, cc.NewErrReply(t, "You are not allowed to post news."))
                return res, err
        }
@@ -992,7 +1019,7 @@ func HandleTranOldPostNews(cc *ClientConn, t *Transaction) (res []Transaction, e
        cc.Server.FlatNews = append([]byte(newsPost), cc.Server.FlatNews...)
 
        // update news on disk
-       if err := ioutil.WriteFile(cc.Server.ConfigDir+"MessageBoard.txt", cc.Server.FlatNews, 0644); err != nil {
+       if err := cc.Server.FS.WriteFile(filepath.Join(cc.Server.ConfigDir, "MessageBoard.txt"), cc.Server.FlatNews, 0644); err != nil {
                return res, err
        }
 
@@ -1007,31 +1034,67 @@ func HandleTranOldPostNews(cc *ClientConn, t *Transaction) (res []Transaction, e
 }
 
 func HandleDisconnectUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
-       if !authorize(cc.Account.Access, accessDisconUser) {
+       if !cc.Authorize(accessDisconUser) {
                res = append(res, cc.NewErrReply(t, "You are not allowed to disconnect users."))
                return res, err
        }
 
        clientConn := cc.Server.Clients[binary.BigEndian.Uint16(t.GetField(fieldUserID).Data)]
 
-       if authorize(clientConn.Account.Access, accessCannotBeDiscon) {
+       if clientConn.Authorize(accessCannotBeDiscon) {
                res = append(res, cc.NewErrReply(t, clientConn.Account.Login+" is not allowed to be disconnected."))
                return res, err
        }
 
-       if err := clientConn.Connection.Close(); err != nil {
-               return res, err
+       // If fieldOptions is set, then the client IP is banned in addition to disconnected.
+       // 00 01 = temporary ban
+       // 00 02 = permanent ban
+       if t.GetField(fieldOptions).Data != nil {
+               switch t.GetField(fieldOptions).Data[1] {
+               case 1:
+                       // send message: "You are temporarily banned on this server"
+                       cc.logger.Infow("Disconnect & temporarily ban " + string(clientConn.UserName))
+
+                       res = append(res, *NewTransaction(
+                               tranServerMsg,
+                               clientConn.ID,
+                               NewField(fieldData, []byte("You are temporarily banned on this server")),
+                               NewField(fieldChatOptions, []byte{0, 0}),
+                       ))
+
+                       banUntil := time.Now().Add(tempBanDuration)
+                       cc.Server.banList[strings.Split(clientConn.RemoteAddr, ":")[0]] = &banUntil
+                       cc.Server.writeBanList()
+               case 2:
+                       // send message: "You are permanently banned on this server"
+                       cc.logger.Infow("Disconnect & ban " + string(clientConn.UserName))
+
+                       res = append(res, *NewTransaction(
+                               tranServerMsg,
+                               clientConn.ID,
+                               NewField(fieldData, []byte("You are permanently banned on this server")),
+                               NewField(fieldChatOptions, []byte{0, 0}),
+                       ))
+
+                       cc.Server.banList[strings.Split(clientConn.RemoteAddr, ":")[0]] = nil
+                       cc.Server.writeBanList()
+               }
        }
 
-       res = append(res, cc.NewReply(t))
-       return res, err
+       // TODO: remove this awful hack
+       go func() {
+               time.Sleep(1 * time.Second)
+               clientConn.Disconnect()
+       }()
+
+       return append(res, cc.NewReply(t)), err
 }
 
 // HandleGetNewsCatNameList returns a list of news categories for a path
 // Fields used in the request:
 // 325 News path       (Optional)
 func HandleGetNewsCatNameList(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
-       if !authorize(cc.Account.Access, accessNewsReadArt) {
+       if !cc.Authorize(accessNewsReadArt) {
                res = append(res, cc.NewErrReply(t, "You are not allowed to read news."))
                return res, err
        }
@@ -1063,7 +1126,7 @@ func HandleGetNewsCatNameList(cc *ClientConn, t *Transaction) (res []Transaction
 }
 
 func HandleNewNewsCat(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
-       if !authorize(cc.Account.Access, accessNewsCreateCat) {
+       if !cc.Authorize(accessNewsCreateCat) {
                res = append(res, cc.NewErrReply(t, "You are not allowed to create news categories."))
                return res, err
        }
@@ -1090,7 +1153,7 @@ func HandleNewNewsCat(cc *ClientConn, t *Transaction) (res []Transaction, err er
 // 322 News category name
 // 325 News path
 func HandleNewNewsFldr(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
-       if !authorize(cc.Account.Access, accessNewsCreateFldr) {
+       if !cc.Authorize(accessNewsCreateFldr) {
                res = append(res, cc.NewErrReply(t, "You are not allowed to create news folders."))
                return res, err
        }
@@ -1114,13 +1177,15 @@ func HandleNewNewsFldr(cc *ClientConn, t *Transaction) (res []Transaction, err e
        return res, err
 }
 
+// HandleGetNewsArtData gets the list of article names at the specified news path.
+
 // Fields used in the request:
 // 325 News path       Optional
-//
-// Reply fields:
+
+// Fields used in the reply:
 // 321 News article list data  Optional
 func HandleGetNewsArtNameList(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
-       if !authorize(cc.Account.Access, accessNewsReadArt) {
+       if !cc.Authorize(accessNewsReadArt) {
                res = append(res, cc.NewErrReply(t, "You are not allowed to read news."))
                return res, err
        }
@@ -1140,47 +1205,51 @@ func HandleGetNewsArtNameList(cc *ClientConn, t *Transaction) (res []Transaction
        return res, err
 }
 
+// HandleGetNewsArtData requests information about the specific news article.
+// Fields used in the request:
+//
+// Request fields
+// 325 News path
+// 326 News article ID
+// 327 News article data flavor
+//
+// Fields used in the reply:
+// 328 News article title
+// 329 News article poster
+// 330 News article date
+// 331 Previous article ID
+// 332 Next article ID
+// 335 Parent article ID
+// 336 First child article ID
+// 327 News article data flavor        "Should be “text/plain”
+// 333 News article data       Optional (if data flavor is “text/plain”)
 func HandleGetNewsArtData(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
-       if !authorize(cc.Account.Access, accessNewsReadArt) {
+       if !cc.Authorize(accessNewsReadArt) {
                res = append(res, cc.NewErrReply(t, "You are not allowed to read news."))
                return res, err
        }
 
-       // Request fields
-       // 325  News fp
-       // 326  News article ID
-       // 327  News article data flavor
-
-       pathStrs := ReadNewsPath(t.GetField(fieldNewsPath).Data)
-
        var cat NewsCategoryListData15
        cats := cc.Server.ThreadedNews.Categories
 
-       for _, fp := range pathStrs {
+       for _, fp := range ReadNewsPath(t.GetField(fieldNewsPath).Data) {
                cat = cats[fp]
                cats = cats[fp].SubCats
        }
-       newsArtID := t.GetField(fieldNewsArtID).Data
 
-       convertedArtID := binary.BigEndian.Uint16(newsArtID)
+       // The official Hotline clients will send the article ID as 2 bytes if possible, but
+       // some third party clients such as Frogblast and Heildrun will always send 4 bytes
+       convertedID, err := byteToInt(t.GetField(fieldNewsArtID).Data)
+       if err != nil {
+               return res, err
+       }
 
-       art := cat.Articles[uint32(convertedArtID)]
+       art := cat.Articles[uint32(convertedID)]
        if art == nil {
                res = append(res, cc.NewReply(t))
                return res, err
        }
 
-       // Reply fields
-       // 328  News article title
-       // 329  News article poster
-       // 330  News article date
-       // 331  Previous article ID
-       // 332  Next article ID
-       // 335  Parent article ID
-       // 336  First child article ID
-       // 327  News article data flavor        "Should be “text/plain”
-       // 333  News article data       Optional (if data flavor is “text/plain”)
-
        res = append(res, cc.NewReply(t,
                NewField(fieldNewsArtTitle, []byte(art.Title)),
                NewField(fieldNewsArtPoster, []byte(art.Poster)),
@@ -1195,18 +1264,15 @@ func HandleGetNewsArtData(cc *ClientConn, t *Transaction) (res []Transaction, er
        return res, err
 }
 
+// HandleDelNewsItem deletes an existing threaded news folder or category from the server.
+// Fields used in the request:
+// 325 News path
+// Fields used in the reply:
+// None
 func HandleDelNewsItem(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
-       // Has multiple access flags: News Delete Folder (37) or News Delete Category (35)
-       // TODO: Implement
-
        pathStrs := ReadNewsPath(t.GetField(fieldNewsPath).Data)
 
-       // TODO: determine if path is a Folder (Bundle) or Category and check for permission
-
-       cc.logger.Infof("DelNewsItem %v", pathStrs)
-
        cats := cc.Server.ThreadedNews.Categories
-
        delName := pathStrs[len(pathStrs)-1]
        if len(pathStrs) > 1 {
                for _, fp := range pathStrs[0 : len(pathStrs)-1] {
@@ -1214,21 +1280,27 @@ func HandleDelNewsItem(cc *ClientConn, t *Transaction) (res []Transaction, err e
                }
        }
 
+       if bytes.Equal(cats[delName].Type, []byte{0, 3}) {
+               if !cc.Authorize(accessNewsDeleteCat) {
+                       return append(res, cc.NewErrReply(t, "You are not allowed to delete news categories.")), nil
+               }
+       } else {
+               if !cc.Authorize(accessNewsDeleteFldr) {
+                       return append(res, cc.NewErrReply(t, "You are not allowed to delete news folders.")), nil
+               }
+       }
+
        delete(cats, delName)
 
-       err = cc.Server.writeThreadedNews()
-       if err != nil {
+       if err := cc.Server.writeThreadedNews(); err != nil {
                return res, err
        }
 
-       // Reply params: none
-       res = append(res, cc.NewReply(t))
-
-       return res, err
+       return append(res, cc.NewReply(t)), nil
 }
 
 func HandleDelNewsArt(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
-       if !authorize(cc.Account.Access, accessNewsDeleteArt) {
+       if !cc.Authorize(accessNewsDeleteArt) {
                res = append(res, cc.NewErrReply(t, "You are not allowed to delete news articles."))
                return res, err
        }
@@ -1238,7 +1310,10 @@ func HandleDelNewsArt(cc *ClientConn, t *Transaction) (res []Transaction, err er
        // 326  News article ID
        // 337  News article – recursive delete       Delete child articles (1) or not (0)
        pathStrs := ReadNewsPath(t.GetField(fieldNewsPath).Data)
-       ID := binary.BigEndian.Uint16(t.GetField(fieldNewsArtID).Data)
+       ID, err := byteToInt(t.GetField(fieldNewsArtID).Data)
+       if err != nil {
+               return res, err
+       }
 
        // TODO: Delete recursive
        cats := cc.Server.GetNewsCatByPath(pathStrs[:len(pathStrs)-1])
@@ -1265,7 +1340,7 @@ func HandleDelNewsArt(cc *ClientConn, t *Transaction) (res []Transaction, err er
 // 327 News article data flavor                Currently “text/plain”
 // 333 News article data
 func HandlePostNewsArt(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
-       if !authorize(cc.Account.Access, accessNewsPostArt) {
+       if !cc.Authorize(accessNewsPostArt) {
                res = append(res, cc.NewErrReply(t, "You are not allowed to post news articles."))
                return res, err
        }
@@ -1276,13 +1351,21 @@ func HandlePostNewsArt(cc *ClientConn, t *Transaction) (res []Transaction, err e
        catName := pathStrs[len(pathStrs)-1]
        cat := cats[catName]
 
+       artID, err := byteToInt(t.GetField(fieldNewsArtID).Data)
+       if err != nil {
+               return res, err
+       }
+       convertedArtID := uint32(artID)
+       bs := make([]byte, 4)
+       binary.BigEndian.PutUint32(bs, convertedArtID)
+
        newArt := NewsArtData{
                Title:         string(t.GetField(fieldNewsArtTitle).Data),
                Poster:        string(cc.UserName),
                Date:          toHotlineTime(time.Now()),
                PrevArt:       []byte{0, 0, 0, 0},
                NextArt:       []byte{0, 0, 0, 0},
-               ParentArt:     append([]byte{0, 0}, t.GetField(fieldNewsArtID).Data...),
+               ParentArt:     bs,
                FirstChildArt: []byte{0, 0, 0, 0},
                DataFlav:      []byte("text/plain"),
                Data:          string(t.GetField(fieldNewsArtData).Data),
@@ -1306,9 +1389,9 @@ func HandlePostNewsArt(cc *ClientConn, t *Transaction) (res []Transaction, err e
        }
 
        // Update parent article with first child reply
-       parentID := binary.BigEndian.Uint16(t.GetField(fieldNewsArtID).Data)
+       parentID := convertedArtID
        if parentID != 0 {
-               parentArt := cat.Articles[uint32(parentID)]
+               parentArt := cat.Articles[parentID]
 
                if bytes.Equal(parentArt.FirstChildArt, []byte{0, 0, 0, 0}) {
                        binary.BigEndian.PutUint32(parentArt.FirstChildArt, nextID)
@@ -1328,7 +1411,7 @@ func HandlePostNewsArt(cc *ClientConn, t *Transaction) (res []Transaction, err e
 
 // HandleGetMsgs returns the flat news data
 func HandleGetMsgs(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
-       if !authorize(cc.Account.Access, accessNewsReadArt) {
+       if !cc.Authorize(accessNewsReadArt) {
                res = append(res, cc.NewErrReply(t, "You are not allowed to read news."))
                return res, err
        }
@@ -1339,7 +1422,7 @@ func HandleGetMsgs(cc *ClientConn, t *Transaction) (res []Transaction, err error
 }
 
 func HandleDownloadFile(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
-       if !authorize(cc.Account.Access, accessDownloadFile) {
+       if !cc.Authorize(accessDownloadFile) {
                res = append(res, cc.NewErrReply(t, "You are not allowed to download files."))
                return res, err
        }
@@ -1401,7 +1484,7 @@ func HandleDownloadFile(cc *ClientConn, t *Transaction) (res []Transaction, err
 
 // Download all files from the specified folder and sub-folders
 func HandleDownloadFolder(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
-       if !authorize(cc.Account.Access, accessDownloadFile) {
+       if !cc.Authorize(accessDownloadFile) {
                res = append(res, cc.NewErrReply(t, "You are not allowed to download folders."))
                return res, err
        }
@@ -1423,7 +1506,7 @@ func HandleDownloadFolder(cc *ClientConn, t *Transaction) (res []Transaction, er
        fileTransfer := cc.newFileTransfer(FolderDownload, t.GetField(fieldFileName).Data, t.GetField(fieldFilePath).Data, transferSize)
 
        var fp FilePath
-       err = fp.UnmarshalBinary(t.GetField(fieldFilePath).Data)
+       _, err = fp.Write(t.GetField(fieldFilePath).Data)
        if err != nil {
                return res, err
        }
@@ -1447,13 +1530,13 @@ func HandleDownloadFolder(cc *ClientConn, t *Transaction) (res []Transaction, er
 func HandleUploadFolder(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
        var fp FilePath
        if t.GetField(fieldFilePath).Data != nil {
-               if err = fp.UnmarshalBinary(t.GetField(fieldFilePath).Data); err != nil {
+               if _, err = fp.Write(t.GetField(fieldFilePath).Data); err != nil {
                        return res, err
                }
        }
 
        // Handle special cases for Upload and Drop Box folders
-       if !authorize(cc.Account.Access, accessUploadAnywhere) {
+       if !cc.Authorize(accessUploadAnywhere) {
                if !fp.IsUploadDir() && !fp.IsDropbox() {
                        res = append(res, cc.NewErrReply(t, fmt.Sprintf("Cannot accept upload of the folder \"%v\" because you are only allowed to upload to the \"Uploads\" folder.", string(t.GetField(fieldFileName).Data))))
                        return res, err
@@ -1480,7 +1563,7 @@ func HandleUploadFolder(cc *ClientConn, t *Transaction) (res []Transaction, err
 // Used only to resume download, currently has value 2"
 // 108 File transfer size      "Optional used if download is not resumed"
 func HandleUploadFile(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
-       if !authorize(cc.Account.Access, accessUploadFile) {
+       if !cc.Authorize(accessUploadFile) {
                res = append(res, cc.NewErrReply(t, "You are not allowed to upload files."))
                return res, err
        }
@@ -1492,13 +1575,13 @@ func HandleUploadFile(cc *ClientConn, t *Transaction) (res []Transaction, err er
 
        var fp FilePath
        if filePath != nil {
-               if err = fp.UnmarshalBinary(filePath); err != nil {
+               if _, err = fp.Write(filePath); err != nil {
                        return res, err
                }
        }
 
        // Handle special cases for Upload and Drop Box folders
-       if !authorize(cc.Account.Access, accessUploadAnywhere) {
+       if !cc.Authorize(accessUploadAnywhere) {
                if !fp.IsUploadDir() && !fp.IsDropbox() {
                        res = append(res, cc.NewErrReply(t, fmt.Sprintf("Cannot accept upload of the file \"%v\" because you are only allowed to upload to the \"Uploads\" folder.", string(fileName))))
                        return res, err
@@ -1545,27 +1628,26 @@ func HandleUploadFile(cc *ClientConn, t *Transaction) (res []Transaction, err er
 }
 
 func HandleSetClientUserInfo(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
-       var icon []byte
        if len(t.GetField(fieldUserIconID).Data) == 4 {
-               icon = t.GetField(fieldUserIconID).Data[2:]
+               cc.Icon = t.GetField(fieldUserIconID).Data[2:]
        } else {
-               icon = t.GetField(fieldUserIconID).Data
+               cc.Icon = t.GetField(fieldUserIconID).Data
+       }
+       if cc.Authorize(accessAnyName) {
+               cc.UserName = t.GetField(fieldUserName).Data
        }
-       *cc.Icon = icon
-       cc.UserName = t.GetField(fieldUserName).Data
 
        // the options field is only passed by the client versions > 1.2.3.
        options := t.GetField(fieldOptions).Data
-
        if options != nil {
                optBitmap := big.NewInt(int64(binary.BigEndian.Uint16(options)))
-               flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(*cc.Flags)))
+               flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(cc.Flags)))
 
                flagBitmap.SetBit(flagBitmap, userFlagRefusePM, optBitmap.Bit(refusePM))
-               binary.BigEndian.PutUint16(*cc.Flags, uint16(flagBitmap.Int64()))
+               binary.BigEndian.PutUint16(cc.Flags, uint16(flagBitmap.Int64()))
 
                flagBitmap.SetBit(flagBitmap, userFLagRefusePChat, optBitmap.Bit(refuseChat))
-               binary.BigEndian.PutUint16(*cc.Flags, uint16(flagBitmap.Int64()))
+               binary.BigEndian.PutUint16(cc.Flags, uint16(flagBitmap.Int64()))
 
                // Check auto response
                if optBitmap.Bit(autoResponse) == 1 {
@@ -1575,14 +1657,16 @@ func HandleSetClientUserInfo(cc *ClientConn, t *Transaction) (res []Transaction,
                }
        }
 
-       // Notify all clients of updated user info
-       cc.sendAll(
-               tranNotifyChangeUser,
-               NewField(fieldUserID, *cc.ID),
-               NewField(fieldUserIconID, *cc.Icon),
-               NewField(fieldUserFlags, *cc.Flags),
-               NewField(fieldUserName, cc.UserName),
-       )
+       for _, c := range sortedClients(cc.Server.Clients) {
+               res = append(res, *NewTransaction(
+                       tranNotifyChangeUser,
+                       c.ID,
+                       NewField(fieldUserID, *cc.ID),
+                       NewField(fieldUserIconID, cc.Icon),
+                       NewField(fieldUserFlags, cc.Flags),
+                       NewField(fieldUserName, cc.UserName),
+               ))
+       }
 
        return res, err
 }
@@ -1608,18 +1692,18 @@ func HandleGetFileNameList(cc *ClientConn, t *Transaction) (res []Transaction, e
 
        var fp FilePath
        if t.GetField(fieldFilePath).Data != nil {
-               if err = fp.UnmarshalBinary(t.GetField(fieldFilePath).Data); err != nil {
+               if _, err = fp.Write(t.GetField(fieldFilePath).Data); err != nil {
                        return res, err
                }
        }
 
        // Handle special case for drop box folders
-       if fp.IsDropbox() && !authorize(cc.Account.Access, accessViewDropBoxes) {
+       if fp.IsDropbox() && !cc.Authorize(accessViewDropBoxes) {
                res = append(res, cc.NewErrReply(t, "You are not allowed to view drop boxes."))
                return res, err
        }
 
-       fileNames, err := getFileNameList(fullPath)
+       fileNames, err := getFileNameList(fullPath, cc.Server.Config.IgnoreFiles)
        if err != nil {
                return res, err
        }
@@ -1643,7 +1727,7 @@ func HandleGetFileNameList(cc *ClientConn, t *Transaction) (res []Transaction, e
 
 // HandleInviteNewChat invites users to new private chat
 func HandleInviteNewChat(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
-       if !authorize(cc.Account.Access, accessOpenChat) {
+       if !cc.Authorize(accessOpenChat) {
                res = append(res, cc.NewErrReply(t, "You are not allowed to request private chat."))
                return res, err
        }
@@ -1652,23 +1736,41 @@ func HandleInviteNewChat(cc *ClientConn, t *Transaction) (res []Transaction, err
        targetID := t.GetField(fieldUserID).Data
        newChatID := cc.Server.NewPrivateChat(cc)
 
-       res = append(res,
-               *NewTransaction(
-                       tranInviteToChat,
-                       &targetID,
-                       NewField(fieldChatID, newChatID),
-                       NewField(fieldUserName, cc.UserName),
-                       NewField(fieldUserID, *cc.ID),
-               ),
-       )
+       // Check if target user has "Refuse private chat" flag
+       binary.BigEndian.Uint16(targetID)
+       targetClient := cc.Server.Clients[binary.BigEndian.Uint16(targetID)]
+
+       flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(targetClient.Flags)))
+       if flagBitmap.Bit(userFLagRefusePChat) == 1 {
+               res = append(res,
+                       *NewTransaction(
+                               tranServerMsg,
+                               cc.ID,
+                               NewField(fieldData, []byte(string(targetClient.UserName)+" does not accept private chats.")),
+                               NewField(fieldUserName, targetClient.UserName),
+                               NewField(fieldUserID, *targetClient.ID),
+                               NewField(fieldOptions, []byte{0, 2}),
+                       ),
+               )
+       } else {
+               res = append(res,
+                       *NewTransaction(
+                               tranInviteToChat,
+                               &targetID,
+                               NewField(fieldChatID, newChatID),
+                               NewField(fieldUserName, cc.UserName),
+                               NewField(fieldUserID, *cc.ID),
+                       ),
+               )
+       }
 
        res = append(res,
                cc.NewReply(t,
                        NewField(fieldChatID, newChatID),
                        NewField(fieldUserName, cc.UserName),
                        NewField(fieldUserID, *cc.ID),
-                       NewField(fieldUserIconID, *cc.Icon),
-                       NewField(fieldUserFlags, *cc.Flags),
+                       NewField(fieldUserIconID, cc.Icon),
+                       NewField(fieldUserFlags, cc.Flags),
                ),
        )
 
@@ -1676,7 +1778,7 @@ func HandleInviteNewChat(cc *ClientConn, t *Transaction) (res []Transaction, err
 }
 
 func HandleInviteToChat(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
-       if !authorize(cc.Account.Access, accessOpenChat) {
+       if !cc.Authorize(accessOpenChat) {
                res = append(res, cc.NewErrReply(t, "You are not allowed to request private chat."))
                return res, err
        }
@@ -1700,8 +1802,8 @@ func HandleInviteToChat(cc *ClientConn, t *Transaction) (res []Transaction, err
                        NewField(fieldChatID, chatID),
                        NewField(fieldUserName, cc.UserName),
                        NewField(fieldUserID, *cc.ID),
-                       NewField(fieldUserIconID, *cc.Icon),
-                       NewField(fieldUserFlags, *cc.Flags),
+                       NewField(fieldUserIconID, cc.Icon),
+                       NewField(fieldUserFlags, cc.Flags),
                ),
        )
 
@@ -1750,8 +1852,8 @@ func HandleJoinChat(cc *ClientConn, t *Transaction) (res []Transaction, err erro
                                NewField(fieldChatID, chatID),
                                NewField(fieldUserName, cc.UserName),
                                NewField(fieldUserID, *cc.ID),
-                               NewField(fieldUserIconID, *cc.Icon),
-                               NewField(fieldUserFlags, *cc.Flags),
+                               NewField(fieldUserIconID, cc.Icon),
+                               NewField(fieldUserFlags, cc.Flags),
                        ),
                )
        }
@@ -1762,8 +1864,8 @@ func HandleJoinChat(cc *ClientConn, t *Transaction) (res []Transaction, err erro
        for _, c := range sortedClients(privChat.ClientConn) {
                user := User{
                        ID:    *c.ID,
-                       Icon:  *c.Icon,
-                       Flags: *c.Flags,
+                       Icon:  c.Icon,
+                       Flags: c.Flags,
                        Name:  string(c.UserName),
                }
 
@@ -1776,7 +1878,8 @@ func HandleJoinChat(cc *ClientConn, t *Transaction) (res []Transaction, err erro
 
 // HandleLeaveChat is sent from a v1.8+ Hotline client when the user exits a private chat
 // Fields used in the request:
-//     * 114   fieldChatID
+//   - 114     fieldChatID
+//
 // Reply is not expected.
 func HandleLeaveChat(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
        chatID := t.GetField(fieldChatID).Data
@@ -1807,7 +1910,7 @@ func HandleLeaveChat(cc *ClientConn, t *Transaction) (res []Transaction, err err
 // HandleSetChatSubject is sent from a v1.8+ Hotline client when the user sets a private chat subject
 // Fields used in the request:
 // * 114       Chat ID
-// * 115       Chat subject    Chat subject string
+// * 115       Chat subject
 // Reply is not expected.
 func HandleSetChatSubject(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
        chatID := t.GetField(fieldChatID).Data
@@ -1830,7 +1933,7 @@ func HandleSetChatSubject(cc *ClientConn, t *Transaction) (res []Transaction, er
        return res, err
 }
 
-// HandleMakeAlias makes a filer alias using the specified path.
+// HandleMakeAlias makes a file alias using the specified path.
 // Fields used in the request:
 // 201 File name
 // 202 File path
@@ -1839,7 +1942,7 @@ func HandleSetChatSubject(cc *ClientConn, t *Transaction) (res []Transaction, er
 // Fields used in the reply:
 // None
 func HandleMakeAlias(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
-       if !authorize(cc.Account.Access, accessMakeAlias) {
+       if !cc.Authorize(accessMakeAlias) {
                res = append(res, cc.NewErrReply(t, "You are not allowed to make aliases."))
                return res, err
        }
@@ -1868,6 +1971,12 @@ func HandleMakeAlias(cc *ClientConn, t *Transaction) (res []Transaction, err err
        return res, err
 }
 
+// HandleDownloadBanner handles requests for a new banner from the server
+// Fields used in the request:
+// None
+// Fields used in the reply:
+// 107 fieldRefNum                     Used later for transfer
+// 108 fieldTransferSize       Size of data to be downloaded
 func HandleDownloadBanner(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
        fi, err := cc.Server.FS.Stat(filepath.Join(cc.Server.ConfigDir, cc.Server.Config.BannerFile))
        if err != nil {