]> git.r.bdr.sh - rbdr/mobius/blobdiff - hotline/transaction_handlers.go
Use fixed size array types in Transaction fields
[rbdr/mobius] / hotline / transaction_handlers.go
index 137b2a0583ce2288f180cc07f37a572b1654360e..cff19d52d4630e2e8f67588c7330eb43820897a2 100644 (file)
@@ -1,11 +1,13 @@
 package hotline
 
 import (
+       "bufio"
        "bytes"
        "encoding/binary"
        "errors"
        "fmt"
        "gopkg.in/yaml.v3"
+       "io"
        "math/big"
        "os"
        "path"
@@ -305,7 +307,7 @@ func HandleChatSend(cc *ClientConn, t *Transaction) (res []Transaction, err erro
 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
+               return res, errors.New("user is not allowed to send private messages")
        }
 
        msg := t.GetField(FieldData)
@@ -326,7 +328,10 @@ func HandleSendInstantMsg(cc *ClientConn, t *Transaction) (res []Transaction, er
                reply.Fields = append(reply.Fields, NewField(FieldQuotingMsg, t.GetField(FieldQuotingMsg).Data))
        }
 
-       id, _ := byteToInt(ID.Data)
+       id, err := byteToInt(ID.Data)
+       if err != nil {
+               return res, errors.New("invalid client ID")
+       }
        otherClient, ok := cc.Server.Clients[uint16(id)]
        if !ok {
                return res, errors.New("invalid client ID")
@@ -334,7 +339,7 @@ func HandleSendInstantMsg(cc *ClientConn, t *Transaction) (res []Transaction, er
 
        // Check if target user has "Refuse private messages" flag
        flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(otherClient.Flags)))
-       if flagBitmap.Bit(userFLagRefusePChat) == 1 {
+       if flagBitmap.Bit(UserFlagRefusePM) == 1 {
                res = append(res,
                        *NewTransaction(
                                TranServerMsg,
@@ -382,24 +387,39 @@ func HandleGetFileInfo(cc *ClientConn, t *Transaction) (res []Transaction, err e
                return res, err
        }
 
-       res = append(res, cc.NewReply(t,
-               NewField(FieldFileName, []byte(fw.name)),
+       encodedName, err := txtEncoder.String(fw.name)
+       if err != nil {
+               return res, fmt.Errorf("invalid filepath encoding: %w", err)
+       }
+
+       fields := []Field{
+               NewField(FieldFileName, []byte(encodedName)),
                NewField(FieldFileTypeString, fw.ffo.FlatFileInformationFork.friendlyType()),
                NewField(FieldFileCreatorString, fw.ffo.FlatFileInformationFork.friendlyCreator()),
-               NewField(FieldFileComment, fw.ffo.FlatFileInformationFork.Comment),
                NewField(FieldFileType, fw.ffo.FlatFileInformationFork.TypeSignature),
                NewField(FieldFileCreateDate, fw.ffo.FlatFileInformationFork.CreateDate),
                NewField(FieldFileModifyDate, fw.ffo.FlatFileInformationFork.ModifyDate),
-               NewField(FieldFileSize, fw.totalSize()),
-       ))
+       }
+
+       // Include the optional FileComment field if there is a comment.
+       if len(fw.ffo.FlatFileInformationFork.Comment) != 0 {
+               fields = append(fields, NewField(FieldFileComment, fw.ffo.FlatFileInformationFork.Comment))
+       }
+
+       // Include the FileSize field for files.
+       if !bytes.Equal(fw.ffo.FlatFileInformationFork.TypeSignature, []byte{0x66, 0x6c, 0x64, 0x72}) {
+               fields = append(fields, NewField(FieldFileSize, fw.totalSize()))
+       }
+
+       res = append(res, cc.NewReply(t, fields...))
        return res, err
 }
 
-// HandleSetFileInfo updates a file or folder name and/or comment from the Get Info window
+// HandleSetFileInfo updates a file or folder Name and/or comment from the Get Info window
 // Fields used in the request:
-// * 201       File name
+// * 201       File Name
 // * 202       File path       Optional
-// * 211       File new name   Optional
+// * 211       File new Name   Optional
 // * 210       File comment    Optional
 // Fields used in the reply:   None
 func HandleSetFileInfo(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
@@ -441,7 +461,7 @@ func HandleSetFileInfo(cc *ClientConn, t *Transaction) (res []Transaction, err e
                if err != nil {
                        return res, err
                }
-               _, err = w.Write(hlFile.ffo.FlatFileInformationFork.MarshalBinary())
+               _, err = io.Copy(w, &hlFile.ffo.FlatFileInformationFork)
                if err != nil {
                        return res, err
                }
@@ -475,7 +495,11 @@ func HandleSetFileInfo(cc *ClientConn, t *Transaction) (res []Transaction, err e
                        if err != nil {
                                return nil, err
                        }
-                       hlFile.name = string(fileNewName)
+                       hlFile.name, err = txtDecoder.String(string(fileNewName))
+                       if err != nil {
+                               return res, fmt.Errorf("invalid filepath encoding: %w", err)
+                       }
+
                        err = hlFile.move(fileDir)
                        if os.IsNotExist(err) {
                                res = append(res, cc.NewErrReply(t, "Cannot rename file "+string(fileName)+" because it does not exist or cannot be found."))
@@ -493,7 +517,7 @@ func HandleSetFileInfo(cc *ClientConn, t *Transaction) (res []Transaction, err e
 
 // HandleDeleteFile deletes a file or folder
 // Fields used in the request:
-// * 201       File name
+// * 201       File Name
 // * 202       File path
 // Fields used in the reply: none
 func HandleDeleteFile(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
@@ -551,7 +575,7 @@ func HandleMoveFile(cc *ClientConn, t *Transaction) (res []Transaction, err erro
                return res, err
        }
 
-       cc.logger.Infow("Move file", "src", filePath+"/"+fileName, "dst", fileNewPath+"/"+fileName)
+       cc.logger.Info("Move file", "src", filePath+"/"+fileName, "dst", fileNewPath+"/"+fileName)
 
        hlFile, err := newFileWrapper(cc.Server.FS, filePath, 0)
        if err != nil {
@@ -563,9 +587,6 @@ func HandleMoveFile(cc *ClientConn, t *Transaction) (res []Transaction, err erro
                res = append(res, cc.NewErrReply(t, "Cannot delete file "+fileName+" because it does not exist or cannot be found."))
                return res, err
        }
-       if err != nil {
-               return res, err
-       }
        switch mode := fi.Mode(); {
        case mode.IsDir():
                if !cc.Authorize(accessMoveFolder) {
@@ -611,16 +632,18 @@ func HandleNewFolder(cc *ClientConn, t *Transaction) (res []Transaction, err err
                }
        }
        newFolderPath := path.Join(cc.Server.Config.FileRoot, subPath, folderName)
+       newFolderPath, err = txtDecoder.String(newFolderPath)
+       if err != nil {
+               return res, fmt.Errorf("invalid filepath encoding: %w", err)
+       }
 
-       // TODO: check path and folder name lengths
+       // TODO: check path and folder Name lengths
 
        if _, err := cc.Server.FS.Stat(newFolderPath); !os.IsNotExist(err) {
-               msg := fmt.Sprintf("Cannot create folder \"%s\" because there is already a file or folder with that name.", folderName)
+               msg := fmt.Sprintf("Cannot create folder \"%s\" because there is already a file or folder with that Name.", folderName)
                return []Transaction{cc.NewErrReply(t, msg)}, nil
        }
 
-       // TODO: check for disallowed characters to maintain compatibility for original client
-
        if err := cc.Server.FS.Mkdir(newFolderPath, 0777); err != nil {
                msg := fmt.Sprintf("Cannot create folder \"%s\" because an error occurred.", folderName)
                return []Transaction{cc.NewErrReply(t, msg)}, nil
@@ -636,12 +659,15 @@ func HandleSetUser(cc *ClientConn, t *Transaction) (res []Transaction, err error
                return res, err
        }
 
-       login := DecodeUserString(t.GetField(FieldUserLogin).Data)
+       login := string(encodeString(t.GetField(FieldUserLogin).Data))
        userName := string(t.GetField(FieldUserName).Data)
 
        newAccessLvl := t.GetField(FieldUserAccess).Data
 
        account := cc.Server.Accounts[login]
+       if account == nil {
+               return append(res, cc.NewErrReply(t, "Account not found.")), nil
+       }
        account.Name = userName
        copy(account.Access[:], newAccessLvl)
 
@@ -650,7 +676,8 @@ func HandleSetUser(cc *ClientConn, t *Transaction) (res []Transaction, err error
        if t.GetField(FieldUserPassword).Data == nil {
                account.Password = hashAndSalt([]byte(""))
        }
-       if len(t.GetField(FieldUserPassword).Data) > 1 {
+
+       if !bytes.Equal([]byte{0}, t.GetField(FieldUserPassword).Data) {
                account.Password = hashAndSalt(t.GetField(FieldUserPassword).Data)
        }
 
@@ -671,9 +698,9 @@ func HandleSetUser(cc *ClientConn, t *Transaction) (res []Transaction, err error
 
                        flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(c.Flags)))
                        if c.Authorize(accessDisconUser) {
-                               flagBitmap.SetBit(flagBitmap, userFlagAdmin, 1)
+                               flagBitmap.SetBit(flagBitmap, UserFlagAdmin, 1)
                        } else {
-                               flagBitmap.SetBit(flagBitmap, userFlagAdmin, 0)
+                               flagBitmap.SetBit(flagBitmap, UserFlagAdmin, 0)
                        }
                        binary.BigEndian.PutUint16(c.Flags, uint16(flagBitmap.Int64()))
 
@@ -707,7 +734,7 @@ func HandleGetUser(cc *ClientConn, t *Transaction) (res []Transaction, err error
 
        res = append(res, cc.NewReply(t,
                NewField(FieldUserName, []byte(account.Name)),
-               NewField(FieldUserLogin, negateString(t.GetField(FieldUserLogin).Data)),
+               NewField(FieldUserLogin, encodeString(t.GetField(FieldUserLogin).Data)),
                NewField(FieldUserPassword, []byte(account.Password)),
                NewField(FieldUserAccess, account.Access[:]),
        ))
@@ -722,13 +749,13 @@ func HandleListUsers(cc *ClientConn, t *Transaction) (res []Transaction, err err
 
        var userFields []Field
        for _, acc := range cc.Server.Accounts {
-               b := make([]byte, 0, 100)
-               n, err := acc.Read(b)
+               accCopy := *acc
+               b, err := io.ReadAll(&accCopy)
                if err != nil {
                        return res, err
                }
 
-               userFields = append(userFields, NewField(FieldData, b[:n]))
+               userFields = append(userFields, NewField(FieldData, b))
        }
 
        res = append(res, cc.NewReply(t, userFields...))
@@ -746,41 +773,80 @@ func HandleListUsers(cc *ClientConn, t *Transaction) (res []Transaction, err err
 // performed.  This seems to be the only place in the Hotline protocol where a data field contains another data field.
 func HandleUpdateUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
        for _, field := range t.Fields {
-               subFields, err := ReadFields(field.Data[0:2], field.Data[2:])
-               if err != nil {
-                       return res, err
+
+               var subFields []Field
+
+               // Create a new scanner for parsing incoming bytes into transaction tokens
+               scanner := bufio.NewScanner(bytes.NewReader(field.Data[2:]))
+               scanner.Split(fieldScanner)
+
+               for i := 0; i < int(binary.BigEndian.Uint16(field.Data[0:2])); i++ {
+                       scanner.Scan()
+
+                       var field Field
+                       if _, err := field.Write(scanner.Bytes()); err != nil {
+                               return res, fmt.Errorf("error reading field: %w", err)
+                       }
+                       subFields = append(subFields, field)
                }
 
+               // If there's only one subfield, that indicates this is a delete operation for the login in FieldData
                if len(subFields) == 1 {
-                       login := DecodeUserString(getField(FieldData, &subFields).Data)
-                       cc.logger.Infow("DeleteUser", "login", login)
-
                        if !cc.Authorize(accessDeleteUser) {
                                res = append(res, cc.NewErrReply(t, "You are not allowed to delete accounts."))
                                return res, err
                        }
 
+                       login := string(encodeString(getField(FieldData, &subFields).Data))
+                       cc.logger.Info("DeleteUser", "login", login)
+
                        if err := cc.Server.DeleteUser(login); err != nil {
                                return res, err
                        }
                        continue
                }
 
-               login := DecodeUserString(getField(FieldUserLogin, &subFields).Data)
+               // login of the account to update
+               var accountToUpdate, loginToRename string
 
-               // check if the login dataFile; if so, we know we are updating an existing user
-               if acc, ok := cc.Server.Accounts[login]; ok {
-                       cc.logger.Infow("UpdateUser", "login", login)
+               // If FieldData is included, this is a rename operation where FieldData contains the login of the existing
+               // account and FieldUserLogin contains the new login.
+               if getField(FieldData, &subFields) != nil {
+                       loginToRename = string(encodeString(getField(FieldData, &subFields).Data))
+               }
+               userLogin := string(encodeString(getField(FieldUserLogin, &subFields).Data))
+               if loginToRename != "" {
+                       accountToUpdate = loginToRename
+               } else {
+                       accountToUpdate = userLogin
+               }
 
-                       // account dataFile, so this is an update action
+               // Check if accountToUpdate has an existing account.  If so, we know we are updating an existing user.
+               if acc, ok := cc.Server.Accounts[accountToUpdate]; ok {
+                       if loginToRename != "" {
+                               cc.logger.Info("RenameUser", "prevLogin", accountToUpdate, "newLogin", userLogin)
+                       } else {
+                               cc.logger.Info("UpdateUser", "login", accountToUpdate)
+                       }
+
+                       // account exists, so this is an update action
                        if !cc.Authorize(accessModifyUser) {
                                res = append(res, cc.NewErrReply(t, "You are not allowed to modify accounts."))
-                               return res, err
+                               return res, nil
                        }
 
+                       // This part is a bit tricky. There are three possibilities:
+                       // 1) The transaction is intended to update the password.
+                       //        In this case, FieldUserPassword is sent with the new password.
+                       // 2) The transaction is intended to remove the password.
+                       //    In this case, FieldUserPassword is not sent.
+                       // 3) The transaction updates the users access bits, but not the password.
+                       //    In this case, FieldUserPassword is sent with zero as the only byte.
                        if getField(FieldUserPassword, &subFields) != nil {
                                newPass := getField(FieldUserPassword, &subFields).Data
-                               acc.Password = hashAndSalt(newPass)
+                               if !bytes.Equal([]byte{0}, newPass) {
+                                       acc.Password = hashAndSalt(newPass)
+                               }
                        } else {
                                acc.Password = hashAndSalt([]byte(""))
                        }
@@ -790,8 +856,8 @@ func HandleUpdateUser(cc *ClientConn, t *Transaction) (res []Transaction, err er
                        }
 
                        err = cc.Server.UpdateUser(
-                               DecodeUserString(getField(FieldData, &subFields).Data),
-                               DecodeUserString(getField(FieldUserLogin, &subFields).Data),
+                               string(encodeString(getField(FieldData, &subFields).Data)),
+                               string(encodeString(getField(FieldUserLogin, &subFields).Data)),
                                string(getField(FieldUserName, &subFields).Data),
                                acc.Password,
                                acc.Access,
@@ -800,13 +866,13 @@ func HandleUpdateUser(cc *ClientConn, t *Transaction) (res []Transaction, err er
                                return res, err
                        }
                } else {
-                       cc.logger.Infow("CreateUser", "login", login)
-
                        if !cc.Authorize(accessCreateUser) {
                                res = append(res, cc.NewErrReply(t, "You are not allowed to create new accounts."))
-                               return res, err
+                               return res, nil
                        }
 
+                       cc.logger.Info("CreateUser", "login", userLogin)
+
                        newAccess := accessBitmap{}
                        copy(newAccess[:], getField(FieldUserAccess, &subFields).Data)
 
@@ -814,14 +880,14 @@ func HandleUpdateUser(cc *ClientConn, t *Transaction) (res []Transaction, err er
                        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
+                                               return append(res, cc.NewErrReply(t, "Cannot create account with more access than yourself.")), nil
                                        }
                                }
                        }
 
-                       err := cc.Server.NewUser(login, string(getField(FieldUserName, &subFields).Data), string(getField(FieldUserPassword, &subFields).Data), newAccess)
+                       err = cc.Server.NewUser(userLogin, string(getField(FieldUserName, &subFields).Data), string(getField(FieldUserPassword, &subFields).Data), newAccess)
                        if err != nil {
-                               return []Transaction{}, err
+                               return append(res, cc.NewErrReply(t, "Cannot create account because there is already an account with that login.")), nil
                        }
                }
        }
@@ -837,7 +903,7 @@ func HandleNewUser(cc *ClientConn, t *Transaction) (res []Transaction, err error
                return res, err
        }
 
-       login := DecodeUserString(t.GetField(FieldUserLogin).Data)
+       login := string(encodeString(t.GetField(FieldUserLogin).Data))
 
        // If the account already dataFile, reply with an error
        if _, ok := cc.Server.Accounts[login]; ok {
@@ -859,7 +925,8 @@ func HandleNewUser(cc *ClientConn, t *Transaction) (res []Transaction, err error
        }
 
        if err := cc.Server.NewUser(login, string(t.GetField(FieldUserName).Data), string(t.GetField(FieldUserPassword).Data), newAccess); err != nil {
-               return []Transaction{}, err
+               res = append(res, cc.NewErrReply(t, "Cannot create account because there is already an account with that login."))
+               return res, err
        }
 
        res = append(res, cc.NewReply(t))
@@ -869,11 +936,10 @@ func HandleNewUser(cc *ClientConn, t *Transaction) (res []Transaction, err error
 func HandleDeleteUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
        if !cc.Authorize(accessDeleteUser) {
                res = append(res, cc.NewErrReply(t, "You are not allowed to delete accounts."))
-               return res, err
+               return res, nil
        }
 
-       // TODO: Handle case where account doesn't exist; e.g. delete race condition
-       login := DecodeUserString(t.GetField(FieldUserLogin).Data)
+       login := string(encodeString(t.GetField(FieldUserLogin).Data))
 
        if err := cc.Server.DeleteUser(login); err != nil {
                return res, err
@@ -906,7 +972,7 @@ func HandleUserBroadcast(cc *ClientConn, t *Transaction) (res []Transaction, err
 // 103 User ID
 //
 // Fields used in the reply:
-// 102 User name
+// 102 User Name
 // 101 Data            User info text string
 func HandleGetClientInfoText(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
        if !cc.Authorize(accessGetClientInfo) {
@@ -929,9 +995,7 @@ func HandleGetClientInfoText(cc *ClientConn, t *Transaction) (res []Transaction,
 }
 
 func HandleGetUserNameList(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
-       res = append(res, cc.NewReply(t, cc.Server.connectedUsers()...))
-
-       return res, err
+       return []Transaction{cc.NewReply(t, cc.Server.connectedUsers()...)}, nil
 }
 
 func HandleTranAgreed(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
@@ -945,8 +1009,8 @@ func HandleTranAgreed(cc *ClientConn, t *Transaction) (res []Transaction, err er
 
        cc.Icon = t.GetField(FieldUserIconID).Data
 
-       cc.logger = cc.logger.With("name", string(cc.UserName))
-       cc.logger.Infow("Login successful", "clientVersion", fmt.Sprintf("%v", func() int { i, _ := byteToInt(cc.Version); return i }()))
+       cc.logger = cc.logger.With("Name", string(cc.UserName))
+       cc.logger.Info("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)))
@@ -954,19 +1018,19 @@ func HandleTranAgreed(cc *ClientConn, t *Transaction) (res []Transaction, err er
        flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(cc.Flags)))
 
        // Check refuse private PM option
-       if optBitmap.Bit(refusePM) == 1 {
-               flagBitmap.SetBit(flagBitmap, userFlagRefusePM, 1)
+       if optBitmap.Bit(UserOptRefusePM) == 1 {
+               flagBitmap.SetBit(flagBitmap, UserFlagRefusePM, 1)
                binary.BigEndian.PutUint16(cc.Flags, uint16(flagBitmap.Int64()))
        }
 
        // Check refuse private chat option
-       if optBitmap.Bit(refuseChat) == 1 {
-               flagBitmap.SetBit(flagBitmap, userFLagRefusePChat, 1)
+       if optBitmap.Bit(UserOptRefuseChat) == 1 {
+               flagBitmap.SetBit(flagBitmap, UserFlagRefusePChat, 1)
                binary.BigEndian.PutUint16(cc.Flags, uint16(flagBitmap.Int64()))
        }
 
        // Check auto response
-       if optBitmap.Bit(autoResponse) == 1 {
+       if optBitmap.Bit(UserOptAutoResponse) == 1 {
                cc.AutoReply = t.GetField(FieldAutomaticResponse).Data
        } else {
                cc.AutoReply = []byte{}
@@ -1015,7 +1079,7 @@ func HandleTranOldPostNews(cc *ClientConn, t *Transaction) (res []Transaction, e
        }
 
        newsPost := fmt.Sprintf(newsTemplate+"\r", cc.UserName, time.Now().Format(newsDateTemplate), t.GetField(FieldData).Data)
-       newsPost = strings.Replace(newsPost, "\n", "\r", -1)
+       newsPost = strings.ReplaceAll(newsPost, "\n", "\r")
 
        // update news in memory
        cc.Server.FlatNews = append([]byte(newsPost), cc.Server.FlatNews...)
@@ -1055,7 +1119,7 @@ func HandleDisconnectUser(cc *ClientConn, t *Transaction) (res []Transaction, er
                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))
+                       cc.logger.Info("Disconnect & temporarily ban " + string(clientConn.UserName))
 
                        res = append(res, *NewTransaction(
                                TranServerMsg,
@@ -1066,10 +1130,9 @@ func HandleDisconnectUser(cc *ClientConn, t *Transaction) (res []Transaction, er
 
                        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))
+                       cc.logger.Info("Disconnect & ban " + string(clientConn.UserName))
 
                        res = append(res, *NewTransaction(
                                TranServerMsg,
@@ -1079,7 +1142,11 @@ func HandleDisconnectUser(cc *ClientConn, t *Transaction) (res []Transaction, er
                        ))
 
                        cc.Server.banList[strings.Split(clientConn.RemoteAddr, ":")[0]] = nil
-                       cc.Server.writeBanList()
+               }
+
+               err := cc.Server.writeBanList()
+               if err != nil {
+                       return res, err
                }
        }
 
@@ -1139,7 +1206,7 @@ func HandleNewNewsCat(cc *ClientConn, t *Transaction) (res []Transaction, err er
        cats := cc.Server.GetNewsCatByPath(pathStrs)
        cats[name] = NewsCategoryListData15{
                Name:     name,
-               Type:     []byte{0, 3},
+               Type:     [2]byte{0, 3},
                Articles: map[uint32]*NewsArtData{},
                SubCats:  make(map[string]NewsCategoryListData15),
        }
@@ -1152,7 +1219,7 @@ func HandleNewNewsCat(cc *ClientConn, t *Transaction) (res []Transaction, err er
 }
 
 // Fields used in the request:
-// 322 News category name
+// 322 News category Name
 // 325 News path
 func HandleNewNewsFldr(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
        if !cc.Authorize(accessNewsCreateFldr) {
@@ -1163,12 +1230,10 @@ func HandleNewNewsFldr(cc *ClientConn, t *Transaction) (res []Transaction, err e
        name := string(t.GetField(FieldFileName).Data)
        pathStrs := ReadNewsPath(t.GetField(FieldNewsPath).Data)
 
-       cc.logger.Infof("Creating new news folder %s", name)
-
        cats := cc.Server.GetNewsCatByPath(pathStrs)
        cats[name] = NewsCategoryListData15{
                Name:     name,
-               Type:     []byte{0, 2},
+               Type:     [2]byte{0, 2},
                Articles: map[uint32]*NewsArtData{},
                SubCats:  make(map[string]NewsCategoryListData15),
        }
@@ -1189,8 +1254,9 @@ func HandleNewNewsFldr(cc *ClientConn, t *Transaction) (res []Transaction, err e
 func HandleGetNewsArtNameList(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
        if !cc.Authorize(accessNewsReadArt) {
                res = append(res, cc.NewErrReply(t, "You are not allowed to read news."))
-               return res, err
+               return res, nil
        }
+
        pathStrs := ReadNewsPath(t.GetField(FieldNewsPath).Data)
 
        var cat NewsCategoryListData15
@@ -1203,8 +1269,13 @@ func HandleGetNewsArtNameList(cc *ClientConn, t *Transaction) (res []Transaction
 
        nald := cat.GetNewsArtListData()
 
-       res = append(res, cc.NewReply(t, NewField(FieldNewsArtListData, nald.Payload())))
-       return res, err
+       b, err := io.ReadAll(&nald)
+       if err != nil {
+               return res, fmt.Errorf("error loading news articles: %w", err)
+       }
+
+       res = append(res, cc.NewReply(t, NewField(FieldNewsArtListData, b)))
+       return res, nil
 }
 
 // HandleGetNewsArtData requests information about the specific news article.
@@ -1255,11 +1326,11 @@ func HandleGetNewsArtData(cc *ClientConn, t *Transaction) (res []Transaction, er
        res = append(res, cc.NewReply(t,
                NewField(FieldNewsArtTitle, []byte(art.Title)),
                NewField(FieldNewsArtPoster, []byte(art.Poster)),
-               NewField(FieldNewsArtDate, art.Date),
-               NewField(FieldNewsArtPrevArt, art.PrevArt),
-               NewField(FieldNewsArtNextArt, art.NextArt),
-               NewField(FieldNewsArtParentArt, art.ParentArt),
-               NewField(FieldNewsArt1stChildArt, art.FirstChildArt),
+               NewField(FieldNewsArtDate, art.Date[:]),
+               NewField(FieldNewsArtPrevArt, art.PrevArt[:]),
+               NewField(FieldNewsArtNextArt, art.NextArt[:]),
+               NewField(FieldNewsArtParentArt, art.ParentArt[:]),
+               NewField(FieldNewsArt1stChildArt, art.FirstChildArt[:]),
                NewField(FieldNewsArtDataFlav, []byte("text/plain")),
                NewField(FieldNewsArtData, []byte(art.Data)),
        ))
@@ -1282,7 +1353,7 @@ func HandleDelNewsItem(cc *ClientConn, t *Transaction) (res []Transaction, err e
                }
        }
 
-       if bytes.Equal(cats[delName].Type, []byte{0, 3}) {
+       if cats[delName].Type == [2]byte{0, 3} {
                if !cc.Authorize(accessNewsDeleteCat) {
                        return append(res, cc.NewErrReply(t, "You are not allowed to delete news categories.")), nil
                }
@@ -1361,14 +1432,17 @@ func HandlePostNewsArt(cc *ClientConn, t *Transaction) (res []Transaction, err e
        bs := make([]byte, 4)
        binary.BigEndian.PutUint32(bs, convertedArtID)
 
+       cc.Server.mux.Lock()
+       defer cc.Server.mux.Unlock()
+
        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:     bs,
-               FirstChildArt: []byte{0, 0, 0, 0},
+               PrevArt:       [4]byte{},
+               NextArt:       [4]byte{},
+               ParentArt:     [4]byte(bs),
+               FirstChildArt: [4]byte{},
                DataFlav:      []byte("text/plain"),
                Data:          string(t.GetField(FieldNewsArtData).Data),
        }
@@ -1384,10 +1458,10 @@ func HandlePostNewsArt(cc *ClientConn, t *Transaction) (res []Transaction, err e
                prevID := uint32(keys[len(keys)-1])
                nextID = prevID + 1
 
-               binary.BigEndian.PutUint32(newArt.PrevArt, prevID)
+               binary.BigEndian.PutUint32(newArt.PrevArt[:], prevID)
 
                // Set next article ID
-               binary.BigEndian.PutUint32(cat.Articles[prevID].NextArt, nextID)
+               binary.BigEndian.PutUint32(cat.Articles[prevID].NextArt[:], nextID)
        }
 
        // Update parent article with first child reply
@@ -1395,8 +1469,8 @@ func HandlePostNewsArt(cc *ClientConn, t *Transaction) (res []Transaction, err e
        if parentID != 0 {
                parentArt := cat.Articles[parentID]
 
-               if bytes.Equal(parentArt.FirstChildArt, []byte{0, 0, 0, 0}) {
-                       binary.BigEndian.PutUint32(parentArt.FirstChildArt, nextID)
+               if parentArt.FirstChildArt == [4]byte{0, 0, 0, 0} {
+                       binary.BigEndian.PutUint32(parentArt.FirstChildArt[:], nextID)
                }
        }
 
@@ -1524,7 +1598,7 @@ func HandleDownloadFolder(cc *ClientConn, t *Transaction) (res []Transaction, er
 
 // Upload all files from the local folder and its subfolders to the specified path on the server
 // Fields used in the request
-// 201 File name
+// 201 File Name
 // 202 File path
 // 108 transfer size   Total size of all items in the folder
 // 220 Folder item count
@@ -1559,7 +1633,7 @@ func HandleUploadFolder(cc *ClientConn, t *Transaction) (res []Transaction, err
 
 // HandleUploadFile
 // Fields used in the request:
-// 201 File name
+// 201 File Name
 // 202 File path
 // 204 File transfer options   "Optional
 // Used only to resume download, currently has value 2"
@@ -1595,7 +1669,7 @@ func HandleUploadFile(cc *ClientConn, t *Transaction) (res []Transaction, err er
        }
 
        if _, err := cc.Server.FS.Stat(fullFilePath); err == nil {
-               res = append(res, cc.NewErrReply(t, fmt.Sprintf("Cannot accept upload because there is already a file named \"%v\".  Try choosing a different name.", string(fileName))))
+               res = append(res, cc.NewErrReply(t, fmt.Sprintf("Cannot accept upload because there is already a file named \"%v\".  Try choosing a different Name.", string(fileName))))
                return res, err
        }
 
@@ -1644,14 +1718,14 @@ func HandleSetClientUserInfo(cc *ClientConn, t *Transaction) (res []Transaction,
                optBitmap := big.NewInt(int64(binary.BigEndian.Uint16(options)))
                flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(cc.Flags)))
 
-               flagBitmap.SetBit(flagBitmap, userFlagRefusePM, optBitmap.Bit(refusePM))
+               flagBitmap.SetBit(flagBitmap, UserFlagRefusePM, optBitmap.Bit(UserOptRefusePM))
                binary.BigEndian.PutUint16(cc.Flags, uint16(flagBitmap.Int64()))
 
-               flagBitmap.SetBit(flagBitmap, userFLagRefusePChat, optBitmap.Bit(refuseChat))
+               flagBitmap.SetBit(flagBitmap, UserFlagRefusePChat, optBitmap.Bit(UserOptRefuseChat))
                binary.BigEndian.PutUint16(cc.Flags, uint16(flagBitmap.Int64()))
 
                // Check auto response
-               if optBitmap.Bit(autoResponse) == 1 {
+               if optBitmap.Bit(UserOptAutoResponse) == 1 {
                        cc.AutoReply = t.GetField(FieldAutomaticResponse).Data
                } else {
                        cc.AutoReply = []byte{}
@@ -1688,13 +1762,13 @@ func HandleGetFileNameList(cc *ClientConn, t *Transaction) (res []Transaction, e
                nil,
        )
        if err != nil {
-               return res, err
+               return res, fmt.Errorf("error reading file path: %w", err)
        }
 
        var fp FilePath
        if t.GetField(FieldFilePath).Data != nil {
                if _, err = fp.Write(t.GetField(FieldFilePath).Data); err != nil {
-                       return res, err
+                       return res, fmt.Errorf("error writing file path: %w", err)
                }
        }
 
@@ -1706,7 +1780,7 @@ func HandleGetFileNameList(cc *ClientConn, t *Transaction) (res []Transaction, e
 
        fileNames, err := getFileNameList(fullPath, cc.Server.Config.IgnoreFiles)
        if err != nil {
-               return res, err
+               return res, fmt.Errorf("getFileNameList: %w", err)
        }
 
        res = append(res, cc.NewReply(t, fileNames...))
@@ -1742,7 +1816,7 @@ func HandleInviteNewChat(cc *ClientConn, t *Transaction) (res []Transaction, err
        targetClient := cc.Server.Clients[binary.BigEndian.Uint16(targetID)]
 
        flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(targetClient.Flags)))
-       if flagBitmap.Bit(userFLagRefusePChat) == 1 {
+       if flagBitmap.Bit(UserFlagRefusePChat) == 1 {
                res = append(res,
                        *NewTransaction(
                                TranServerMsg,
@@ -1836,7 +1910,7 @@ func HandleRejectChatInvite(cc *ClientConn, t *Transaction) (res []Transaction,
 // HandleJoinChat is sent from a v1.8+ Hotline client when the joins a private chat
 // Fields used in the reply:
 // * 115       Chat subject
-// * 300       User name with info (Optional)
+// * 300       User Name with info (Optional)
 // * 300       (more user names with info)
 func HandleJoinChat(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
        chatID := t.GetField(FieldChatID).Data
@@ -1863,14 +1937,17 @@ func HandleJoinChat(cc *ClientConn, t *Transaction) (res []Transaction, err erro
 
        replyFields := []Field{NewField(FieldChatSubject, []byte(privChat.Subject))}
        for _, c := range sortedClients(privChat.ClientConn) {
-               user := User{
+
+               b, err := io.ReadAll(&User{
                        ID:    *c.ID,
                        Icon:  c.Icon,
                        Flags: c.Flags,
                        Name:  string(c.UserName),
+               })
+               if err != nil {
+                       return res, nil
                }
-
-               replyFields = append(replyFields, NewField(FieldUsernameWithInfo, user.Payload()))
+               replyFields = append(replyFields, NewField(FieldUsernameWithInfo, b))
        }
 
        res = append(res, cc.NewReply(t, replyFields...))
@@ -1936,7 +2013,7 @@ func HandleSetChatSubject(cc *ClientConn, t *Transaction) (res []Transaction, er
 
 // HandleMakeAlias makes a file alias using the specified path.
 // Fields used in the request:
-// 201 File name
+// 201 File Name
 // 202 File path
 // 212 File new path   Destination path
 //
@@ -1961,7 +2038,7 @@ func HandleMakeAlias(cc *ClientConn, t *Transaction) (res []Transaction, err err
                return res, err
        }
 
-       cc.logger.Debugw("Make alias", "src", fullFilePath, "dst", fullNewFilePath)
+       cc.logger.Debug("Make alias", "src", fullFilePath, "dst", fullNewFilePath)
 
        if err := cc.Server.FS.Symlink(fullFilePath, fullNewFilePath); err != nil {
                res = append(res, cc.NewErrReply(t, "Error creating alias"))
@@ -1979,19 +2056,11 @@ func HandleMakeAlias(cc *ClientConn, t *Transaction) (res []Transaction, err err
 // 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 {
-               return res, err
-       }
-
        ft := cc.newFileTransfer(bannerDownload, []byte{}, []byte{}, make([]byte, 4))
+       binary.BigEndian.PutUint32(ft.TransferSize, uint32(len(cc.Server.banner)))
 
-       binary.BigEndian.PutUint32(ft.TransferSize, uint32(fi.Size()))
-
-       res = append(res, cc.NewReply(t,
+       return append(res, cc.NewReply(t,
                NewField(FieldRefNum, ft.refNum[:]),
                NewField(FieldTransferSize, ft.TransferSize),
-       ))
-
-       return res, err
+       )), err
 }