]> git.r.bdr.sh - rbdr/mobius/blobdiff - hotline/transaction_handlers.go
Fix "No Agreement" behavior for 1.2.3 clients
[rbdr/mobius] / hotline / transaction_handlers.go
index 1b6911493862cbf177e42c8a22794abca7a5e20c..85879a4de1bc714c0a9d18e56e9e610bf0ad5f91 100644 (file)
@@ -10,6 +10,7 @@ import (
        "math/big"
        "os"
        "path"
+       "path/filepath"
        "sort"
        "strings"
        "time"
@@ -86,7 +87,7 @@ var TransactionHandlers = map[uint16]TransactionType{
        },
        tranGetClientInfoText: {
                Name:    "tranGetClientInfoText",
-               Handler: HandleGetClientConnInfoText,
+               Handler: HandleGetClientInfoText,
        },
        tranGetFileInfo: {
                Name:    "tranGetFileInfo",
@@ -230,6 +231,10 @@ var TransactionHandlers = map[uint16]TransactionType{
                Name:    "tranUserBroadcast",
                Handler: HandleUserBroadcast,
        },
+       tranDownloadBanner: {
+               Name:    "tranDownloadBanner",
+               Handler: HandleDownloadBanner,
+       },
 }
 
 func HandleChatSend(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
@@ -249,10 +254,6 @@ func HandleChatSend(cc *ClientConn, t *Transaction) (res []Transaction, err erro
                formattedMsg = fmt.Sprintf("\r*** %s %s", cc.UserName, t.GetField(fieldData).Data)
        }
 
-       if bytes.Equal(t.GetField(fieldData).Data, []byte("/stats")) {
-               formattedMsg = strings.Replace(cc.Server.Stats.String(), "\n", "\r", -1)
-       }
-
        chatID := t.GetField(fieldChatID).Data
        // a non-nil chatID indicates the message belongs to a private chat
        if chatID != nil {
@@ -298,6 +299,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)
 
@@ -347,26 +353,30 @@ func HandleGetFileInfo(cc *ClientConn, t *Transaction) (res []Transaction, err e
        fileName := t.GetField(fieldFileName).Data
        filePath := t.GetField(fieldFilePath).Data
 
-       ffo, err := NewFlattenedFileObject(cc.Server.Config.FileRoot, filePath, fileName, 0)
+       fullFilePath, err := readPath(cc.Server.Config.FileRoot, filePath, fileName)
+       if err != nil {
+               return res, err
+       }
+
+       fw, err := newFileWrapper(cc.Server.FS, fullFilePath, 0)
        if err != nil {
                return res, err
        }
 
        res = append(res, cc.NewReply(t,
-               NewField(fieldFileName, fileName),
-               NewField(fieldFileTypeString, ffo.FlatFileInformationFork.friendlyType()),
-               NewField(fieldFileCreatorString, ffo.FlatFileInformationFork.CreatorSignature),
-               NewField(fieldFileComment, ffo.FlatFileInformationFork.Comment),
-               NewField(fieldFileType, ffo.FlatFileInformationFork.TypeSignature),
-               NewField(fieldFileCreateDate, ffo.FlatFileInformationFork.CreateDate),
-               NewField(fieldFileModifyDate, ffo.FlatFileInformationFork.ModifyDate),
-               NewField(fieldFileSize, ffo.FlatFileDataForkHeader.DataSize[:]),
+               NewField(fieldFileName, []byte(fw.name)),
+               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()),
        ))
        return res, err
 }
 
 // HandleSetFileInfo updates a file or folder name and/or comment from the Get Info window
-// TODO: Implement support for comments
 // Fields used in the request:
 // * 201       File name
 // * 202       File path       Optional
@@ -382,36 +392,79 @@ func HandleSetFileInfo(cc *ClientConn, t *Transaction) (res []Transaction, err e
                return res, err
        }
 
+       fi, err := cc.Server.FS.Stat(fullFilePath)
+       if err != nil {
+               return res, err
+       }
+
+       hlFile, err := newFileWrapper(cc.Server.FS, fullFilePath, 0)
+       if err != nil {
+               return res, err
+       }
+       if t.GetField(fieldFileComment).Data != nil {
+               switch mode := fi.Mode(); {
+               case mode.IsDir():
+                       if !authorize(cc.Account.Access, 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) {
+                               res = append(res, cc.NewErrReply(t, "You are not allowed to set comments for files."))
+                               return res, err
+                       }
+               }
+
+               if err := hlFile.ffo.FlatFileInformationFork.setComment(t.GetField(fieldFileComment).Data); err != nil {
+                       return res, err
+               }
+               w, err := hlFile.infoForkWriter()
+               if err != nil {
+                       return res, err
+               }
+               _, err = w.Write(hlFile.ffo.FlatFileInformationFork.MarshalBinary())
+               if err != nil {
+                       return res, err
+               }
+       }
+
        fullNewFilePath, err := readPath(cc.Server.Config.FileRoot, filePath, t.GetField(fieldFileNewName).Data)
        if err != nil {
                return nil, err
        }
 
-       // fileComment := t.GetField(fieldFileComment).Data
        fileNewName := t.GetField(fieldFileNewName).Data
 
        if fileNewName != nil {
-               fi, err := cc.Server.FS.Stat(fullFilePath)
-               if err != nil {
-                       return res, err
-               }
                switch mode := fi.Mode(); {
                case mode.IsDir():
                        if !authorize(cc.Account.Access, accessRenameFolder) {
                                res = append(res, cc.NewErrReply(t, "You are not allowed to rename folders."))
                                return res, err
                        }
+                       err = os.Rename(fullFilePath, fullNewFilePath)
+                       if os.IsNotExist(err) {
+                               res = append(res, cc.NewErrReply(t, "Cannot rename folder "+string(fileName)+" because it does not exist or cannot be found."))
+                               return res, err
+                       }
                case mode.IsRegular():
                        if !authorize(cc.Account.Access, accessRenameFile) {
                                res = append(res, cc.NewErrReply(t, "You are not allowed to rename files."))
                                return res, err
                        }
-               }
-
-               err = os.Rename(fullFilePath, fullNewFilePath)
-               if os.IsNotExist(err) {
-                       res = append(res, cc.NewErrReply(t, "Cannot rename file "+string(fileName)+" because it does not exist or cannot be found."))
-                       return res, err
+                       fileDir, err := readPath(cc.Server.Config.FileRoot, filePath, []byte{})
+                       if err != nil {
+                               return nil, err
+                       }
+                       hlFile.name = string(fileNewName)
+                       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."))
+                               return res, err
+                       }
+                       if err != nil {
+                               panic(err)
+                       }
                }
        }
 
