]> git.r.bdr.sh - rbdr/mobius/blobdiff - hotline/transaction_handlers.go
Update Docker instructions
[rbdr/mobius] / hotline / transaction_handlers.go
index 992dcec8525b2fa64035cb0970a66c12c04d19e8..ec7910a49fdad6af34d9d5c551ef367934eb4d3e 100644 (file)
@@ -5,7 +5,7 @@ import (
        "encoding/binary"
        "errors"
        "fmt"
-       "gopkg.in/yaml.v2"
+       "gopkg.in/yaml.v3"
        "io/ioutil"
        "math/big"
        "os"
@@ -45,10 +45,12 @@ var TransactionHandlers = map[uint16]TransactionType{
                Name: "tranNotifyDeleteUser",
        },
        tranAgreed: {
+               Access:  accessAlwaysAllow,
                Name:    "tranAgreed",
                Handler: HandleTranAgreed,
        },
        tranChatSend: {
+               Access:  accessAlwaysAllow,
                Handler: HandleChatSend,
                Name:    "tranChatSend",
                RequiredFields: []requiredField{
@@ -65,16 +67,19 @@ var TransactionHandlers = map[uint16]TransactionType{
                Handler: HandleDelNewsArt,
        },
        tranDelNewsItem: {
+               Access: accessAlwaysAllow, // Granular access enforced inside the handler
                // Has multiple access flags: News Delete Folder (37) or News Delete Category (35)
                // TODO: Implement inside the handler
                Name:    "tranDelNewsItem",
                Handler: HandleDelNewsItem,
        },
        tranDeleteFile: {
+               Access:  accessAlwaysAllow, // Granular access enforced inside the handler
                Name:    "tranDeleteFile",
                Handler: HandleDeleteFile,
        },
        tranDeleteUser: {
+               Access:  accessAlwaysAllow,
                Name:    "tranDeleteUser",
                Handler: HandleDeleteUser,
        },
@@ -85,8 +90,7 @@ var TransactionHandlers = map[uint16]TransactionType{
                Handler: HandleDisconnectUser,
        },
        tranDownloadFile: {
-               Access:  accessDownloadFile,
-               DenyMsg: "You are not allowed to download files.",
+               Access:  accessAlwaysAllow,
                Name:    "tranDownloadFile",
                Handler: HandleDownloadFile,
        },
@@ -103,16 +107,17 @@ var TransactionHandlers = map[uint16]TransactionType{
                Handler: HandleGetClientConnInfoText,
        },
        tranGetFileInfo: {
+               Access:  accessAlwaysAllow,
                Name:    "tranGetFileInfo",
                Handler: HandleGetFileInfo,
        },
        tranGetFileNameList: {
+               Access:  accessAlwaysAllow,
                Name:    "tranGetFileNameList",
                Handler: HandleGetFileNameList,
        },
        tranGetMsgs: {
-               Access:  accessNewsReadArt,
-               DenyMsg: "You are not allowed to read news.",
+               Access:  accessAlwaysAllow,
                Name:    "tranGetMsgs",
                Handler: HandleGetMsgs,
        },
@@ -135,11 +140,12 @@ var TransactionHandlers = map[uint16]TransactionType{
                Handler: HandleGetNewsCatNameList,
        },
        tranGetUser: {
-               DenyMsg: "You are not allowed to view accounts.",
+               Access:  accessAlwaysAllow,
                Name:    "tranGetUser",
                Handler: HandleGetUser,
        },
        tranGetUserNameList: {
+               Access:  accessAlwaysAllow,
                Name:    "tranHandleGetUserNameList",
                Handler: HandleGetUserNameList,
        },
@@ -156,21 +162,22 @@ var TransactionHandlers = map[uint16]TransactionType{
                Handler: HandleInviteToChat,
        },
        tranJoinChat: {
+               Access:  accessAlwaysAllow,
                Name:    "tranJoinChat",
                Handler: HandleJoinChat,
        },
        tranKeepAlive: {
+               Access:  accessAlwaysAllow,
                Name:    "tranKeepAlive",
                Handler: HandleKeepAlive,
        },
        tranLeaveChat: {
+               Access:  accessAlwaysAllow,
                Name:    "tranJoinChat",
                Handler: HandleLeaveChat,
        },
-
        tranListUsers: {
-               Access:  accessOpenUser,
-               DenyMsg: "You are not allowed to view accounts.",
+               Access:  accessAlwaysAllow,
                Name:    "tranListUsers",
                Handler: HandleListUsers,
        },
@@ -199,8 +206,7 @@ var TransactionHandlers = map[uint16]TransactionType{
                Handler: HandleNewNewsFldr,
        },
        tranNewUser: {
-               Access:  accessCreateUser,
-               DenyMsg: "You are not allowed to create new accounts.",
+               Access:  accessAlwaysAllow,
                Name:    "tranNewUser",
                Handler: HandleNewUser,
        },
@@ -217,6 +223,7 @@ var TransactionHandlers = map[uint16]TransactionType{
                Handler: HandlePostNewsArt,
        },
        tranRejectChatInvite: {
+               Access:  accessAlwaysAllow,
                Name:    "tranRejectChatInvite",
                Handler: HandleRejectChatInvite,
        },
@@ -237,10 +244,12 @@ var TransactionHandlers = map[uint16]TransactionType{
                },
        },
        tranSetChatSubject: {
+               Access:  accessAlwaysAllow,
                Name:    "tranSetChatSubject",
                Handler: HandleSetChatSubject,
        },
        tranMakeFileAlias: {
+               Access:  accessAlwaysAllow,
                Name:    "tranMakeFileAlias",
                Handler: HandleMakeAlias,
                RequiredFields: []requiredField{
@@ -250,10 +259,12 @@ var TransactionHandlers = map[uint16]TransactionType{
                },
        },
        tranSetClientUserInfo: {
+               Access:  accessAlwaysAllow,
                Name:    "tranSetClientUserInfo",
                Handler: HandleSetClientUserInfo,
        },
        tranSetFileInfo: {
+               Access:  accessAlwaysAllow,
                Name:    "tranSetFileInfo",
                Handler: HandleSetFileInfo,
        },
@@ -264,10 +275,12 @@ var TransactionHandlers = map[uint16]TransactionType{
                Handler: HandleSetUser,
        },
        tranUploadFile: {
+               Access:  accessAlwaysAllow,
                Name:    "tranUploadFile",
                Handler: HandleUploadFile,
        },
        tranUploadFldr: {
+               Access:  accessAlwaysAllow,
                Name:    "tranUploadFldr",
                Handler: HandleUploadFolder,
        },
@@ -306,8 +319,10 @@ func HandleChatSend(cc *ClientConn, t *Transaction) (res []Transaction, err erro
                chatInt := binary.BigEndian.Uint32(chatID)
                privChat := cc.Server.PrivateChats[chatInt]
 
+               clients := sortedClients(privChat.ClientConn)
+
                // send the message to all connected clients of the private chat
-               for _, c := range privChat.ClientConn {
+               for _, c := range clients {
                        res = append(res, *NewTransaction(
                                tranChatMsg,
                                c.ID,
@@ -345,21 +360,25 @@ func HandleChatSend(cc *ClientConn, t *Transaction) (res []Transaction, err erro
 func HandleSendInstantMsg(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
        msg := t.GetField(fieldData)
        ID := t.GetField(fieldUserID)
-       // TODO: Implement reply quoting
-       // options := transaction.GetField(hotline.fieldOptions)
 
-       res = append(res,
-               *NewTransaction(
-                       tranServerMsg,
-                       &ID.Data,
-                       NewField(fieldData, msg.Data),
-                       NewField(fieldUserName, cc.UserName),
-                       NewField(fieldUserID, *cc.ID),
-                       NewField(fieldOptions, []byte{0, 1}),
-               ),
+       reply := *NewTransaction(
+               tranServerMsg,
+               &ID.Data,
+               NewField(fieldData, msg.Data),
+               NewField(fieldUserName, cc.UserName),
+               NewField(fieldUserID, *cc.ID),
+               NewField(fieldOptions, []byte{0, 1}),
        )
-       id, _ := byteToInt(ID.Data)
 
+       // Later versions of Hotline include the original message in the fieldQuotingMsg field so
+       //  the receiving client can display both the received message and what it is in reply to
+       if t.GetField(fieldQuotingMsg).Data != nil {
+               reply.Fields = append(reply.Fields, NewField(fieldQuotingMsg, t.GetField(fieldQuotingMsg).Data))
+       }
+
+       res = append(res, reply)
+
+       id, _ := byteToInt(ID.Data)
        otherClient := cc.Server.Clients[uint16(id)]
        if otherClient == nil {
                return res, errors.New("ohno")
@@ -388,7 +407,7 @@ 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)
+       ffo, err := NewFlattenedFileObject(cc.Server.Config.FileRoot, filePath, fileName, 0)
        if err != nil {
                return res, err
        }
@@ -644,8 +663,7 @@ func HandleGetUser(cc *ClientConn, t *Transaction) (res []Transaction, err error
 
        account := cc.Server.Accounts[string(t.GetField(fieldUserLogin).Data)]
        if account == nil {
-               errorT := cc.NewErrReply(t, "Account does not exist.")
-               res = append(res, errorT)
+               res = append(res, cc.NewErrReply(t, "Account does not exist."))
                return res, err
        }
 
@@ -659,6 +677,11 @@ func HandleGetUser(cc *ClientConn, t *Transaction) (res []Transaction, err error
 }
 
 func HandleListUsers(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
+       if !authorize(cc.Account.Access, accessOpenUser) {
+               res = append(res, cc.NewErrReply(t, "You are not allowed to view accounts."))
+               return res, err
+       }
+
        var userFields []Field
        // TODO: make order deterministic
        for _, acc := range cc.Server.Accounts {
@@ -672,10 +695,14 @@ func HandleListUsers(cc *ClientConn, t *Transaction) (res []Transaction, err err
 
 // HandleNewUser creates a new user account
 func HandleNewUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
+       if !authorize(cc.Account.Access, accessCreateUser) {
+               res = append(res, cc.NewErrReply(t, "You are not allowed to create new accounts."))
+               return res, err
+       }
+
        login := DecodeUserString(t.GetField(fieldUserLogin).Data)
 
        // If the account already exists, reply with an error
-       // TODO: make order deterministic
        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
@@ -1176,22 +1203,45 @@ 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) {
+               res = append(res, cc.NewErrReply(t, "You are not allowed to read news."))
+               return res, err
+       }
+
        res = append(res, cc.NewReply(t, NewField(fieldData, cc.Server.FlatNews)))
 
        return res, err
 }
 
 func HandleDownloadFile(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
+       if !authorize(cc.Account.Access, accessDownloadFile) {
+               res = append(res, cc.NewErrReply(t, "You are not allowed to download files."))
+               return res, err
+       }
+
        fileName := t.GetField(fieldFileName).Data
        filePath := t.GetField(fieldFilePath).Data
 
+       // 2 bytes
+       // transferOptions := t.GetField(fieldFileTransferOptions).Data
+       resumeData := t.GetField(fieldFileResumeData).Data
+
+       var dataOffset int64
+       var frd FileResumeData
+       if resumeData != nil {
+               if err := frd.UnmarshalBinary(t.GetField(fieldFileResumeData).Data); err != nil {
+                       return res, err
+               }
+               dataOffset = int64(binary.BigEndian.Uint32(frd.ForkInfoList[0].DataSize[:]))
+       }
+
        var fp FilePath
        err = fp.UnmarshalBinary(filePath)
        if err != nil {
                return res, err
        }
 
-       ffo, err := NewFlattenedFileObject(cc.Server.Config.FileRoot, filePath, fileName)
+       ffo, err := NewFlattenedFileObject(cc.Server.Config.FileRoot, filePath, fileName, dataOffset)
        if err != nil {
                return res, err
        }
@@ -1206,6 +1256,12 @@ func HandleDownloadFile(cc *ClientConn, t *Transaction) (res []Transaction, err
                Type:            FileDownload,
        }
 
+       if resumeData != nil {
+               var frd FileResumeData
+               frd.UnmarshalBinary(t.GetField(fieldFileResumeData).Data)
+               ft.fileResumeData = &frd
+       }
+
        cc.Server.FileTransfers[data] = ft
        cc.Transfers[FileDownload] = append(cc.Transfers[FileDownload], ft)
 
@@ -1325,8 +1381,12 @@ func HandleUploadFolder(cc *ClientConn, t *Transaction) (res []Transaction, err
 }
 
 // HandleUploadFile
-// Special cases:
-// * If the target directory contains "uploads" (case insensitive)
+// Fields used in the request:
+// 201 File name
+// 202 File path
+// 204 File transfer options   "Optional
+// 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) {
                res = append(res, cc.NewErrReply(t, "You are not allowed to upload files."))
@@ -1336,6 +1396,11 @@ 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
+
        var fp FilePath
        if filePath != nil {
                if err = fp.UnmarshalBinary(filePath); err != nil {
@@ -1361,7 +1426,33 @@ func HandleUploadFile(cc *ClientConn, t *Transaction) (res []Transaction, err er
                Type:            FileUpload,
        }
 
-       res = append(res, cc.NewReply(t, NewField(fieldRefNum, transactionRef)))
+       replyT := cc.NewReply(t, NewField(fieldRefNum, transactionRef))
+
+       // client has requested to resume a partially transfered file
+       if transferOptions != nil {
+               fullFilePath, err := readPath(cc.Server.Config.FileRoot, filePath, fileName)
+               if err != nil {
+                       return res, err
+               }
+
+               fileInfo, err := FS.Stat(fullFilePath + incompleteFileSuffix)
+               if err != nil {
+                       return res, err
+               }
+
+               offset := make([]byte, 4)
+               binary.BigEndian.PutUint32(offset, uint32(fileInfo.Size()))
+
+               fileResumeData := NewFileResumeData([]ForkInfoList{
+                       *NewForkInfoList(offset),
+               })
+
+               b, _ := fileResumeData.BinaryMarshal()
+
+               replyT.Fields = append(replyT.Fields, NewField(fieldFileResumeData, b))
+       }
+
+       res = append(res, replyT)
        return res, err
 }