X-Git-Url: https://git.r.bdr.sh/rbdr/mobius/blobdiff_plain/9cf66aeafbcbb9237fedc2efc97cc2856eb60f7f..45ca5d60383cbe270624c713b916da29af7ba88f:/hotline/transaction_handlers.go diff --git a/hotline/transaction_handlers.go b/hotline/transaction_handlers.go index 4bb956a..20b87e2 100644 --- a/hotline/transaction_handlers.go +++ b/hotline/transaction_handlers.go @@ -1,6 +1,7 @@ package hotline import ( + "bufio" "bytes" "encoding/binary" "errors" @@ -414,11 +415,11 @@ func HandleGetFileInfo(cc *ClientConn, t *Transaction) (res []Transaction, err e 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) { @@ -516,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) { @@ -574,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 { @@ -636,10 +637,10 @@ func HandleNewFolder(cc *ClientConn, t *Transaction) (res []Transaction, err err 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 } @@ -658,7 +659,7 @@ func HandleSetUser(cc *ClientConn, t *Transaction) (res []Transaction, err error return res, err } - login := decodeString(t.GetField(FieldUserLogin).Data) + login := string(encodeString(t.GetField(FieldUserLogin).Data)) userName := string(t.GetField(FieldUserName).Data) newAccessLvl := t.GetField(FieldUserAccess).Data @@ -748,7 +749,8 @@ func HandleListUsers(cc *ClientConn, t *Transaction) (res []Transaction, err err var userFields []Field for _, acc := range cc.Server.Accounts { - b, err := io.ReadAll(acc) + accCopy := *acc + b, err := io.ReadAll(&accCopy) if err != nil { return res, err } @@ -771,9 +773,21 @@ 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 @@ -783,8 +797,8 @@ func HandleUpdateUser(cc *ClientConn, t *Transaction) (res []Transaction, err er return res, err } - login := decodeString(getField(FieldData, &subFields).Data) - cc.logger.Infow("DeleteUser", "login", login) + login := string(encodeString(getField(FieldData, &subFields).Data)) + cc.logger.Info("DeleteUser", "login", login) if err := cc.Server.DeleteUser(login); err != nil { return res, err @@ -798,9 +812,9 @@ func HandleUpdateUser(cc *ClientConn, t *Transaction) (res []Transaction, err er // 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 = decodeString(getField(FieldData, &subFields).Data) + loginToRename = string(encodeString(getField(FieldData, &subFields).Data)) } - userLogin := decodeString(getField(FieldUserLogin, &subFields).Data) + userLogin := string(encodeString(getField(FieldUserLogin, &subFields).Data)) if loginToRename != "" { accountToUpdate = loginToRename } else { @@ -810,9 +824,9 @@ func HandleUpdateUser(cc *ClientConn, t *Transaction) (res []Transaction, err er // 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.Infow("RenameUser", "prevLogin", accountToUpdate, "newLogin", userLogin) + cc.logger.Info("RenameUser", "prevLogin", accountToUpdate, "newLogin", userLogin) } else { - cc.logger.Infow("UpdateUser", "login", accountToUpdate) + cc.logger.Info("UpdateUser", "login", accountToUpdate) } // account exists, so this is an update action @@ -842,8 +856,8 @@ func HandleUpdateUser(cc *ClientConn, t *Transaction) (res []Transaction, err er } err = cc.Server.UpdateUser( - decodeString(getField(FieldData, &subFields).Data), - decodeString(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, @@ -857,7 +871,7 @@ func HandleUpdateUser(cc *ClientConn, t *Transaction) (res []Transaction, err er return res, nil } - cc.logger.Infow("CreateUser", "login", userLogin) + cc.logger.Info("CreateUser", "login", userLogin) newAccess := accessBitmap{} copy(newAccess[:], getField(FieldUserAccess, &subFields).Data) @@ -889,7 +903,7 @@ func HandleNewUser(cc *ClientConn, t *Transaction) (res []Transaction, err error return res, err } - login := decodeString(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 { @@ -925,7 +939,7 @@ func HandleDeleteUser(cc *ClientConn, t *Transaction) (res []Transaction, err er return res, nil } - login := decodeString(t.GetField(FieldUserLogin).Data) + login := string(encodeString(t.GetField(FieldUserLogin).Data)) if err := cc.Server.DeleteUser(login); err != nil { return res, err @@ -958,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) { @@ -997,8 +1011,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))) @@ -1006,19 +1020,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 { + 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 { + 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{} @@ -1107,7 +1121,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, @@ -1120,7 +1134,7 @@ func HandleDisconnectUser(cc *ClientConn, t *Transaction) (res []Transaction, er cc.Server.banList[strings.Split(clientConn.RemoteAddr, ":")[0]] = &banUntil 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, @@ -1207,7 +1221,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) { @@ -1218,8 +1232,6 @@ 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, @@ -1315,11 +1327,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)), )) @@ -1421,14 +1433,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), } @@ -1444,10 +1459,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 @@ -1455,8 +1470,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) } } @@ -1584,7 +1599,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 @@ -1619,7 +1634,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" @@ -1655,7 +1670,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 } @@ -1704,14 +1719,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{} @@ -1748,13 +1763,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) } } @@ -1766,7 +1781,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...)) @@ -1896,7 +1911,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 @@ -1999,7 +2014,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 // @@ -2024,7 +2039,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")) @@ -2042,19 +2057,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 }