@@ -433,13 +486,17 @@ func HandleDeleteFile(cc *ClientConn, t *Transaction) (res []Transaction, err er
                return res, err
        }
 
-       cc.Server.Logger.Debugw("Delete file", "src", fullFilePath)
+       hlFile, err := newFileWrapper(cc.Server.FS, fullFilePath, 0)
+       if err != nil {
+               return res, err
+       }
 
-       fi, err := os.Stat(fullFilePath)
+       fi, err := hlFile.dataFile()
        if err != nil {
                res = append(res, cc.NewErrReply(t, "Cannot delete file "+string(fileName)+" because it does not exist or cannot be found."))
                return res, nil
        }
+
        switch mode := fi.Mode(); {
        case mode.IsDir():
                if !authorize(cc.Account.Access, accessDeleteFolder) {
@@ -453,7 +510,7 @@ func HandleDeleteFile(cc *ClientConn, t *Transaction) (res []Transaction, err er
                }
        }
 
-       if err := os.RemoveAll(fullFilePath); err != nil {
+       if err := hlFile.delete(); err != nil {
                return res, err
        }
 
@@ -464,13 +521,29 @@ func HandleDeleteFile(cc *ClientConn, t *Transaction) (res []Transaction, err er
 // HandleMoveFile moves files or folders. Note: seemingly not documented
 func HandleMoveFile(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
        fileName := string(t.GetField(fieldFileName).Data)
-       filePath := cc.Server.Config.FileRoot + ReadFilePath(t.GetField(fieldFilePath).Data)
-       fileNewPath := cc.Server.Config.FileRoot + ReadFilePath(t.GetField(fieldFileNewPath).Data)
 
-       cc.Server.Logger.Debugw("Move file", "src", filePath+"/"+fileName, "dst", fileNewPath+"/"+fileName)
+       filePath, err := readPath(cc.Server.Config.FileRoot, t.GetField(fieldFilePath).Data, t.GetField(fieldFileName).Data)
+       if err != nil {
+               return res, err
+       }
+
+       fileNewPath, err := readPath(cc.Server.Config.FileRoot, t.GetField(fieldFileNewPath).Data, nil)
+       if err != nil {
+               return res, err
+       }
+
+       cc.logger.Infow("Move file", "src", filePath+"/"+fileName, "dst", fileNewPath+"/"+fileName)
 
-       fp := filePath + "/" + fileName
-       fi, err := os.Stat(fp)
+       hlFile, err := newFileWrapper(cc.Server.FS, filePath, 0)
+       if err != nil {
+               return res, err
+       }
+
+       fi, err := hlFile.dataFile()
+       if err != nil {
+               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
        }
@@ -486,16 +559,10 @@ func HandleMoveFile(cc *ClientConn, t *Transaction) (res []Transaction, err erro
                        return res, err
                }
        }
-
-       err = os.Rename(filePath+"/"+fileName, fileNewPath+"/"+fileName)
-       if os.IsNotExist(err) {
-               res = append(res, cc.NewErrReply(t, "Cannot delete file "+fileName+" because it does not exist or cannot be found."))
+       if err := hlFile.move(fileNewPath); err != nil {
                return res, err
        }
-       if err != nil {
-               return []Transaction{}, err
-       }
-       // TODO: handle other possible errors; e.g. file delete fails due to file permission issue
+       // TODO: handle other possible errors; e.g. fileWrapper delete fails due to fileWrapper permission issue
 
        res = append(res, cc.NewReply(t))
        return res, err
@@ -506,11 +573,12 @@ func HandleNewFolder(cc *ClientConn, t *Transaction) (res []Transaction, err err
                res = append(res, cc.NewErrReply(t, "You are not allowed to create folders."))
                return res, err
        }
-       newFolderPath := cc.Server.Config.FileRoot
        folderName := string(t.GetField(fieldFileName).Data)
 
        folderName = path.Join("/", folderName)
 
+       var subPath string
+
        // fieldFilePath is only present for nested paths
        if t.GetField(fieldFilePath).Data != nil {
                var newFp FilePath
@@ -518,9 +586,12 @@ func HandleNewFolder(cc *ClientConn, t *Transaction) (res []Transaction, err err
                if err != nil {
                        return nil, err
                }
-               newFolderPath += newFp.String()
+
+               for _, pathItem := range newFp.Items {
+                       subPath = filepath.Join("/", subPath, string(pathItem.Name))
+               }
        }
-       newFolderPath = path.Join(newFolderPath, folderName)
+       newFolderPath := path.Join(cc.Server.Config.FileRoot, subPath, folderName)
 
        // TODO: check path and folder name lengths
 
@@ -568,7 +639,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
        }
 
@@ -579,22 +650,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)))
+                       flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(c.Flags)))
                        if authorize(c.Account.Access, 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),
                        )
                }
        }
