8 "github.com/jhalter/mobius/hotline"
9 "golang.org/x/text/encoding/charmap"
19 // Converts bytes from Mac Roman encoding to UTF-8
20 var txtDecoder = charmap.Macintosh.NewDecoder()
22 // Converts bytes from UTF-8 to Mac Roman encoding
23 var txtEncoder = charmap.Macintosh.NewEncoder()
25 // Assign functions to handle specific Hotline transaction types
26 func RegisterHandlers(srv *hotline.Server) {
27 srv.HandleFunc(hotline.TranAgreed, HandleTranAgreed)
28 srv.HandleFunc(hotline.TranChatSend, HandleChatSend)
29 srv.HandleFunc(hotline.TranDelNewsArt, HandleDelNewsArt)
30 srv.HandleFunc(hotline.TranDelNewsItem, HandleDelNewsItem)
31 srv.HandleFunc(hotline.TranDeleteFile, HandleDeleteFileWithUserFolders)
32 srv.HandleFunc(hotline.TranDeleteUser, HandleDeleteUser)
33 srv.HandleFunc(hotline.TranDisconnectUser, HandleDisconnectUser)
34 srv.HandleFunc(hotline.TranDownloadFile, HandleDownloadFile)
35 srv.HandleFunc(hotline.TranDownloadFldr, HandleDownloadFolder)
36 srv.HandleFunc(hotline.TranGetClientInfoText, HandleGetClientInfoText)
37 srv.HandleFunc(hotline.TranGetFileInfo, HandleGetFileInfo)
38 srv.HandleFunc(hotline.TranGetFileNameList, HandleGetFileNameListWithUserFolders)
39 srv.HandleFunc(hotline.TranGetMsgs, HandleGetMsgs)
40 srv.HandleFunc(hotline.TranGetNewsArtData, HandleGetNewsArtData)
41 srv.HandleFunc(hotline.TranGetNewsArtNameList, HandleGetNewsArtNameList)
42 srv.HandleFunc(hotline.TranGetNewsCatNameList, HandleGetNewsCatNameList)
43 srv.HandleFunc(hotline.TranGetUser, HandleGetUser)
44 srv.HandleFunc(hotline.TranGetUserNameList, HandleGetUserNameList)
45 srv.HandleFunc(hotline.TranInviteNewChat, HandleInviteNewChat)
46 srv.HandleFunc(hotline.TranInviteToChat, HandleInviteToChat)
47 srv.HandleFunc(hotline.TranJoinChat, HandleJoinChat)
48 srv.HandleFunc(hotline.TranKeepAlive, HandleKeepAlive)
49 srv.HandleFunc(hotline.TranLeaveChat, HandleLeaveChat)
50 srv.HandleFunc(hotline.TranListUsers, HandleListUsers)
51 srv.HandleFunc(hotline.TranMoveFile, HandleMoveFile)
52 srv.HandleFunc(hotline.TranNewFolder, HandleNewFolder)
53 srv.HandleFunc(hotline.TranNewNewsCat, HandleNewNewsCat)
54 srv.HandleFunc(hotline.TranNewNewsFldr, HandleNewNewsFldr)
55 srv.HandleFunc(hotline.TranNewUser, HandleNewUser)
56 srv.HandleFunc(hotline.TranUpdateUser, HandleUpdateUser)
57 srv.HandleFunc(hotline.TranOldPostNews, HandleTranOldPostNews)
58 srv.HandleFunc(hotline.TranPostNewsArt, HandlePostNewsArt)
59 srv.HandleFunc(hotline.TranRejectChatInvite, HandleRejectChatInvite)
60 srv.HandleFunc(hotline.TranSendInstantMsg, HandleSendInstantMsg)
61 srv.HandleFunc(hotline.TranSetChatSubject, HandleSetChatSubject)
62 srv.HandleFunc(hotline.TranMakeFileAlias, HandleMakeAlias)
63 srv.HandleFunc(hotline.TranSetClientUserInfo, HandleSetClientUserInfo)
64 srv.HandleFunc(hotline.TranSetFileInfo, HandleSetFileInfo)
65 srv.HandleFunc(hotline.TranSetUser, HandleSetUser)
66 srv.HandleFunc(hotline.TranUploadFile, HandleUploadFileWithUserFolders)
67 srv.HandleFunc(hotline.TranUploadFldr, HandleUploadFolderWithUserFolders)
68 srv.HandleFunc(hotline.TranUserBroadcast, HandleUserBroadcast)
69 srv.HandleFunc(hotline.TranDownloadBanner, HandleDownloadBanner)
72 func HandleChatSend(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
73 if !cc.Authorize(hotline.AccessSendChat) {
74 return cc.NewErrReply(t, "You are not allowed to participate in chat.")
77 // Truncate long usernames
78 // %13.13s: This means a string that is right-aligned in a field of 13 characters.
79 // If the string is longer than 13 characters, it will be truncated to 13 characters.
80 formattedMsg := fmt.Sprintf("\r%13.13s: %s", cc.UserName, t.GetField(hotline.FieldData).Data)
82 // By holding the option key, Hotline chat allows users to send /me formatted messages like:
83 // *** Halcyon does stuff
84 // This is indicated by the presence of the optional field FieldChatOptions set to a value of 1.
85 // Most clients do not send this option for normal chat messages.
86 if t.GetField(hotline.FieldChatOptions).Data != nil && bytes.Equal(t.GetField(hotline.FieldChatOptions).Data, []byte{0, 1}) {
87 formattedMsg = fmt.Sprintf("\r*** %s %s", cc.UserName, t.GetField(hotline.FieldData).Data)
90 // Truncate the message to the limit. This does not handle the edge case of a string ending on multibyte character.
91 formattedMsg = formattedMsg[:min(len(formattedMsg), hotline.LimitChatMsg)]
93 // The ChatID field is used to identify messages as belonging to a private chat.
94 // All clients *except* Frogblast omit this field for public chat, but Frogblast sends a value of 00 00 00 00.
95 chatID := t.GetField(hotline.FieldChatID).Data
96 if chatID != nil && !bytes.Equal([]byte{0, 0, 0, 0}, chatID) {
98 // send the message to all connected clients of the private chat
99 for _, c := range cc.Server.ChatMgr.Members([4]byte(chatID)) {
100 res = append(res, hotline.NewTransaction(
103 hotline.NewField(hotline.FieldChatID, chatID),
104 hotline.NewField(hotline.FieldData, []byte(formattedMsg)),
110 //cc.Server.mux.Lock()
111 for _, c := range cc.Server.ClientMgr.List() {
112 if c == nil || cc.Account == nil {
115 // Skip clients that do not have the read chat permission.
116 if c.Authorize(hotline.AccessReadChat) {
117 res = append(res, hotline.NewTransaction(hotline.TranChatMsg, c.ID, hotline.NewField(hotline.FieldData, []byte(formattedMsg))))
120 //cc.Server.mux.Unlock()
125 // HandleSendInstantMsg sends instant message to the user on the current server.
126 // Fields used in the request:
130 // One of the following values:
131 // - User message (myOpt_UserMessage = 1)
132 // - Refuse message (myOpt_RefuseMessage = 2)
133 // - Refuse chat (myOpt_RefuseChat = 3)
134 // - Automatic response (myOpt_AutomaticResponse = 4)"
136 // 214 Quoting message Optional
138 // Fields used in the reply:
140 func HandleSendInstantMsg(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
141 if !cc.Authorize(hotline.AccessSendPrivMsg) {
142 return cc.NewErrReply(t, "You are not allowed to send private messages.")
145 msg := t.GetField(hotline.FieldData)
146 userID := t.GetField(hotline.FieldUserID)
148 reply := hotline.NewTransaction(
149 hotline.TranServerMsg,
150 [2]byte(userID.Data),
151 hotline.NewField(hotline.FieldData, msg.Data),
152 hotline.NewField(hotline.FieldUserName, cc.UserName),
153 hotline.NewField(hotline.FieldUserID, cc.ID[:]),
154 hotline.NewField(hotline.FieldOptions, []byte{0, 1}),
157 // Later versions of Hotline include the original message in the FieldQuotingMsg field so
158 // the receiving client can display both the received message and what it is in reply to
159 if t.GetField(hotline.FieldQuotingMsg).Data != nil {
160 reply.Fields = append(reply.Fields, hotline.NewField(hotline.FieldQuotingMsg, t.GetField(hotline.FieldQuotingMsg).Data))
163 otherClient := cc.Server.ClientMgr.Get([2]byte(userID.Data))
164 if otherClient == nil {
168 // Check if target user has "Refuse private messages" flag
169 if otherClient.Flags.IsSet(hotline.UserFlagRefusePM) {
171 hotline.NewTransaction(
172 hotline.TranServerMsg,
174 hotline.NewField(hotline.FieldData, []byte(string(otherClient.UserName)+" does not accept private messages.")),
175 hotline.NewField(hotline.FieldUserName, otherClient.UserName),
176 hotline.NewField(hotline.FieldUserID, otherClient.ID[:]),
177 hotline.NewField(hotline.FieldOptions, []byte{0, 2}),
181 res = append(res, reply)
184 // Respond with auto reply if other client has it enabled
185 if len(otherClient.AutoReply) > 0 {
187 hotline.NewTransaction(
188 hotline.TranServerMsg,
190 hotline.NewField(hotline.FieldData, otherClient.AutoReply),
191 hotline.NewField(hotline.FieldUserName, otherClient.UserName),
192 hotline.NewField(hotline.FieldUserID, otherClient.ID[:]),
193 hotline.NewField(hotline.FieldOptions, []byte{0, 1}),
198 return append(res, cc.NewReply(t))
201 var fileTypeFLDR = [4]byte{0x66, 0x6c, 0x64, 0x72}
203 func HandleGetFileInfo(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
204 fileName := t.GetField(hotline.FieldFileName).Data
205 filePath := t.GetField(hotline.FieldFilePath).Data
207 fullFilePath, err := hotline.ReadPath(cc.FileRoot(), filePath, fileName)
212 fw, err := hotline.NewFileWrapper(cc.Server.FS, fullFilePath, 0)
217 encodedName, err := txtEncoder.String(fw.Name)
222 fields := []hotline.Field{
223 hotline.NewField(hotline.FieldFileName, []byte(encodedName)),
224 hotline.NewField(hotline.FieldFileTypeString, fw.Ffo.FlatFileInformationFork.FriendlyType()),
225 hotline.NewField(hotline.FieldFileCreatorString, fw.Ffo.FlatFileInformationFork.FriendlyCreator()),
226 hotline.NewField(hotline.FieldFileType, fw.Ffo.FlatFileInformationFork.TypeSignature[:]),
227 hotline.NewField(hotline.FieldFileCreateDate, fw.Ffo.FlatFileInformationFork.CreateDate[:]),
228 hotline.NewField(hotline.FieldFileModifyDate, fw.Ffo.FlatFileInformationFork.ModifyDate[:]),
231 // Include the optional FileComment field if there is a comment.
232 if len(fw.Ffo.FlatFileInformationFork.Comment) != 0 {
233 fields = append(fields, hotline.NewField(hotline.FieldFileComment, fw.Ffo.FlatFileInformationFork.Comment))
236 // Include the FileSize field for files.
237 if fw.Ffo.FlatFileInformationFork.TypeSignature != fileTypeFLDR {
238 fields = append(fields, hotline.NewField(hotline.FieldFileSize, fw.TotalSize()))
241 res = append(res, cc.NewReply(t, fields...))
245 // HandleSetFileInfo updates a file or folder Name and/or comment from the Get Info window
246 // Fields used in the request:
248 // * 202 File path Optional
249 // * 211 File new Name Optional
250 // * 210 File comment Optional
251 // Fields used in the reply: None
252 func HandleSetFileInfo(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
253 fileName := t.GetField(hotline.FieldFileName).Data
254 filePath := t.GetField(hotline.FieldFilePath).Data
256 fullFilePath, err := hotline.ReadPath(cc.FileRoot(), filePath, fileName)
261 fi, err := cc.Server.FS.Stat(fullFilePath)
266 hlFile, err := hotline.NewFileWrapper(cc.Server.FS, fullFilePath, 0)
270 if t.GetField(hotline.FieldFileComment).Data != nil {
271 switch mode := fi.Mode(); {
273 if !cc.Authorize(hotline.AccessSetFolderComment) {
274 return cc.NewErrReply(t, "You are not allowed to set comments for folders.")
276 case mode.IsRegular():
277 if !cc.Authorize(hotline.AccessSetFileComment) {
278 return cc.NewErrReply(t, "You are not allowed to set comments for files.")
282 if err := hlFile.Ffo.FlatFileInformationFork.SetComment(t.GetField(hotline.FieldFileComment).Data); err != nil {
285 w, err := hlFile.InfoForkWriter()
289 _, err = io.Copy(w, &hlFile.Ffo.FlatFileInformationFork)
295 fullNewFilePath, err := hotline.ReadPath(cc.FileRoot(), filePath, t.GetField(hotline.FieldFileNewName).Data)
300 fileNewName := t.GetField(hotline.FieldFileNewName).Data
302 if fileNewName != nil {
303 switch mode := fi.Mode(); {
305 if !cc.Authorize(hotline.AccessRenameFolder) {
306 return cc.NewErrReply(t, "You are not allowed to rename folders.")
308 err = os.Rename(fullFilePath, fullNewFilePath)
309 if os.IsNotExist(err) {
310 return cc.NewErrReply(t, "Cannot rename folder "+string(fileName)+" because it does not exist or cannot be found.")
313 case mode.IsRegular():
314 if !cc.Authorize(hotline.AccessRenameFile) {
315 return cc.NewErrReply(t, "You are not allowed to rename files.")
317 fileDir, err := hotline.ReadPath(cc.FileRoot(), filePath, []byte{})
321 hlFile.Name, err = txtDecoder.String(string(fileNewName))
326 err = hlFile.Move(fileDir)
327 if os.IsNotExist(err) {
328 return cc.NewErrReply(t, "Cannot rename file "+string(fileName)+" because it does not exist or cannot be found.")
336 res = append(res, cc.NewReply(t))
340 // HandleDeleteFile deletes a file or folder
341 // Fields used in the request:
344 // Fields used in the reply: none
345 func HandleDeleteFile(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
346 fileName := t.GetField(hotline.FieldFileName).Data
347 filePath := t.GetField(hotline.FieldFilePath).Data
349 var fp hotline.FilePath
351 if _, err := fp.Write(filePath); err != nil {
356 fullFilePath, err := hotline.ReadPath(cc.FileRoot(), filePath, fileName)
361 hlFile, err := hotline.NewFileWrapper(cc.Server.FS, fullFilePath, 0)
366 fi, err := hlFile.DataFile()
368 return cc.NewErrReply(t, "Cannot delete file "+string(fileName)+" because it does not exist or cannot be found.")
371 switch mode := fi.Mode(); {
373 if !cc.Authorize(hotline.AccessDeleteFolder) && !fp.IsUserDir() {
374 return cc.NewErrReply(t, "You are not allowed to delete folders.")
376 case mode.IsRegular():
377 if !cc.Authorize(hotline.AccessDeleteFile) && !fp.IsUserDir() {
378 return cc.NewErrReply(t, "You are not allowed to delete files.")
382 if err := hlFile.Delete(); err != nil {
386 res = append(res, cc.NewReply(t))
390 // HandleMoveFile moves files or folders. Note: seemingly not documented
391 func HandleMoveFile(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
392 fileName := string(t.GetField(hotline.FieldFileName).Data)
394 filePath, err := hotline.ReadPath(cc.FileRoot(), t.GetField(hotline.FieldFilePath).Data, t.GetField(hotline.FieldFileName).Data)
399 fileNewPath, err := hotline.ReadPath(cc.FileRoot(), t.GetField(hotline.FieldFileNewPath).Data, nil)
404 cc.Logger.Info("Move file", "src", filePath+"/"+fileName, "dst", fileNewPath+"/"+fileName)
406 hlFile, err := hotline.NewFileWrapper(cc.Server.FS, filePath, 0)
411 fi, err := hlFile.DataFile()
413 return cc.NewErrReply(t, "Cannot delete file "+fileName+" because it does not exist or cannot be found.")
415 switch mode := fi.Mode(); {
417 if !cc.Authorize(hotline.AccessMoveFolder) {
418 return cc.NewErrReply(t, "You are not allowed to move folders.")
420 case mode.IsRegular():
421 if !cc.Authorize(hotline.AccessMoveFile) {
422 return cc.NewErrReply(t, "You are not allowed to move files.")
425 if err := hlFile.Move(fileNewPath); err != nil {
428 // TODO: handle other possible errors; e.g. file delete fails due to permission issue
430 res = append(res, cc.NewReply(t))
434 func HandleNewFolder(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
435 if !cc.Authorize(hotline.AccessCreateFolder) {
436 return cc.NewErrReply(t, "You are not allowed to create folders.")
438 folderName := string(t.GetField(hotline.FieldFileName).Data)
440 folderName = path.Join("/", folderName)
444 // FieldFilePath is only present for nested paths
445 if t.GetField(hotline.FieldFilePath).Data != nil {
446 var newFp hotline.FilePath
447 _, err := newFp.Write(t.GetField(hotline.FieldFilePath).Data)
452 for _, pathItem := range newFp.Items {
453 subPath = filepath.Join("/", subPath, string(pathItem.Name))
456 newFolderPath := path.Join(cc.FileRoot(), subPath, folderName)
457 newFolderPath, err := txtDecoder.String(newFolderPath)
462 // TODO: check path and folder Name lengths
464 if _, err := cc.Server.FS.Stat(newFolderPath); !os.IsNotExist(err) {
465 msg := fmt.Sprintf("Cannot create folder \"%s\" because there is already a file or folder with that Name.", folderName)
466 return cc.NewErrReply(t, msg)
469 if err := cc.Server.FS.Mkdir(newFolderPath, 0777); err != nil {
470 msg := fmt.Sprintf("Cannot create folder \"%s\" because an error occurred.", folderName)
471 return cc.NewErrReply(t, msg)
474 return append(res, cc.NewReply(t))
477 func HandleSetUser(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
478 if !cc.Authorize(hotline.AccessModifyUser) {
479 return cc.NewErrReply(t, "You are not allowed to modify accounts.")
482 login := t.GetField(hotline.FieldUserLogin).DecodeObfuscatedString()
483 userName := string(t.GetField(hotline.FieldUserName).Data)
485 newAccessLvl := t.GetField(hotline.FieldUserAccess).Data
487 account := cc.Server.AccountManager.Get(login)
489 return cc.NewErrReply(t, "Account not found.")
491 account.Name = userName
492 copy(account.Access[:], newAccessLvl)
494 // If the password field is cleared in the Hotline edit user UI, the SetUser transaction does
495 // not include FieldUserPassword
496 if t.GetField(hotline.FieldUserPassword).Data == nil {
497 account.Password = hotline.HashAndSalt([]byte(""))
500 if !bytes.Equal([]byte{0}, t.GetField(hotline.FieldUserPassword).Data) {
501 account.Password = hotline.HashAndSalt(t.GetField(hotline.FieldUserPassword).Data)
504 err := cc.Server.AccountManager.Update(*account, account.Login)
506 cc.Logger.Error("Error updating account", "Err", err)
509 // Notify connected clients logged in as the user of the new access level
510 for _, c := range cc.Server.ClientMgr.List() {
511 if c.Account.Login == login {
512 newT := hotline.NewTransaction(hotline.TranUserAccess, c.ID, hotline.NewField(hotline.FieldUserAccess, newAccessLvl))
513 res = append(res, newT)
515 if c.Authorize(hotline.AccessDisconUser) {
516 c.Flags.Set(hotline.UserFlagAdmin, 1)
518 c.Flags.Set(hotline.UserFlagAdmin, 0)
521 c.Account.Access = account.Access
524 hotline.TranNotifyChangeUser,
525 hotline.NewField(hotline.FieldUserID, c.ID[:]),
526 hotline.NewField(hotline.FieldUserFlags, c.Flags[:]),
527 hotline.NewField(hotline.FieldUserName, c.UserName),
528 hotline.NewField(hotline.FieldUserIconID, c.Icon),
533 return append(res, cc.NewReply(t))
536 func HandleGetUser(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
537 if !cc.Authorize(hotline.AccessOpenUser) {
538 return cc.NewErrReply(t, "You are not allowed to view accounts.")
541 account := cc.Server.AccountManager.Get(string(t.GetField(hotline.FieldUserLogin).Data))
543 return cc.NewErrReply(t, "Account does not exist.")
546 return append(res, cc.NewReply(t,
547 hotline.NewField(hotline.FieldUserName, []byte(account.Name)),
548 hotline.NewField(hotline.FieldUserLogin, hotline.EncodeString(t.GetField(hotline.FieldUserLogin).Data)),
549 hotline.NewField(hotline.FieldUserPassword, []byte(account.Password)),
550 hotline.NewField(hotline.FieldUserAccess, account.Access[:]),
554 func HandleListUsers(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
555 if !cc.Authorize(hotline.AccessOpenUser) {
556 return cc.NewErrReply(t, "You are not allowed to view accounts.")
559 var userFields []hotline.Field
560 for _, acc := range cc.Server.AccountManager.List() {
561 b, err := io.ReadAll(&acc)
563 cc.Logger.Error("Error reading account", "Account", acc.Login, "Err", err)
567 userFields = append(userFields, hotline.NewField(hotline.FieldData, b))
570 return append(res, cc.NewReply(t, userFields...))
573 // HandleUpdateUser is used by the v1.5+ multi-user editor to perform account editing for multiple users at a time.
574 // An update can be a mix of these actions:
577 // * Modify user (including renaming the account login)
579 // The Transaction sent by the client includes one data field per user that was modified. This data field in turn
580 // contains another data field encoded in its payload with a varying number of sub fields depending on which action is
581 // performed. This seems to be the only place in the Hotline protocol where a data field contains another data field.
582 func HandleUpdateUser(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
583 for _, field := range t.Fields {
584 var subFields []hotline.Field
586 // Create a new scanner for parsing incoming bytes into transaction tokens
587 scanner := bufio.NewScanner(bytes.NewReader(field.Data[2:]))
588 scanner.Split(hotline.FieldScanner)
590 for i := 0; i < int(binary.BigEndian.Uint16(field.Data[0:2])); i++ {
593 var field hotline.Field
594 if _, err := field.Write(scanner.Bytes()); err != nil {
597 subFields = append(subFields, field)
600 // If there's only one subfield, that indicates this is a delete operation for the login in FieldData
601 if len(subFields) == 1 {
602 if !cc.Authorize(hotline.AccessDeleteUser) {
603 return cc.NewErrReply(t, "You are not allowed to delete accounts.")
606 login := string(hotline.EncodeString(hotline.GetField(hotline.FieldData, &subFields).Data))
608 cc.Logger.Info("DeleteUser", "login", login)
610 if err := cc.Server.AccountManager.Delete(login); err != nil {
611 cc.Logger.Error("Error deleting account", "Err", err)
615 for _, client := range cc.Server.ClientMgr.List() {
616 if client.Account.Login == login {
617 // "You are logged in with an account which was deleted."
620 hotline.NewTransaction(hotline.TranServerMsg, [2]byte{},
621 hotline.NewField(hotline.FieldData, []byte("You are logged in with an account which was deleted.")),
622 hotline.NewField(hotline.FieldChatOptions, []byte{0}),
626 go func(c *hotline.ClientConn) {
627 time.Sleep(3 * time.Second)
636 // login of the account to update
637 var accountToUpdate, loginToRename string
639 // If FieldData is included, this is a rename operation where FieldData contains the login of the existing
640 // account and FieldUserLogin contains the new login.
641 if hotline.GetField(hotline.FieldData, &subFields) != nil {
642 loginToRename = string(hotline.EncodeString(hotline.GetField(hotline.FieldData, &subFields).Data))
644 userLogin := string(hotline.EncodeString(hotline.GetField(hotline.FieldUserLogin, &subFields).Data))
645 if loginToRename != "" {
646 accountToUpdate = loginToRename
648 accountToUpdate = userLogin
651 // Check if accountToUpdate has an existing account. If so, we know we are updating an existing user.
652 if acc := cc.Server.AccountManager.Get(accountToUpdate); acc != nil {
653 if loginToRename != "" {
654 cc.Logger.Info("RenameUser", "prevLogin", accountToUpdate, "newLogin", userLogin)
656 cc.Logger.Info("UpdateUser", "login", accountToUpdate)
659 // Account exists, so this is an update action.
660 if !cc.Authorize(hotline.AccessModifyUser) {
661 return cc.NewErrReply(t, "You are not allowed to modify accounts.")
664 // This part is a bit tricky. There are three possibilities:
665 // 1) The transaction is intended to update the password.
666 // In this case, FieldUserPassword is sent with the new password.
667 // 2) The transaction is intended to remove the password.
668 // In this case, FieldUserPassword is not sent.
669 // 3) The transaction updates the users access bits, but not the password.
670 // In this case, FieldUserPassword is sent with zero as the only byte.
671 if hotline.GetField(hotline.FieldUserPassword, &subFields) != nil {
672 newPass := hotline.GetField(hotline.FieldUserPassword, &subFields).Data
673 if !bytes.Equal([]byte{0}, newPass) {
674 acc.Password = hotline.HashAndSalt(newPass)
677 acc.Password = hotline.HashAndSalt([]byte(""))
680 if hotline.GetField(hotline.FieldUserAccess, &subFields) != nil {
681 copy(acc.Access[:], hotline.GetField(hotline.FieldUserAccess, &subFields).Data)
684 acc.Name = string(hotline.GetField(hotline.FieldUserName, &subFields).Data)
686 err := cc.Server.AccountManager.Update(*acc, string(hotline.EncodeString(hotline.GetField(hotline.FieldUserLogin, &subFields).Data)))
692 if !cc.Authorize(hotline.AccessCreateUser) {
693 return cc.NewErrReply(t, "You are not allowed to create new accounts.")
696 cc.Logger.Info("CreateUser", "login", userLogin)
698 var newAccess hotline.AccessBitmap
699 copy(newAccess[:], hotline.GetField(hotline.FieldUserAccess, &subFields).Data)
701 // Prevent account from creating new account with greater permission
702 for i := 0; i < 64; i++ {
703 if newAccess.IsSet(i) {
704 if !cc.Authorize(i) {
705 return cc.NewErrReply(t, "Cannot create account with more access than yourself.")
710 account := hotline.NewAccount(
712 string(hotline.GetField(hotline.FieldUserName, &subFields).Data),
713 string(hotline.GetField(hotline.FieldUserPassword, &subFields).Data),
717 err := cc.Server.AccountManager.Create(*account)
719 return cc.NewErrReply(t, "Cannot create account because there is already an account with that login.")
724 return append(res, cc.NewReply(t))
727 // HandleNewUser creates a new user account
728 func HandleNewUser(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
729 if !cc.Authorize(hotline.AccessCreateUser) {
730 return cc.NewErrReply(t, "You are not allowed to create new accounts.")
733 login := t.GetField(hotline.FieldUserLogin).DecodeObfuscatedString()
735 // If the account already exists, reply with an error.
736 if account := cc.Server.AccountManager.Get(login); account != nil {
737 return cc.NewErrReply(t, "Cannot create account "+login+" because there is already an account with that login.")
740 var newAccess hotline.AccessBitmap
741 copy(newAccess[:], t.GetField(hotline.FieldUserAccess).Data)
743 // Prevent account from creating new account with greater permission
744 for i := 0; i < 64; i++ {
745 if newAccess.IsSet(i) {
746 if !cc.Authorize(i) {
747 return cc.NewErrReply(t, "Cannot create account with more access than yourself.")
752 account := hotline.NewAccount(login, string(t.GetField(hotline.FieldUserName).Data), string(t.GetField(hotline.FieldUserPassword).Data), newAccess)
754 err := cc.Server.AccountManager.Create(*account)
756 return cc.NewErrReply(t, "Cannot create account because there is already an account with that login.")
759 return append(res, cc.NewReply(t))
762 func HandleDeleteUser(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
763 if !cc.Authorize(hotline.AccessDeleteUser) {
764 return cc.NewErrReply(t, "You are not allowed to delete accounts.")
767 login := t.GetField(hotline.FieldUserLogin).DecodeObfuscatedString()
769 if err := cc.Server.AccountManager.Delete(login); err != nil {
770 cc.Logger.Error("Error deleting account", "Err", err)
774 for _, client := range cc.Server.ClientMgr.List() {
775 if client.Account.Login == login {
777 hotline.NewTransaction(hotline.TranServerMsg, client.ID,
778 hotline.NewField(hotline.FieldData, []byte("You are logged in with an account which was deleted.")),
779 hotline.NewField(hotline.FieldChatOptions, []byte{2}),
783 go func(c *hotline.ClientConn) {
784 time.Sleep(2 * time.Second)
790 return append(res, cc.NewReply(t))
793 // HandleUserBroadcast sends an Administrator Message to all connected clients of the server
794 func HandleUserBroadcast(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
795 if !cc.Authorize(hotline.AccessBroadcast) {
796 return cc.NewErrReply(t, "You are not allowed to send broadcast messages.")
800 hotline.TranServerMsg,
801 hotline.NewField(hotline.FieldData, t.GetField(hotline.FieldData).Data),
802 hotline.NewField(hotline.FieldChatOptions, []byte{0}),
805 return append(res, cc.NewReply(t))
808 // HandleGetClientInfoText returns user information for the specific user.
810 // Fields used in the request:
813 // Fields used in the reply:
815 // 101 Data User info text string
816 func HandleGetClientInfoText(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
817 if !cc.Authorize(hotline.AccessGetClientInfo) {
818 return cc.NewErrReply(t, "You are not allowed to get client info.")
821 clientID := t.GetField(hotline.FieldUserID).Data
823 clientConn := cc.Server.ClientMgr.Get(hotline.ClientID(clientID))
824 if clientConn == nil {
825 return cc.NewErrReply(t, "User not found.")
828 return append(res, cc.NewReply(t,
829 hotline.NewField(hotline.FieldData, []byte(clientConn.String())),
830 hotline.NewField(hotline.FieldUserName, clientConn.UserName),
834 func HandleGetUserNameList(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
835 var fields []hotline.Field
836 for _, c := range cc.Server.ClientMgr.List() {
837 b, err := io.ReadAll(&hotline.User{
841 Name: string(c.UserName),
847 fields = append(fields, hotline.NewField(hotline.FieldUsernameWithInfo, b))
850 return []hotline.Transaction{cc.NewReply(t, fields...)}
853 func HandleTranAgreed(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
854 if t.GetField(hotline.FieldUserName).Data != nil {
855 if cc.Authorize(hotline.AccessAnyName) {
856 cc.UserName = t.GetField(hotline.FieldUserName).Data
858 cc.UserName = []byte(cc.Account.Name)
862 cc.Icon = t.GetField(hotline.FieldUserIconID).Data
864 cc.Logger = cc.Logger.With("Name", string(cc.UserName))
865 cc.Logger.Info("Login successful")
867 options := t.GetField(hotline.FieldOptions).Data
868 optBitmap := big.NewInt(int64(binary.BigEndian.Uint16(options)))
870 // Check refuse private PM option
873 defer cc.FlagsMU.Unlock()
874 cc.Flags.Set(hotline.UserFlagRefusePM, optBitmap.Bit(hotline.UserOptRefusePM))
876 // Check refuse private chat option
877 cc.Flags.Set(hotline.UserFlagRefusePChat, optBitmap.Bit(hotline.UserOptRefuseChat))
879 // Check auto response
880 if optBitmap.Bit(hotline.UserOptAutoResponse) == 1 {
881 cc.AutoReply = t.GetField(hotline.FieldAutomaticResponse).Data
884 trans := cc.NotifyOthers(
885 hotline.NewTransaction(
886 hotline.TranNotifyChangeUser, [2]byte{0, 0},
887 hotline.NewField(hotline.FieldUserName, cc.UserName),
888 hotline.NewField(hotline.FieldUserID, cc.ID[:]),
889 hotline.NewField(hotline.FieldUserIconID, cc.Icon),
890 hotline.NewField(hotline.FieldUserFlags, cc.Flags[:]),
893 res = append(res, trans...)
895 if cc.Server.Config.BannerFile != "" {
896 res = append(res, hotline.NewTransaction(hotline.TranServerBanner, cc.ID, hotline.NewField(hotline.FieldBannerType, []byte("JPEG"))))
899 res = append(res, cc.NewReply(t))
904 // HandleTranOldPostNews updates the flat news
905 // Fields used in this request:
907 func HandleTranOldPostNews(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
908 if !cc.Authorize(hotline.AccessNewsPostArt) {
909 return cc.NewErrReply(t, "You are not allowed to post news.")
912 newsDateTemplate := hotline.NewsDateFormat
913 if cc.Server.Config.NewsDateFormat != "" {
914 newsDateTemplate = cc.Server.Config.NewsDateFormat
917 newsTemplate := hotline.NewsTemplate
918 if cc.Server.Config.NewsDelimiter != "" {
919 newsTemplate = cc.Server.Config.NewsDelimiter
922 newsPost := fmt.Sprintf(newsTemplate+"\r", cc.UserName, time.Now().Format(newsDateTemplate), t.GetField(hotline.FieldData).Data)
923 newsPost = strings.ReplaceAll(newsPost, "\n", "\r")
925 _, err := cc.Server.MessageBoard.Write([]byte(newsPost))
927 cc.Logger.Error("error writing news post", "err", err)
931 // Notify all clients of updated news
934 hotline.NewField(hotline.FieldData, []byte(newsPost)),
937 return append(res, cc.NewReply(t))
940 func HandleDisconnectUser(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
941 if !cc.Authorize(hotline.AccessDisconUser) {
942 return cc.NewErrReply(t, "You are not allowed to disconnect users.")
945 clientID := [2]byte(t.GetField(hotline.FieldUserID).Data)
946 clientConn := cc.Server.ClientMgr.Get(clientID)
948 if clientConn.Authorize(hotline.AccessCannotBeDiscon) {
949 return cc.NewErrReply(t, clientConn.Account.Login+" is not allowed to be disconnected.")
952 // If FieldOptions is set, then the client IP is banned in addition to disconnected.
953 // 00 01 = temporary ban
954 // 00 02 = permanent ban
955 if t.GetField(hotline.FieldOptions).Data != nil {
956 switch t.GetField(hotline.FieldOptions).Data[1] {
958 // send message: "You are temporarily banned on this server"
959 cc.Logger.Info("Disconnect & temporarily ban " + string(clientConn.UserName))
961 res = append(res, hotline.NewTransaction(
962 hotline.TranServerMsg,
964 hotline.NewField(hotline.FieldData, []byte("You are temporarily banned on this server")),
965 hotline.NewField(hotline.FieldChatOptions, []byte{0, 0}),
968 banUntil := time.Now().Add(hotline.BanDuration)
969 ip := strings.Split(clientConn.RemoteAddr, ":")[0]
971 err := cc.Server.BanList.Add(ip, &banUntil)
973 cc.Logger.Error("Error saving ban", "err", err)
977 // send message: "You are permanently banned on this server"
978 cc.Logger.Info("Disconnect & ban " + string(clientConn.UserName))
980 res = append(res, hotline.NewTransaction(
981 hotline.TranServerMsg,
983 hotline.NewField(hotline.FieldData, []byte("You are permanently banned on this server")),
984 hotline.NewField(hotline.FieldChatOptions, []byte{0, 0}),
987 ip := strings.Split(clientConn.RemoteAddr, ":")[0]
989 err := cc.Server.BanList.Add(ip, nil)
991 cc.Logger.Error("Error saving ban", "err", err)
997 time.Sleep(1 * time.Second)
998 clientConn.Disconnect()
1001 return append(res, cc.NewReply(t))
1004 // HandleGetNewsCatNameList returns a list of news categories for a path
1005 // Fields used in the request:
1006 // 325 News path (Optional)
1007 func HandleGetNewsCatNameList(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1008 if !cc.Authorize(hotline.AccessNewsReadArt) {
1009 return cc.NewErrReply(t, "You are not allowed to read news.")
1012 pathStrs, err := t.GetField(hotline.FieldNewsPath).DecodeNewsPath()
1014 cc.Logger.Error("get news path", "err", err)
1018 var fields []hotline.Field
1019 for _, cat := range cc.Server.ThreadedNewsMgr.GetCategories(pathStrs) {
1020 b, err := io.ReadAll(&cat)
1022 cc.Logger.Error("get news categories", "err", err)
1025 fields = append(fields, hotline.NewField(hotline.FieldNewsCatListData15, b))
1028 return append(res, cc.NewReply(t, fields...))
1031 func HandleNewNewsCat(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1032 if !cc.Authorize(hotline.AccessNewsCreateCat) {
1033 return cc.NewErrReply(t, "You are not allowed to create news categories.")
1036 name := string(t.GetField(hotline.FieldNewsCatName).Data)
1037 pathStrs, err := t.GetField(hotline.FieldNewsPath).DecodeNewsPath()
1042 err = cc.Server.ThreadedNewsMgr.CreateGrouping(pathStrs, name, hotline.NewsCategory)
1044 cc.Logger.Error("error creating news category", "err", err)
1047 return []hotline.Transaction{cc.NewReply(t)}
1050 // Fields used in the request:
1051 // 322 News category Name
1053 func HandleNewNewsFldr(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1054 if !cc.Authorize(hotline.AccessNewsCreateFldr) {
1055 return cc.NewErrReply(t, "You are not allowed to create news folders.")
1058 name := string(t.GetField(hotline.FieldFileName).Data)
1059 pathStrs, err := t.GetField(hotline.FieldNewsPath).DecodeNewsPath()
1064 err = cc.Server.ThreadedNewsMgr.CreateGrouping(pathStrs, name, hotline.NewsBundle)
1066 cc.Logger.Error("error creating news bundle", "err", err)
1069 return append(res, cc.NewReply(t))
1072 // HandleGetNewsArtData gets the list of article names at the specified news path.
1074 // Fields used in the request:
1075 // 325 News path Optional
1077 // Fields used in the reply:
1078 // 321 News article list data Optional
1079 func HandleGetNewsArtNameList(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1080 if !cc.Authorize(hotline.AccessNewsReadArt) {
1081 return cc.NewErrReply(t, "You are not allowed to read news.")
1084 pathStrs, err := t.GetField(hotline.FieldNewsPath).DecodeNewsPath()
1089 nald := cc.Server.ThreadedNewsMgr.ListArticles(pathStrs)
1091 b, err := io.ReadAll(&nald)
1096 return append(res, cc.NewReply(t, hotline.NewField(hotline.FieldNewsArtListData, b)))
1099 // HandleGetNewsArtData requests information about the specific news article.
1100 // Fields used in the request:
1104 // 326 News article Type
1105 // 327 News article data flavor
1107 // Fields used in the reply:
1108 // 328 News article title
1109 // 329 News article poster
1110 // 330 News article date
1111 // 331 Previous article Type
1112 // 332 Next article Type
1113 // 335 Parent article Type
1114 // 336 First child article Type
1115 // 327 News article data flavor "Should be “text/plain”
1116 // 333 News article data Optional (if data flavor is “text/plain”)
1117 func HandleGetNewsArtData(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1118 if !cc.Authorize(hotline.AccessNewsReadArt) {
1119 return cc.NewErrReply(t, "You are not allowed to read news.")
1122 newsPath, err := t.GetField(hotline.FieldNewsPath).DecodeNewsPath()
1127 convertedID, err := t.GetField(hotline.FieldNewsArtID).DecodeInt()
1132 art := cc.Server.ThreadedNewsMgr.GetArticle(newsPath, uint32(convertedID))
1134 return append(res, cc.NewReply(t))
1137 res = append(res, cc.NewReply(t,
1138 hotline.NewField(hotline.FieldNewsArtTitle, []byte(art.Title)),
1139 hotline.NewField(hotline.FieldNewsArtPoster, []byte(art.Poster)),
1140 hotline.NewField(hotline.FieldNewsArtDate, art.Date[:]),
1141 hotline.NewField(hotline.FieldNewsArtPrevArt, art.PrevArt[:]),
1142 hotline.NewField(hotline.FieldNewsArtNextArt, art.NextArt[:]),
1143 hotline.NewField(hotline.FieldNewsArtParentArt, art.ParentArt[:]),
1144 hotline.NewField(hotline.FieldNewsArt1stChildArt, art.FirstChildArt[:]),
1145 hotline.NewField(hotline.FieldNewsArtDataFlav, []byte("text/plain")),
1146 hotline.NewField(hotline.FieldNewsArtData, []byte(art.Data)),
1151 // HandleDelNewsItem deletes a threaded news folder or category.
1152 // Fields used in the request:
1154 // Fields used in the reply:
1156 func HandleDelNewsItem(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1157 pathStrs, err := t.GetField(hotline.FieldNewsPath).DecodeNewsPath()
1158 if err != nil || len(pathStrs) == 0 {
1159 cc.Logger.Error("invalid news path")
1163 item := cc.Server.ThreadedNewsMgr.NewsItem(pathStrs)
1165 if item.Type == [2]byte{0, 3} {
1166 if !cc.Authorize(hotline.AccessNewsDeleteCat) {
1167 return cc.NewErrReply(t, "You are not allowed to delete news categories.")
1170 if !cc.Authorize(hotline.AccessNewsDeleteFldr) {
1171 return cc.NewErrReply(t, "You are not allowed to delete news folders.")
1175 err = cc.Server.ThreadedNewsMgr.DeleteNewsItem(pathStrs)
1180 return append(res, cc.NewReply(t))
1183 // HandleDelNewsArt deletes a threaded news article.
1186 // 326 News article Type
1187 // 337 News article recursive delete - Delete child articles (1) or not (0)
1188 func HandleDelNewsArt(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1189 if !cc.Authorize(hotline.AccessNewsDeleteArt) {
1190 return cc.NewErrReply(t, "You are not allowed to delete news articles.")
1194 pathStrs, err := t.GetField(hotline.FieldNewsPath).DecodeNewsPath()
1199 articleID, err := t.GetField(hotline.FieldNewsArtID).DecodeInt()
1201 cc.Logger.Error("error reading article Type", "err", err)
1205 deleteRecursive := bytes.Equal([]byte{0, 1}, t.GetField(hotline.FieldNewsArtRecurseDel).Data)
1207 err = cc.Server.ThreadedNewsMgr.DeleteArticle(pathStrs, uint32(articleID), deleteRecursive)
1209 cc.Logger.Error("error deleting news article", "err", err)
1212 return []hotline.Transaction{cc.NewReply(t)}
1217 // 326 News article Type Type of the parent article?
1218 // 328 News article title
1219 // 334 News article flags
1220 // 327 News article data flavor Currently “text/plain”
1221 // 333 News article data
1222 func HandlePostNewsArt(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1223 if !cc.Authorize(hotline.AccessNewsPostArt) {
1224 return cc.NewErrReply(t, "You are not allowed to post news articles.")
1227 pathStrs, err := t.GetField(hotline.FieldNewsPath).DecodeNewsPath()
1228 if err != nil || len(pathStrs) == 0 {
1229 cc.Logger.Error("invalid news path")
1233 parentArticleID, err := t.GetField(hotline.FieldNewsArtID).DecodeInt()
1238 err = cc.Server.ThreadedNewsMgr.PostArticle(
1240 uint32(parentArticleID),
1241 hotline.NewsArtData{
1242 Title: string(t.GetField(hotline.FieldNewsArtTitle).Data),
1243 Poster: string(cc.UserName),
1244 Date: hotline.NewTime(time.Now()),
1245 DataFlav: hotline.NewsFlavor,
1246 Data: string(t.GetField(hotline.FieldNewsArtData).Data),
1250 cc.Logger.Error("error posting news article", "err", err)
1253 return append(res, cc.NewReply(t))
1256 // HandleGetMsgs returns the flat news data
1257 func HandleGetMsgs(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1258 if !cc.Authorize(hotline.AccessNewsReadArt) {
1259 return cc.NewErrReply(t, "You are not allowed to read news.")
1262 _, _ = cc.Server.MessageBoard.Seek(0, 0)
1264 newsData, err := io.ReadAll(cc.Server.MessageBoard)
1266 cc.Logger.Error("Error reading messageboard", "err", err)
1269 return append(res, cc.NewReply(t, hotline.NewField(hotline.FieldData, newsData)))
1272 func HandleDownloadFile(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1273 if !cc.Authorize(hotline.AccessDownloadFile) {
1274 return cc.NewErrReply(t, "You are not allowed to download files.")
1277 fileName := t.GetField(hotline.FieldFileName).Data
1278 filePath := t.GetField(hotline.FieldFilePath).Data
1279 resumeData := t.GetField(hotline.FieldFileResumeData).Data
1281 var dataOffset int64
1282 var frd hotline.FileResumeData
1283 if resumeData != nil {
1284 if err := frd.UnmarshalBinary(t.GetField(hotline.FieldFileResumeData).Data); err != nil {
1287 // TODO: handle rsrc fork offset
1288 dataOffset = int64(binary.BigEndian.Uint32(frd.ForkInfoList[0].DataSize[:]))
1291 fullFilePath, err := hotline.ReadPath(cc.FileRoot(), filePath, fileName)
1296 hlFile, err := hotline.NewFileWrapper(cc.Server.FS, fullFilePath, dataOffset)
1301 xferSize := hlFile.Ffo.TransferSize(0)
1303 ft := cc.NewFileTransfer(
1304 hotline.FileDownload,
1311 if resumeData != nil {
1312 var frd hotline.FileResumeData
1313 if err := frd.UnmarshalBinary(t.GetField(hotline.FieldFileResumeData).Data); err != nil {
1316 ft.FileResumeData = &frd
1319 // Optional field for when a client requests file preview
1320 // Used only for TEXT, JPEG, GIFF, BMP or PICT files
1321 // The value will always be 2
1322 if t.GetField(hotline.FieldFileTransferOptions).Data != nil {
1323 ft.Options = t.GetField(hotline.FieldFileTransferOptions).Data
1324 xferSize = hlFile.Ffo.FlatFileDataForkHeader.DataSize[:]
1327 res = append(res, cc.NewReply(t,
1328 hotline.NewField(hotline.FieldRefNum, ft.RefNum[:]),
1329 hotline.NewField(hotline.FieldWaitingCount, []byte{0x00, 0x00}), // TODO: Implement waiting count
1330 hotline.NewField(hotline.FieldTransferSize, xferSize),
1331 hotline.NewField(hotline.FieldFileSize, hlFile.Ffo.FlatFileDataForkHeader.DataSize[:]),
1337 // Download all files from the specified folder and sub-folders
1338 func HandleDownloadFolder(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1339 if !cc.Authorize(hotline.AccessDownloadFolder) {
1340 return cc.NewErrReply(t, "You are not allowed to download folders.")
1343 fullFilePath, err := hotline.ReadPath(cc.FileRoot(), t.GetField(hotline.FieldFilePath).Data, t.GetField(hotline.FieldFileName).Data)
1348 transferSize, err := hotline.CalcTotalSize(fullFilePath)
1352 itemCount, err := hotline.CalcItemCount(fullFilePath)
1357 fileTransfer := cc.NewFileTransfer(hotline.FolderDownload, cc.FileRoot(), t.GetField(hotline.FieldFileName).Data, t.GetField(hotline.FieldFilePath).Data, transferSize)
1359 var fp hotline.FilePath
1360 _, err = fp.Write(t.GetField(hotline.FieldFilePath).Data)
1365 res = append(res, cc.NewReply(t,
1366 hotline.NewField(hotline.FieldRefNum, fileTransfer.RefNum[:]),
1367 hotline.NewField(hotline.FieldTransferSize, transferSize),
1368 hotline.NewField(hotline.FieldFolderItemCount, itemCount),
1369 hotline.NewField(hotline.FieldWaitingCount, []byte{0x00, 0x00}), // TODO: Implement waiting count
1374 // Upload all files from the local folder and its subfolders to the specified path on the server
1375 // Fields used in the request
1378 // 108 hotline.Transfer size Total size of all items in the folder
1379 // 220 Folder item count
1380 // 204 File transfer options "Optional Currently set to 1" (TODO: ??)
1381 func HandleUploadFolder(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1382 if !cc.Authorize(hotline.AccessUploadFolder) {
1383 return cc.NewErrReply(t, "You are not allowed to upload folders.")
1386 var fp hotline.FilePath
1387 if t.GetField(hotline.FieldFilePath).Data != nil {
1388 if _, err := fp.Write(t.GetField(hotline.FieldFilePath).Data); err != nil {
1393 // Handle special cases for Upload and Drop Box folders
1394 if !cc.Authorize(hotline.AccessUploadAnywhere) {
1395 if !fp.IsUploadDir() && !fp.IsDropbox() && !fp.IsUserDir() {
1396 return cc.NewErrReply(t, fmt.Sprintf("Cannot accept upload of the folder \"%v\" because you are only allowed to upload to the \"Uploads\" folder.", string(t.GetField(hotline.FieldFileName).Data)))
1400 fileTransfer := cc.NewFileTransfer(hotline.FolderUpload,
1402 t.GetField(hotline.FieldFileName).Data,
1403 t.GetField(hotline.FieldFilePath).Data,
1404 t.GetField(hotline.FieldTransferSize).Data,
1407 fileTransfer.FolderItemCount = t.GetField(hotline.FieldFolderItemCount).Data
1409 return append(res, cc.NewReply(t, hotline.NewField(hotline.FieldRefNum, fileTransfer.RefNum[:])))
1413 // Fields used in the request:
1416 // 204 File transfer options "Optional
1417 // Used only to resume download, currently has value 2"
1418 // 108 File transfer size "Optional used if download is not resumed"
1419 func HandleUploadFile(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1420 if !cc.Authorize(hotline.AccessUploadFile) {
1421 return cc.NewErrReply(t, "You are not allowed to upload files.")
1424 fileName := t.GetField(hotline.FieldFileName).Data
1425 filePath := t.GetField(hotline.FieldFilePath).Data
1426 transferOptions := t.GetField(hotline.FieldFileTransferOptions).Data
1427 transferSize := t.GetField(hotline.FieldTransferSize).Data // not sent for resume
1429 var fp hotline.FilePath
1430 if filePath != nil {
1431 if _, err := fp.Write(filePath); err != nil {
1436 // Handle special cases for Upload and Drop Box folders
1437 if !cc.Authorize(hotline.AccessUploadAnywhere) {
1438 if !fp.IsUploadDir() && !fp.IsDropbox() && !fp.IsUserDir() {
1439 return cc.NewErrReply(t, fmt.Sprintf("Cannot accept upload of the file \"%v\" because you are only allowed to upload to the \"Uploads\" folder.", string(fileName)))
1442 fullFilePath, err := hotline.ReadPath(cc.FileRoot(), filePath, fileName)
1447 if _, err := cc.Server.FS.Stat(fullFilePath); err == nil {
1448 if !fp.IsUserDir() {
1449 return cc.NewErrReply(t, fmt.Sprintf("Cannot accept upload because there is already a file named \"%v\". Try choosing a different Name.", string(fileName)))
1453 ft := cc.NewFileTransfer(hotline.FileUpload, cc.FileRoot(), fileName, filePath, transferSize)
1455 replyT := cc.NewReply(t, hotline.NewField(hotline.FieldRefNum, ft.RefNum[:]))
1457 // client has requested to resume a partially transferred file
1458 if transferOptions != nil {
1459 fileInfo, err := cc.Server.FS.Stat(fullFilePath + hotline.IncompleteFileSuffix)
1464 offset := make([]byte, 4)
1465 binary.BigEndian.PutUint32(offset, uint32(fileInfo.Size()))
1467 fileResumeData := hotline.NewFileResumeData([]hotline.ForkInfoList{
1468 *hotline.NewForkInfoList(offset),
1471 b, _ := fileResumeData.BinaryMarshal()
1473 ft.TransferSize = offset
1475 replyT.Fields = append(replyT.Fields, hotline.NewField(hotline.FieldFileResumeData, b))
1478 res = append(res, replyT)
1482 func HandleSetClientUserInfo(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1483 if len(t.GetField(hotline.FieldUserIconID).Data) == 4 {
1484 cc.Icon = t.GetField(hotline.FieldUserIconID).Data[2:]
1486 cc.Icon = t.GetField(hotline.FieldUserIconID).Data
1488 if cc.Authorize(hotline.AccessAnyName) {
1489 cc.UserName = t.GetField(hotline.FieldUserName).Data
1492 // the options field is only passed by the client versions > 1.2.3.
1493 options := t.GetField(hotline.FieldOptions).Data
1495 optBitmap := big.NewInt(int64(binary.BigEndian.Uint16(options)))
1497 cc.Flags.Set(hotline.UserFlagRefusePM, optBitmap.Bit(hotline.UserOptRefusePM))
1498 cc.Flags.Set(hotline.UserFlagRefusePChat, optBitmap.Bit(hotline.UserOptRefuseChat))
1500 // Check auto response
1501 if optBitmap.Bit(hotline.UserOptAutoResponse) == 1 {
1502 cc.AutoReply = t.GetField(hotline.FieldAutomaticResponse).Data
1504 cc.AutoReply = []byte{}
1508 for _, c := range cc.Server.ClientMgr.List() {
1509 res = append(res, hotline.NewTransaction(
1510 hotline.TranNotifyChangeUser,
1512 hotline.NewField(hotline.FieldUserID, cc.ID[:]),
1513 hotline.NewField(hotline.FieldUserIconID, cc.Icon),
1514 hotline.NewField(hotline.FieldUserFlags, cc.Flags[:]),
1515 hotline.NewField(hotline.FieldUserName, cc.UserName),
1522 // HandleKeepAlive responds to keepalive transactions with an empty reply
1523 // * HL 1.9.2 Client sends keepalive msg every 3 minutes
1524 // * HL 1.2.3 Client doesn't send keepalives
1525 func HandleKeepAlive(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1526 res = append(res, cc.NewReply(t))
1531 func HandleGetFileNameList(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1532 fullPath, err := hotline.ReadPath(
1534 t.GetField(hotline.FieldFilePath).Data,
1541 var fp hotline.FilePath
1542 if t.GetField(hotline.FieldFilePath).Data != nil {
1543 if _, err = fp.Write(t.GetField(hotline.FieldFilePath).Data); err != nil {
1548 // Handle special case for drop box folders
1549 if fp.IsDropbox() && !cc.Authorize(hotline.AccessViewDropBoxes) {
1550 return cc.NewErrReply(t, "You are not allowed to view drop boxes.")
1553 fileNames, err := hotline.GetFileNameList(fullPath, cc.Server.Config.IgnoreFiles)
1558 res = append(res, cc.NewReply(t, fileNames...))
1563 // =================================
1564 // Hotline private chat flow
1565 // =================================
1566 // 1. ClientA sends TranInviteNewChat to server with user Type to invite
1567 // 2. Server creates new ChatID
1568 // 3. Server sends TranInviteToChat to invitee
1569 // 4. Server replies to ClientA with new Chat Type
1571 // A dialog box pops up in the invitee client with options to accept or decline the invitation.
1572 // If Accepted is clicked:
1573 // 1. ClientB sends TranJoinChat with FieldChatID
1575 // HandleInviteNewChat invites users to new private chat
1576 func HandleInviteNewChat(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1577 if !cc.Authorize(hotline.AccessOpenChat) {
1578 return cc.NewErrReply(t, "You are not allowed to request private chat.")
1582 targetID := t.GetField(hotline.FieldUserID).Data
1584 // Create a new chat with self as initial member.
1585 newChatID := cc.Server.ChatMgr.New(cc)
1587 // Check if target user has "Refuse private chat" flag
1588 targetClient := cc.Server.ClientMgr.Get([2]byte(targetID))
1589 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(targetClient.Flags[:])))
1590 if flagBitmap.Bit(hotline.UserFlagRefusePChat) == 1 {
1592 hotline.NewTransaction(
1593 hotline.TranServerMsg,
1595 hotline.NewField(hotline.FieldData, []byte(string(targetClient.UserName)+" does not accept private chats.")),
1596 hotline.NewField(hotline.FieldUserName, targetClient.UserName),
1597 hotline.NewField(hotline.FieldUserID, targetClient.ID[:]),
1598 hotline.NewField(hotline.FieldOptions, []byte{0, 2}),
1603 hotline.NewTransaction(
1604 hotline.TranInviteToChat,
1606 hotline.NewField(hotline.FieldChatID, newChatID[:]),
1607 hotline.NewField(hotline.FieldUserName, cc.UserName),
1608 hotline.NewField(hotline.FieldUserID, cc.ID[:]),
1616 hotline.NewField(hotline.FieldChatID, newChatID[:]),
1617 hotline.NewField(hotline.FieldUserName, cc.UserName),
1618 hotline.NewField(hotline.FieldUserID, cc.ID[:]),
1619 hotline.NewField(hotline.FieldUserIconID, cc.Icon),
1620 hotline.NewField(hotline.FieldUserFlags, cc.Flags[:]),
1625 func HandleInviteToChat(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1626 if !cc.Authorize(hotline.AccessOpenChat) {
1627 return cc.NewErrReply(t, "You are not allowed to request private chat.")
1631 targetID := t.GetField(hotline.FieldUserID).Data
1632 chatID := t.GetField(hotline.FieldChatID).Data
1634 return []hotline.Transaction{
1635 hotline.NewTransaction(
1636 hotline.TranInviteToChat,
1638 hotline.NewField(hotline.FieldChatID, chatID),
1639 hotline.NewField(hotline.FieldUserName, cc.UserName),
1640 hotline.NewField(hotline.FieldUserID, cc.ID[:]),
1644 hotline.NewField(hotline.FieldChatID, chatID),
1645 hotline.NewField(hotline.FieldUserName, cc.UserName),
1646 hotline.NewField(hotline.FieldUserID, cc.ID[:]),
1647 hotline.NewField(hotline.FieldUserIconID, cc.Icon),
1648 hotline.NewField(hotline.FieldUserFlags, cc.Flags[:]),
1653 func HandleRejectChatInvite(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1654 chatID := [4]byte(t.GetField(hotline.FieldChatID).Data)
1656 for _, c := range cc.Server.ChatMgr.Members(chatID) {
1658 hotline.NewTransaction(
1659 hotline.TranChatMsg,
1661 hotline.NewField(hotline.FieldChatID, chatID[:]),
1662 hotline.NewField(hotline.FieldData, append(cc.UserName, []byte(" declined invitation to chat")...)),
1670 // HandleJoinChat is sent from a v1.8+ Hotline client when the joins a private chat
1671 // Fields used in the reply:
1672 // * 115 Chat subject
1673 // * 300 User Name with info (Optional)
1674 // * 300 (more user names with info)
1675 func HandleJoinChat(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1676 chatID := t.GetField(hotline.FieldChatID).Data
1678 // Send TranNotifyChatChangeUser to current members of the chat to inform of new user
1679 for _, c := range cc.Server.ChatMgr.Members([4]byte(chatID)) {
1681 hotline.NewTransaction(
1682 hotline.TranNotifyChatChangeUser,
1684 hotline.NewField(hotline.FieldChatID, chatID),
1685 hotline.NewField(hotline.FieldUserName, cc.UserName),
1686 hotline.NewField(hotline.FieldUserID, cc.ID[:]),
1687 hotline.NewField(hotline.FieldUserIconID, cc.Icon),
1688 hotline.NewField(hotline.FieldUserFlags, cc.Flags[:]),
1693 cc.Server.ChatMgr.Join(hotline.ChatID(chatID), cc)
1695 subject := cc.Server.ChatMgr.GetSubject(hotline.ChatID(chatID))
1697 replyFields := []hotline.Field{hotline.NewField(hotline.FieldChatSubject, []byte(subject))}
1698 for _, c := range cc.Server.ChatMgr.Members([4]byte(chatID)) {
1699 b, err := io.ReadAll(&hotline.User{
1703 Name: string(c.UserName),
1708 replyFields = append(replyFields, hotline.NewField(hotline.FieldUsernameWithInfo, b))
1711 return append(res, cc.NewReply(t, replyFields...))
1714 // HandleLeaveChat is sent from a v1.8+ Hotline client when the user exits a private chat
1715 // Fields used in the request:
1716 // - 114 FieldChatID
1718 // Reply is not expected.
1719 func HandleLeaveChat(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1720 chatID := t.GetField(hotline.FieldChatID).Data
1722 cc.Server.ChatMgr.Leave([4]byte(chatID), cc.ID)
1724 // Notify members of the private chat that the user has left
1725 for _, c := range cc.Server.ChatMgr.Members(hotline.ChatID(chatID)) {
1727 hotline.NewTransaction(
1728 hotline.TranNotifyChatDeleteUser,
1730 hotline.NewField(hotline.FieldChatID, chatID),
1731 hotline.NewField(hotline.FieldUserID, cc.ID[:]),
1739 // HandleSetChatSubject is sent from a v1.8+ Hotline client when the user sets a private chat subject
1740 // Fields used in the request:
1742 // * 115 Chat subject
1743 // Reply is not expected.
1744 func HandleSetChatSubject(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1745 chatID := t.GetField(hotline.FieldChatID).Data
1747 cc.Server.ChatMgr.SetSubject([4]byte(chatID), string(t.GetField(hotline.FieldChatSubject).Data))
1749 // Notify chat members of new subject.
1750 for _, c := range cc.Server.ChatMgr.Members([4]byte(chatID)) {
1752 hotline.NewTransaction(
1753 hotline.TranNotifyChatSubject,
1755 hotline.NewField(hotline.FieldChatID, chatID),
1756 hotline.NewField(hotline.FieldChatSubject, t.GetField(hotline.FieldChatSubject).Data),
1764 // HandleMakeAlias makes a file alias using the specified path.
1765 // Fields used in the request:
1768 // 212 File new path Destination path
1770 // Fields used in the reply:
1772 func HandleMakeAlias(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1773 if !cc.Authorize(hotline.AccessMakeAlias) {
1774 return cc.NewErrReply(t, "You are not allowed to make aliases.")
1776 fileName := t.GetField(hotline.FieldFileName).Data
1777 filePath := t.GetField(hotline.FieldFilePath).Data
1778 fileNewPath := t.GetField(hotline.FieldFileNewPath).Data
1780 fullFilePath, err := hotline.ReadPath(cc.FileRoot(), filePath, fileName)
1785 fullNewFilePath, err := hotline.ReadPath(cc.FileRoot(), fileNewPath, fileName)
1790 if err := cc.Server.FS.Symlink(fullFilePath, fullNewFilePath); err != nil {
1791 return cc.NewErrReply(t, "Error creating alias")
1794 res = append(res, cc.NewReply(t))
1798 // HandleDownloadBanner handles requests for a new banner from the server
1799 // Fields used in the request:
1801 // Fields used in the reply:
1802 // 107 FieldRefNum Used later for transfer
1803 // 108 FieldTransferSize Size of data to be downloaded
1804 func HandleDownloadBanner(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotline.Transaction) {
1805 ft := cc.NewFileTransfer(hotline.BannerDownload, "", []byte{}, []byte{}, make([]byte, 4))
1806 binary.BigEndian.PutUint32(ft.TransferSize, uint32(len(cc.Server.Banner)))
1808 return append(res, cc.NewReply(t,
1809 hotline.NewField(hotline.FieldRefNum, ft.RefNum[:]),
1810 hotline.NewField(hotline.FieldTransferSize, ft.TransferSize),