@@ -632,8 +703,13 @@ func HandleListUsers(cc *ClientConn, t *Transaction) (res []Transaction, err 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...))
@@ -658,7 +734,7 @@ func HandleUpdateUser(cc *ClientConn, t *Transaction) (res []Transaction, err er
 
                if len(subFields) == 1 {
                        login := DecodeUserString(getField(fieldData, &subFields).Data)
-                       cc.Server.Logger.Infow("DeleteUser", "login", login)
+                       cc.logger.Infow("DeleteUser", "login", login)
 
                        if !authorize(cc.Account.Access, accessDeleteUser) {
                                res = append(res, cc.NewErrReply(t, "You are not allowed to delete accounts."))
@@ -673,11 +749,11 @@ func HandleUpdateUser(cc *ClientConn, t *Transaction) (res []Transaction, err er
 
                login := DecodeUserString(getField(fieldUserLogin, &subFields).Data)
 
-               // check if the login exists; if so, we know we are updating an existing user
+               // check if the login dataFile; if so, we know we are updating an existing user
                if acc, ok := cc.Server.Accounts[login]; ok {
-                       cc.Server.Logger.Infow("UpdateUser", "login", login)
+                       cc.logger.Infow("UpdateUser", "login", login)
 
-                       // account exists, so this is an update action
+                       // account dataFile, so this is an update action
                        if !authorize(cc.Account.Access, accessModifyUser) {
                                res = append(res, cc.NewErrReply(t, "You are not allowed to modify accounts."))
                                return res, err
@@ -705,7 +781,7 @@ func HandleUpdateUser(cc *ClientConn, t *Transaction) (res []Transaction, err er
                                return res, err
                        }
                } else {
-                       cc.Server.Logger.Infow("CreateUser", "login", login)
+                       cc.logger.Infow("CreateUser", "login", login)
 
                        if !authorize(cc.Account.Access, accessCreateUser) {
                                res = append(res, cc.NewErrReply(t, "You are not allowed to create new accounts."))
@@ -737,7 +813,7 @@ func HandleNewUser(cc *ClientConn, t *Transaction) (res []Transaction, err error
 
        login := DecodeUserString(t.GetField(fieldUserLogin).Data)
 
-       // If the account already exists, reply with an error
+       // If the account already dataFile, reply with an error
        if _, ok := cc.Server.Accounts[login]; ok {
                res = append(res, cc.NewErrReply(t, "Cannot create account "+login+" because there is already an account with that login."))
                return res, err
@@ -801,9 +877,17 @@ func byteToInt(bytes []byte) (int, error) {
        return 0, errors.New("unknown byte length")
 }
 
-func HandleGetClientConnInfoText(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
+// HandleGetClientInfoText returns user information for the specific user.
+//
+// Fields used in the request:
+// 103 User ID
+//
+// Fields used in the reply:
+// 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) {
-               res = append(res, cc.NewErrReply(t, "You are not allowed to get client info"))
+               res = append(res, cc.NewErrReply(t, "You are not allowed to get client info."))
                return res, err
        }
 
@@ -811,55 +895,11 @@ func HandleGetClientConnInfoText(cc *ClientConn, t *Transaction) (res []Transact
 
        clientConn := cc.Server.Clients[uint16(clientID)]
        if clientConn == nil {
-               return res, errors.New("invalid client")
-       }
-
-       // TODO: Implement non-hardcoded values
-       template := `Nickname:   %s
-Name:       %s
-Account:    %s
-Address:    %s
-
--------- File Downloads ---------
-
-%s
-
-------- Folder Downloads --------
-
-None.
-
---------- File Uploads ----------
-
-None.
-
--------- Folder Uploads ---------
-
-None.
-
-------- Waiting Downloads -------
-
-None.
-
-       `
-
-       activeDownloads := clientConn.Transfers[FileDownload]
-       activeDownloadList := "None."
-       for _, dl := range activeDownloads {
-               activeDownloadList += dl.String() + "\n"
+               return append(res, cc.NewErrReply(t, "User not found.")), err
        }
 
-       template = fmt.Sprintf(
-               template,
-               clientConn.UserName,
-               clientConn.Account.Name,
-               clientConn.Account.Login,
-               clientConn.RemoteAddr,
-               activeDownloadList,
-       )
-       template = strings.Replace(template, "\n", "\r", -1)
-
        res = append(res, cc.NewReply(t,
-               NewField(fieldData, []byte(template)),
+               NewField(fieldData, []byte(clientConn.String())),
                NewField(fieldUserName, clientConn.UserName),
        ))
        return res, err
@@ -873,24 +913,35 @@ 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))
 
        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
@@ -900,15 +951,20 @@ func HandleTranAgreed(cc *ClientConn, t *Transaction) (res []Transaction, err er
                cc.AutoReply = []byte{}
        }
 
-       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),
                ),
        )
+       res = append(res, trans...)
+
+       if cc.Server.Config.BannerFile != "" {
+               res = append(res, *NewTransaction(tranServerBanner, cc.ID, NewField(fieldBannerType, []byte("JPEG"))))
+       }
 
        res = append(res, cc.NewReply(t))
 
@@ -980,12 +1036,48 @@ func HandleDisconnectUser(cc *ClientConn, t *Transaction) (res []Transaction, er
                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
@@ -997,9 +1089,6 @@ func HandleGetNewsCatNameList(cc *ClientConn, t *Transaction) (res []Transaction
                return res, err
        }
 
-       newsPath := t.GetField(fieldNewsPath).Data
-       cc.Server.Logger.Infow("NewsPath: ", "np", string(newsPath))
-
        pathStrs := ReadNewsPath(t.GetField(fieldNewsPath).Data)
        cats := cc.Server.GetNewsCatByPath(pathStrs)
 
@@ -1062,7 +1151,7 @@ func HandleNewNewsFldr(cc *ClientConn, t *Transaction) (res []Transaction, err e
        name := string(t.GetField(fieldFileName).Data)
        pathStrs := ReadNewsPath(t.GetField(fieldNewsPath).Data)
 
-       cc.Server.Logger.Infof("Creating new news folder %s", name)
+       cc.logger.Infof("Creating new news folder %s", name)
 
        cats := cc.Server.GetNewsCatByPath(pathStrs)
        cats[name] = NewsCategoryListData15{
@@ -1167,7 +1256,7 @@ func HandleDelNewsItem(cc *ClientConn, t *Transaction) (res []Transaction, err e
 
        // TODO: determine if path is a Folder (Bundle) or Category and check for permission
 
-       cc.Server.Logger.Infof("DelNewsItem %v", pathStrs)
+       cc.logger.Infof("DelNewsItem %v", pathStrs)
 
        cats := cc.Server.ThreadedNews.Categories
 
@@ -1310,7 +1399,6 @@ func HandleDownloadFile(cc *ClientConn, t *Transaction) (res []Transaction, err
 
        fileName := t.GetField(fieldFileName).Data
        filePath := t.GetField(fieldFilePath).Data
-
        resumeData := t.GetField(fieldFileResumeData).Data
 
        var dataOffset int64
@@ -1319,30 +1407,25 @@ func HandleDownloadFile(cc *ClientConn, t *Transaction) (res []Transaction, err
                if err := frd.UnmarshalBinary(t.GetField(fieldFileResumeData).Data); err != nil {
                        return res, err
                }
+               // TODO: handle rsrc fork offset
                dataOffset = int64(binary.BigEndian.Uint32(frd.ForkInfoList[0].DataSize[:]))
        }
 
-       var fp FilePath
-       err = fp.UnmarshalBinary(filePath)
+       fullFilePath, err := readPath(cc.Server.Config.FileRoot, filePath, fileName)
        if err != nil {
                return res, err
        }
 
-       ffo, err := NewFlattenedFileObject(cc.Server.Config.FileRoot, filePath, fileName, dataOffset)
+       hlFile, err := newFileWrapper(cc.Server.FS, fullFilePath, dataOffset)
        if err != nil {
                return res, err
        }
 
-       transactionRef := cc.Server.NewTransactionRef()
-       data := binary.BigEndian.Uint32(transactionRef)
+       xferSize := hlFile.ffo.TransferSize(0)
 
-       ft := &FileTransfer{
-               FileName:        fileName,
-               FilePath:        filePath,
-               ReferenceNumber: transactionRef,
-               Type:            FileDownload,
-       }
+       ft := cc.newFileTransfer(FileDownload, fileName, filePath, xferSize)
 
+       // TODO: refactor to remove this
        if resumeData != nil {
                var frd FileResumeData
                if err := frd.UnmarshalBinary(t.GetField(fieldFileResumeData).Data); err != nil {
@@ -1351,27 +1434,19 @@ func HandleDownloadFile(cc *ClientConn, t *Transaction) (res []Transaction, err
                ft.fileResumeData = &frd
        }
 
-       xferSize := ffo.TransferSize()
-
        // Optional field for when a HL v1.5+ client requests file preview
        // Used only for TEXT, JPEG, GIFF, BMP or PICT files
        // The value will always be 2
        if t.GetField(fieldFileTransferOptions).Data != nil {
                ft.options = t.GetField(fieldFileTransferOptions).Data
-               xferSize = ffo.FlatFileDataForkHeader.DataSize[:]
+               xferSize = hlFile.ffo.FlatFileDataForkHeader.DataSize[:]
        }
 
-       cc.Server.mux.Lock()
-       defer cc.Server.mux.Unlock()
-       cc.Server.FileTransfers[data] = ft
-
-       cc.Transfers[FileDownload] = append(cc.Transfers[FileDownload], ft)
-
        res = append(res, cc.NewReply(t,
-               NewField(fieldRefNum, transactionRef),
+               NewField(fieldRefNum, ft.refNum[:]),
                NewField(fieldWaitingCount, []byte{0x00, 0x00}), // TODO: Implement waiting count
                NewField(fieldTransferSize, xferSize),
-               NewField(fieldFileSize, ffo.FlatFileDataForkHeader.DataSize[:]),
+               NewField(fieldFileSize, hlFile.ffo.FlatFileDataForkHeader.DataSize[:]),
        ))
 
        return res, err
@@ -1384,26 +1459,6 @@ func HandleDownloadFolder(cc *ClientConn, t *Transaction) (res []Transaction, er
                return res, err
        }
 
-       transactionRef := cc.Server.NewTransactionRef()
-       data := binary.BigEndian.Uint32(transactionRef)
-
-       fileTransfer := &FileTransfer{
-               FileName:        t.GetField(fieldFileName).Data,
-               FilePath:        t.GetField(fieldFilePath).Data,
-               ReferenceNumber: transactionRef,
-               Type:            FolderDownload,
-       }
-       cc.Server.mux.Lock()
-       cc.Server.FileTransfers[data] = fileTransfer
-       cc.Server.mux.Unlock()
-       cc.Transfers[FolderDownload] = append(cc.Transfers[FolderDownload], fileTransfer)
-
-       var fp FilePath
-       err = fp.UnmarshalBinary(t.GetField(fieldFilePath).Data)
-       if err != nil {
-               return res, err
-       }
-
        fullFilePath, err := readPath(cc.Server.Config.FileRoot, t.GetField(fieldFilePath).Data, t.GetField(fieldFileName).Data)
        if err != nil {
                return res, err
@@ -1417,8 +1472,17 @@ func HandleDownloadFolder(cc *ClientConn, t *Transaction) (res []Transaction, er
        if err != nil {
                return res, err
        }
+
+       fileTransfer := cc.newFileTransfer(FolderDownload, t.GetField(fieldFileName).Data, t.GetField(fieldFilePath).Data, transferSize)
+
+       var fp FilePath
+       err = fp.UnmarshalBinary(t.GetField(fieldFilePath).Data)
+       if err != nil {
+               return res, err
+       }
+
        res = append(res, cc.NewReply(t,
-               NewField(fieldRefNum, transactionRef),
+               NewField(fieldRefNum, fileTransfer.ReferenceNumber),
                NewField(fieldTransferSize, transferSize),
                NewField(fieldFolderItemCount, itemCount),
                NewField(fieldWaitingCount, []byte{0x00, 0x00}), // TODO: Implement waiting count
@@ -1434,9 +1498,6 @@ func HandleDownloadFolder(cc *ClientConn, t *Transaction) (res []Transaction, er
 // 220 Folder item count
 // 204 File transfer options   "Optional Currently set to 1" (TODO: ??)
 func HandleUploadFolder(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
-       transactionRef := cc.Server.NewTransactionRef()
-       data := binary.BigEndian.Uint32(transactionRef)
-
        var fp FilePath
        if t.GetField(fieldFilePath).Data != nil {
                if err = fp.UnmarshalBinary(t.GetField(fieldFilePath).Data); err != nil {
@@ -1452,17 +1513,15 @@ func HandleUploadFolder(cc *ClientConn, t *Transaction) (res []Transaction, err
                }
        }
 
-       fileTransfer := &FileTransfer{
-               FileName:        t.GetField(fieldFileName).Data,
-               FilePath:        t.GetField(fieldFilePath).Data,
-               ReferenceNumber: transactionRef,
-               Type:            FolderUpload,
-               FolderItemCount: t.GetField(fieldFolderItemCount).Data,
-               TransferSize:    t.GetField(fieldTransferSize).Data,
-       }
-       cc.Server.FileTransfers[data] = fileTransfer
+       fileTransfer := cc.newFileTransfer(FolderUpload,
+               t.GetField(fieldFileName).Data,
+               t.GetField(fieldFilePath).Data,
+               t.GetField(fieldTransferSize).Data,
+       )
 
-       res = append(res, cc.NewReply(t, NewField(fieldRefNum, transactionRef)))
+       fileTransfer.FolderItemCount = t.GetField(fieldFolderItemCount).Data
+
+       res = append(res, cc.NewReply(t, NewField(fieldRefNum, fileTransfer.ReferenceNumber)))
        return res, err
 }
 
@@ -1481,11 +1540,8 @@ func HandleUploadFile(cc *ClientConn, t *Transaction) (res []Transaction, err er
 
        fileName := t.GetField(fieldFileName).Data
        filePath := t.GetField(fieldFilePath).Data
-
        transferOptions := t.GetField(fieldFileTransferOptions).Data
-
-       // TODO: is this field useful for anything?
-       // transferSize := t.GetField(fieldTransferSize).Data
+       transferSize := t.GetField(fieldTransferSize).Data // not sent for resume
 
        var fp FilePath
        if filePath != nil {
@@ -1501,27 +1557,22 @@ func HandleUploadFile(cc *ClientConn, t *Transaction) (res []Transaction, err er
                        return res, err
                }
        }
+       fullFilePath, err := readPath(cc.Server.Config.FileRoot, filePath, fileName)
+       if err != nil {
+               return res, err
+       }
 
-       transactionRef := cc.Server.NewTransactionRef()
-       data := binary.BigEndian.Uint32(transactionRef)
-
-       cc.Server.mux.Lock()
-       cc.Server.FileTransfers[data] = &FileTransfer{
-               FileName:        fileName,
-               FilePath:        filePath,
-               ReferenceNumber: transactionRef,
-               Type:            FileUpload,
+       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))))
+               return res, err
        }
-       cc.Server.mux.Unlock()
 
-       replyT := cc.NewReply(t, NewField(fieldRefNum, transactionRef))
+       ft := cc.newFileTransfer(FileUpload, fileName, filePath, transferSize)
 
-       // client has requested to resume a partially transfered file
+       replyT := cc.NewReply(t, NewField(fieldRefNum, ft.ReferenceNumber))
+
+       // client has requested to resume a partially transferred file
        if transferOptions != nil {
-               fullFilePath, err := readPath(cc.Server.Config.FileRoot, filePath, fileName)
-               if err != nil {
-                       return res, err
-               }
 
                fileInfo, err := cc.Server.FS.Stat(fullFilePath + incompleteFileSuffix)
                if err != nil {
@@ -1537,6 +1588,8 @@ func HandleUploadFile(cc *ClientConn, t *Transaction) (res []Transaction, err er
 
                b, _ := fileResumeData.BinaryMarshal()
 
+               ft.TransferSize = offset
+
                replyT.Fields = append(replyT.Fields, NewField(fieldFileResumeData, b))
        }
 
@@ -1545,27 +1598,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 +1627,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
 }
@@ -1615,11 +1669,11 @@ func HandleGetFileNameList(cc *ClientConn, t *Transaction) (res []Transaction, e
 
        // Handle special case for drop box folders
        if fp.IsDropbox() && !authorize(cc.Account.Access, accessViewDropBoxes) {
-               res = append(res, cc.NewReply(t))
+               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
        }
@@ -1667,8 +1721,8 @@ func HandleInviteNewChat(cc *ClientConn, t *Transaction) (res []Transaction, err
                        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),
                ),
        )
 
@@ -1700,8 +1754,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 +1804,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 +1816,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),
                }
 
@@ -1782,7 +1836,10 @@ func HandleLeaveChat(cc *ClientConn, t *Transaction) (res []Transaction, err err
        chatID := t.GetField(fieldChatID).Data
        chatInt := binary.BigEndian.Uint32(chatID)
 
-       privChat := cc.Server.PrivateChats[chatInt]
+       privChat, ok := cc.Server.PrivateChats[chatInt]
+       if !ok {
+               return res, nil
+       }
 
        delete(privChat.ClientConn, cc.uint16ID())
 
@@ -1827,7 +1884,7 @@ func HandleSetChatSubject(cc *ClientConn, t *Transaction) (res []Transaction, er
        return res, err
 }
 
-// HandleMakeAlias makes a file alias using the specified path.
+// HandleMakeAlias makes a filer alias using the specified path.
 // Fields used in the request:
 // 201 File name
 // 202 File path
@@ -1854,7 +1911,7 @@ func HandleMakeAlias(cc *ClientConn, t *Transaction) (res []Transaction, err err
                return res, err
        }
 
-       cc.Server.Logger.Debugw("Make alias", "src", fullFilePath, "dst", fullNewFilePath)
+       cc.logger.Debugw("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"))
@@ -1864,3 +1921,21 @@ func HandleMakeAlias(cc *ClientConn, t *Transaction) (res []Transaction, err err
        res = append(res, cc.NewReply(t))
        return res, err
 }
+
+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(fi.Size()))
+
+       res = append(res, cc.NewReply(t,
+               NewField(fieldRefNum, ft.refNum[:]),
+               NewField(fieldTransferSize, ft.TransferSize),
+       ))
+
+       return res, err
+}