18 type HandlerFunc func(*ClientConn, *Transaction) ([]Transaction, error)
20 type TransactionType struct {
21 Handler HandlerFunc // function for handling the transaction type
22 Name string // Name of transaction as it will appear in logging
23 RequiredFields []requiredField
26 var TransactionHandlers = map[uint16]TransactionType{
32 TranNotifyChangeUser: {
33 Name: "TranNotifyChangeUser",
39 Name: "TranShowAgreement",
42 Name: "TranUserAccess",
44 TranNotifyDeleteUser: {
45 Name: "TranNotifyDeleteUser",
49 Handler: HandleTranAgreed,
53 Handler: HandleChatSend,
54 RequiredFields: []requiredField{
62 Name: "TranDelNewsArt",
63 Handler: HandleDelNewsArt,
66 Name: "TranDelNewsItem",
67 Handler: HandleDelNewsItem,
70 Name: "TranDeleteFile",
71 Handler: HandleDeleteFile,
74 Name: "TranDeleteUser",
75 Handler: HandleDeleteUser,
78 Name: "TranDisconnectUser",
79 Handler: HandleDisconnectUser,
82 Name: "TranDownloadFile",
83 Handler: HandleDownloadFile,
86 Name: "TranDownloadFldr",
87 Handler: HandleDownloadFolder,
89 TranGetClientInfoText: {
90 Name: "TranGetClientInfoText",
91 Handler: HandleGetClientInfoText,
94 Name: "TranGetFileInfo",
95 Handler: HandleGetFileInfo,
97 TranGetFileNameList: {
98 Name: "TranGetFileNameList",
99 Handler: HandleGetFileNameList,
103 Handler: HandleGetMsgs,
105 TranGetNewsArtData: {
106 Name: "TranGetNewsArtData",
107 Handler: HandleGetNewsArtData,
109 TranGetNewsArtNameList: {
110 Name: "TranGetNewsArtNameList",
111 Handler: HandleGetNewsArtNameList,
113 TranGetNewsCatNameList: {
114 Name: "TranGetNewsCatNameList",
115 Handler: HandleGetNewsCatNameList,
119 Handler: HandleGetUser,
121 TranGetUserNameList: {
122 Name: "tranHandleGetUserNameList",
123 Handler: HandleGetUserNameList,
126 Name: "TranInviteNewChat",
127 Handler: HandleInviteNewChat,
130 Name: "TranInviteToChat",
131 Handler: HandleInviteToChat,
134 Name: "TranJoinChat",
135 Handler: HandleJoinChat,
138 Name: "TranKeepAlive",
139 Handler: HandleKeepAlive,
142 Name: "TranJoinChat",
143 Handler: HandleLeaveChat,
146 Name: "TranListUsers",
147 Handler: HandleListUsers,
150 Name: "TranMoveFile",
151 Handler: HandleMoveFile,
154 Name: "TranNewFolder",
155 Handler: HandleNewFolder,
158 Name: "TranNewNewsCat",
159 Handler: HandleNewNewsCat,
162 Name: "TranNewNewsFldr",
163 Handler: HandleNewNewsFldr,
167 Handler: HandleNewUser,
170 Name: "TranUpdateUser",
171 Handler: HandleUpdateUser,
174 Name: "TranOldPostNews",
175 Handler: HandleTranOldPostNews,
178 Name: "TranPostNewsArt",
179 Handler: HandlePostNewsArt,
181 TranRejectChatInvite: {
182 Name: "TranRejectChatInvite",
183 Handler: HandleRejectChatInvite,
185 TranSendInstantMsg: {
186 Name: "TranSendInstantMsg",
187 Handler: HandleSendInstantMsg,
188 RequiredFields: []requiredField{
198 TranSetChatSubject: {
199 Name: "TranSetChatSubject",
200 Handler: HandleSetChatSubject,
203 Name: "TranMakeFileAlias",
204 Handler: HandleMakeAlias,
205 RequiredFields: []requiredField{
206 {ID: FieldFileName, minLen: 1},
207 {ID: FieldFilePath, minLen: 1},
208 {ID: FieldFileNewPath, minLen: 1},
211 TranSetClientUserInfo: {
212 Name: "TranSetClientUserInfo",
213 Handler: HandleSetClientUserInfo,
216 Name: "TranSetFileInfo",
217 Handler: HandleSetFileInfo,
221 Handler: HandleSetUser,
224 Name: "TranUploadFile",
225 Handler: HandleUploadFile,
228 Name: "TranUploadFldr",
229 Handler: HandleUploadFolder,
232 Name: "TranUserBroadcast",
233 Handler: HandleUserBroadcast,
235 TranDownloadBanner: {
236 Name: "TranDownloadBanner",
237 Handler: HandleDownloadBanner,
241 func HandleChatSend(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
242 if !cc.Authorize(accessSendChat) {
243 res = append(res, cc.NewErrReply(t, "You are not allowed to participate in chat."))
247 // Truncate long usernames
248 trunc := fmt.Sprintf("%13s", cc.UserName)
249 formattedMsg := fmt.Sprintf("\r%.14s: %s", trunc, t.GetField(FieldData).Data)
251 // By holding the option key, Hotline chat allows users to send /me formatted messages like:
252 // *** Halcyon does stuff
253 // This is indicated by the presence of the optional field FieldChatOptions set to a value of 1.
254 // Most clients do not send this option for normal chat messages.
255 if t.GetField(FieldChatOptions).Data != nil && bytes.Equal(t.GetField(FieldChatOptions).Data, []byte{0, 1}) {
256 formattedMsg = fmt.Sprintf("\r*** %s %s", cc.UserName, t.GetField(FieldData).Data)
259 // The ChatID field is used to identify messages as belonging to a private chat.
260 // All clients *except* Frogblast omit this field for public chat, but Frogblast sends a value of 00 00 00 00.
261 chatID := t.GetField(FieldChatID).Data
262 if chatID != nil && !bytes.Equal([]byte{0, 0, 0, 0}, chatID) {
263 chatInt := binary.BigEndian.Uint32(chatID)
264 privChat := cc.Server.PrivateChats[chatInt]
266 clients := sortedClients(privChat.ClientConn)
268 // send the message to all connected clients of the private chat
269 for _, c := range clients {
270 res = append(res, *NewTransaction(
273 NewField(FieldChatID, chatID),
274 NewField(FieldData, []byte(formattedMsg)),
280 for _, c := range sortedClients(cc.Server.Clients) {
281 // Filter out clients that do not have the read chat permission
282 if c.Authorize(accessReadChat) {
283 res = append(res, *NewTransaction(TranChatMsg, c.ID, NewField(FieldData, []byte(formattedMsg))))
290 // HandleSendInstantMsg sends instant message to the user on the current server.
291 // Fields used in the request:
295 // One of the following values:
296 // - User message (myOpt_UserMessage = 1)
297 // - Refuse message (myOpt_RefuseMessage = 2)
298 // - Refuse chat (myOpt_RefuseChat = 3)
299 // - Automatic response (myOpt_AutomaticResponse = 4)"
301 // 214 Quoting message Optional
303 // Fields used in the reply:
305 func HandleSendInstantMsg(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
306 if !cc.Authorize(accessSendPrivMsg) {
307 res = append(res, cc.NewErrReply(t, "You are not allowed to send private messages."))
308 return res, errors.New("user is not allowed to send private messages")
311 msg := t.GetField(FieldData)
312 ID := t.GetField(FieldUserID)
314 reply := NewTransaction(
317 NewField(FieldData, msg.Data),
318 NewField(FieldUserName, cc.UserName),
319 NewField(FieldUserID, *cc.ID),
320 NewField(FieldOptions, []byte{0, 1}),
323 // Later versions of Hotline include the original message in the FieldQuotingMsg field so
324 // the receiving client can display both the received message and what it is in reply to
325 if t.GetField(FieldQuotingMsg).Data != nil {
326 reply.Fields = append(reply.Fields, NewField(FieldQuotingMsg, t.GetField(FieldQuotingMsg).Data))
329 id, err := byteToInt(ID.Data)
331 return res, errors.New("invalid client ID")
333 otherClient, ok := cc.Server.Clients[uint16(id)]
335 return res, errors.New("invalid client ID")
338 // Check if target user has "Refuse private messages" flag
339 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(otherClient.Flags)))
340 if flagBitmap.Bit(UserFlagRefusePChat) == 1 {
345 NewField(FieldData, []byte(string(otherClient.UserName)+" does not accept private messages.")),
346 NewField(FieldUserName, otherClient.UserName),
347 NewField(FieldUserID, *otherClient.ID),
348 NewField(FieldOptions, []byte{0, 2}),
352 res = append(res, *reply)
355 // Respond with auto reply if other client has it enabled
356 if len(otherClient.AutoReply) > 0 {
361 NewField(FieldData, otherClient.AutoReply),
362 NewField(FieldUserName, otherClient.UserName),
363 NewField(FieldUserID, *otherClient.ID),
364 NewField(FieldOptions, []byte{0, 1}),
369 res = append(res, cc.NewReply(t))
374 func HandleGetFileInfo(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
375 fileName := t.GetField(FieldFileName).Data
376 filePath := t.GetField(FieldFilePath).Data
378 fullFilePath, err := readPath(cc.Server.Config.FileRoot, filePath, fileName)
383 fw, err := newFileWrapper(cc.Server.FS, fullFilePath, 0)
388 encodedName, err := txtEncoder.String(fw.name)
390 return res, fmt.Errorf("invalid filepath encoding: %w", err)
393 res = append(res, cc.NewReply(t,
394 NewField(FieldFileName, []byte(encodedName)),
395 NewField(FieldFileTypeString, fw.ffo.FlatFileInformationFork.friendlyType()),
396 NewField(FieldFileCreatorString, fw.ffo.FlatFileInformationFork.friendlyCreator()),
397 NewField(FieldFileComment, fw.ffo.FlatFileInformationFork.Comment),
398 NewField(FieldFileType, fw.ffo.FlatFileInformationFork.TypeSignature),
399 NewField(FieldFileCreateDate, fw.ffo.FlatFileInformationFork.CreateDate),
400 NewField(FieldFileModifyDate, fw.ffo.FlatFileInformationFork.ModifyDate),
401 NewField(FieldFileSize, fw.totalSize()),
406 // HandleSetFileInfo updates a file or folder name and/or comment from the Get Info window
407 // Fields used in the request:
409 // * 202 File path Optional
410 // * 211 File new name Optional
411 // * 210 File comment Optional
412 // Fields used in the reply: None
413 func HandleSetFileInfo(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
414 fileName := t.GetField(FieldFileName).Data
415 filePath := t.GetField(FieldFilePath).Data
417 fullFilePath, err := readPath(cc.Server.Config.FileRoot, filePath, fileName)
422 fi, err := cc.Server.FS.Stat(fullFilePath)
427 hlFile, err := newFileWrapper(cc.Server.FS, fullFilePath, 0)
431 if t.GetField(FieldFileComment).Data != nil {
432 switch mode := fi.Mode(); {
434 if !cc.Authorize(accessSetFolderComment) {
435 res = append(res, cc.NewErrReply(t, "You are not allowed to set comments for folders."))
438 case mode.IsRegular():
439 if !cc.Authorize(accessSetFileComment) {
440 res = append(res, cc.NewErrReply(t, "You are not allowed to set comments for files."))
445 if err := hlFile.ffo.FlatFileInformationFork.setComment(t.GetField(FieldFileComment).Data); err != nil {
448 w, err := hlFile.infoForkWriter()
452 _, err = w.Write(hlFile.ffo.FlatFileInformationFork.MarshalBinary())
458 fullNewFilePath, err := readPath(cc.Server.Config.FileRoot, filePath, t.GetField(FieldFileNewName).Data)
463 fileNewName := t.GetField(FieldFileNewName).Data
465 if fileNewName != nil {
466 switch mode := fi.Mode(); {
468 if !cc.Authorize(accessRenameFolder) {
469 res = append(res, cc.NewErrReply(t, "You are not allowed to rename folders."))
472 err = os.Rename(fullFilePath, fullNewFilePath)
473 if os.IsNotExist(err) {
474 res = append(res, cc.NewErrReply(t, "Cannot rename folder "+string(fileName)+" because it does not exist or cannot be found."))
477 case mode.IsRegular():
478 if !cc.Authorize(accessRenameFile) {
479 res = append(res, cc.NewErrReply(t, "You are not allowed to rename files."))
482 fileDir, err := readPath(cc.Server.Config.FileRoot, filePath, []byte{})
486 hlFile.name, err = txtDecoder.String(string(fileNewName))
488 return res, fmt.Errorf("invalid filepath encoding: %w", err)
491 err = hlFile.move(fileDir)
492 if os.IsNotExist(err) {
493 res = append(res, cc.NewErrReply(t, "Cannot rename file "+string(fileName)+" because it does not exist or cannot be found."))
502 res = append(res, cc.NewReply(t))
506 // HandleDeleteFile deletes a file or folder
507 // Fields used in the request:
510 // Fields used in the reply: none
511 func HandleDeleteFile(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
512 fileName := t.GetField(FieldFileName).Data
513 filePath := t.GetField(FieldFilePath).Data
515 fullFilePath, err := readPath(cc.Server.Config.FileRoot, filePath, fileName)
520 hlFile, err := newFileWrapper(cc.Server.FS, fullFilePath, 0)
525 fi, err := hlFile.dataFile()
527 res = append(res, cc.NewErrReply(t, "Cannot delete file "+string(fileName)+" because it does not exist or cannot be found."))
531 switch mode := fi.Mode(); {
533 if !cc.Authorize(accessDeleteFolder) {
534 res = append(res, cc.NewErrReply(t, "You are not allowed to delete folders."))
537 case mode.IsRegular():
538 if !cc.Authorize(accessDeleteFile) {
539 res = append(res, cc.NewErrReply(t, "You are not allowed to delete files."))
544 if err := hlFile.delete(); err != nil {
548 res = append(res, cc.NewReply(t))
552 // HandleMoveFile moves files or folders. Note: seemingly not documented
553 func HandleMoveFile(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
554 fileName := string(t.GetField(FieldFileName).Data)
556 filePath, err := readPath(cc.Server.Config.FileRoot, t.GetField(FieldFilePath).Data, t.GetField(FieldFileName).Data)
561 fileNewPath, err := readPath(cc.Server.Config.FileRoot, t.GetField(FieldFileNewPath).Data, nil)
566 cc.logger.Infow("Move file", "src", filePath+"/"+fileName, "dst", fileNewPath+"/"+fileName)
568 hlFile, err := newFileWrapper(cc.Server.FS, filePath, 0)
573 fi, err := hlFile.dataFile()
575 res = append(res, cc.NewErrReply(t, "Cannot delete file "+fileName+" because it does not exist or cannot be found."))
578 switch mode := fi.Mode(); {
580 if !cc.Authorize(accessMoveFolder) {
581 res = append(res, cc.NewErrReply(t, "You are not allowed to move folders."))
584 case mode.IsRegular():
585 if !cc.Authorize(accessMoveFile) {
586 res = append(res, cc.NewErrReply(t, "You are not allowed to move files."))
590 if err := hlFile.move(fileNewPath); err != nil {
593 // TODO: handle other possible errors; e.g. fileWrapper delete fails due to fileWrapper permission issue
595 res = append(res, cc.NewReply(t))
599 func HandleNewFolder(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
600 if !cc.Authorize(accessCreateFolder) {
601 res = append(res, cc.NewErrReply(t, "You are not allowed to create folders."))
604 folderName := string(t.GetField(FieldFileName).Data)
606 folderName = path.Join("/", folderName)
610 // FieldFilePath is only present for nested paths
611 if t.GetField(FieldFilePath).Data != nil {
613 _, err := newFp.Write(t.GetField(FieldFilePath).Data)
618 for _, pathItem := range newFp.Items {
619 subPath = filepath.Join("/", subPath, string(pathItem.Name))
622 newFolderPath := path.Join(cc.Server.Config.FileRoot, subPath, folderName)
623 newFolderPath, err = txtDecoder.String(newFolderPath)
625 return res, fmt.Errorf("invalid filepath encoding: %w", err)
628 // TODO: check path and folder name lengths
630 if _, err := cc.Server.FS.Stat(newFolderPath); !os.IsNotExist(err) {
631 msg := fmt.Sprintf("Cannot create folder \"%s\" because there is already a file or folder with that name.", folderName)
632 return []Transaction{cc.NewErrReply(t, msg)}, nil
635 if err := cc.Server.FS.Mkdir(newFolderPath, 0777); err != nil {
636 msg := fmt.Sprintf("Cannot create folder \"%s\" because an error occurred.", folderName)
637 return []Transaction{cc.NewErrReply(t, msg)}, nil
640 res = append(res, cc.NewReply(t))
644 func HandleSetUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
645 if !cc.Authorize(accessModifyUser) {
646 res = append(res, cc.NewErrReply(t, "You are not allowed to modify accounts."))
650 login := decodeString(t.GetField(FieldUserLogin).Data)
651 userName := string(t.GetField(FieldUserName).Data)
653 newAccessLvl := t.GetField(FieldUserAccess).Data
655 account := cc.Server.Accounts[login]
656 account.Name = userName
657 copy(account.Access[:], newAccessLvl)
659 // If the password field is cleared in the Hotline edit user UI, the SetUser transaction does
660 // not include FieldUserPassword
661 if t.GetField(FieldUserPassword).Data == nil {
662 account.Password = hashAndSalt([]byte(""))
664 if len(t.GetField(FieldUserPassword).Data) > 1 {
665 account.Password = hashAndSalt(t.GetField(FieldUserPassword).Data)
668 out, err := yaml.Marshal(&account)
672 if err := os.WriteFile(filepath.Join(cc.Server.ConfigDir, "Users", login+".yaml"), out, 0666); err != nil {
676 // Notify connected clients logged in as the user of the new access level
677 for _, c := range cc.Server.Clients {
678 if c.Account.Login == login {
679 // Note: comment out these two lines to test server-side deny messages
680 newT := NewTransaction(TranUserAccess, c.ID, NewField(FieldUserAccess, newAccessLvl))
681 res = append(res, *newT)
683 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(c.Flags)))
684 if c.Authorize(accessDisconUser) {
685 flagBitmap.SetBit(flagBitmap, UserFlagAdmin, 1)
687 flagBitmap.SetBit(flagBitmap, UserFlagAdmin, 0)
689 binary.BigEndian.PutUint16(c.Flags, uint16(flagBitmap.Int64()))
691 c.Account.Access = account.Access
694 TranNotifyChangeUser,
695 NewField(FieldUserID, *c.ID),
696 NewField(FieldUserFlags, c.Flags),
697 NewField(FieldUserName, c.UserName),
698 NewField(FieldUserIconID, c.Icon),
703 res = append(res, cc.NewReply(t))
707 func HandleGetUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
708 if !cc.Authorize(accessOpenUser) {
709 res = append(res, cc.NewErrReply(t, "You are not allowed to view accounts."))
713 account := cc.Server.Accounts[string(t.GetField(FieldUserLogin).Data)]
715 res = append(res, cc.NewErrReply(t, "Account does not exist."))
719 res = append(res, cc.NewReply(t,
720 NewField(FieldUserName, []byte(account.Name)),
721 NewField(FieldUserLogin, encodeString(t.GetField(FieldUserLogin).Data)),
722 NewField(FieldUserPassword, []byte(account.Password)),
723 NewField(FieldUserAccess, account.Access[:]),
728 func HandleListUsers(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
729 if !cc.Authorize(accessOpenUser) {
730 res = append(res, cc.NewErrReply(t, "You are not allowed to view accounts."))
734 var userFields []Field
735 for _, acc := range cc.Server.Accounts {
736 b := make([]byte, 0, 100)
737 n, err := acc.Read(b)
742 userFields = append(userFields, NewField(FieldData, b[:n]))
745 res = append(res, cc.NewReply(t, userFields...))
749 // HandleUpdateUser is used by the v1.5+ multi-user editor to perform account editing for multiple users at a time.
750 // An update can be a mix of these actions:
753 // * Modify user (including renaming the account login)
755 // The Transaction sent by the client includes one data field per user that was modified. This data field in turn
756 // contains another data field encoded in its payload with a varying number of sub fields depending on which action is
757 // performed. This seems to be the only place in the Hotline protocol where a data field contains another data field.
758 func HandleUpdateUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
759 for _, field := range t.Fields {
760 subFields, err := ReadFields(field.Data[0:2], field.Data[2:])
765 if len(subFields) == 1 {
766 login := decodeString(getField(FieldData, &subFields).Data)
767 cc.logger.Infow("DeleteUser", "login", login)
769 if !cc.Authorize(accessDeleteUser) {
770 res = append(res, cc.NewErrReply(t, "You are not allowed to delete accounts."))
774 if err := cc.Server.DeleteUser(login); err != nil {
780 login := decodeString(getField(FieldUserLogin, &subFields).Data)
782 // check if the login dataFile; if so, we know we are updating an existing user
783 if acc, ok := cc.Server.Accounts[login]; ok {
784 cc.logger.Infow("UpdateUser", "login", login)
786 // account dataFile, so this is an update action
787 if !cc.Authorize(accessModifyUser) {
788 res = append(res, cc.NewErrReply(t, "You are not allowed to modify accounts."))
792 if getField(FieldUserPassword, &subFields) != nil {
793 newPass := getField(FieldUserPassword, &subFields).Data
794 acc.Password = hashAndSalt(newPass)
796 acc.Password = hashAndSalt([]byte(""))
799 if getField(FieldUserAccess, &subFields) != nil {
800 copy(acc.Access[:], getField(FieldUserAccess, &subFields).Data)
803 err = cc.Server.UpdateUser(
804 decodeString(getField(FieldData, &subFields).Data),
805 decodeString(getField(FieldUserLogin, &subFields).Data),
806 string(getField(FieldUserName, &subFields).Data),
814 cc.logger.Infow("CreateUser", "login", login)
816 if !cc.Authorize(accessCreateUser) {
817 res = append(res, cc.NewErrReply(t, "You are not allowed to create new accounts."))
821 newAccess := accessBitmap{}
822 copy(newAccess[:], getField(FieldUserAccess, &subFields).Data)
824 // Prevent account from creating new account with greater permission
825 for i := 0; i < 64; i++ {
826 if newAccess.IsSet(i) {
827 if !cc.Authorize(i) {
828 return append(res, cc.NewErrReply(t, "Cannot create account with more access than yourself.")), err
833 err := cc.Server.NewUser(login, string(getField(FieldUserName, &subFields).Data), string(getField(FieldUserPassword, &subFields).Data), newAccess)
835 return []Transaction{}, err
840 res = append(res, cc.NewReply(t))
844 // HandleNewUser creates a new user account
845 func HandleNewUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
846 if !cc.Authorize(accessCreateUser) {
847 res = append(res, cc.NewErrReply(t, "You are not allowed to create new accounts."))
851 login := decodeString(t.GetField(FieldUserLogin).Data)
853 // If the account already dataFile, reply with an error
854 if _, ok := cc.Server.Accounts[login]; ok {
855 res = append(res, cc.NewErrReply(t, "Cannot create account "+login+" because there is already an account with that login."))
859 newAccess := accessBitmap{}
860 copy(newAccess[:], t.GetField(FieldUserAccess).Data)
862 // Prevent account from creating new account with greater permission
863 for i := 0; i < 64; i++ {
864 if newAccess.IsSet(i) {
865 if !cc.Authorize(i) {
866 res = append(res, cc.NewErrReply(t, "Cannot create account with more access than yourself."))
872 if err := cc.Server.NewUser(login, string(t.GetField(FieldUserName).Data), string(t.GetField(FieldUserPassword).Data), newAccess); err != nil {
873 return []Transaction{}, err
876 res = append(res, cc.NewReply(t))
880 func HandleDeleteUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
881 if !cc.Authorize(accessDeleteUser) {
882 res = append(res, cc.NewErrReply(t, "You are not allowed to delete accounts."))
886 // TODO: Handle case where account doesn't exist; e.g. delete race condition
887 login := decodeString(t.GetField(FieldUserLogin).Data)
889 if err := cc.Server.DeleteUser(login); err != nil {
893 res = append(res, cc.NewReply(t))
897 // HandleUserBroadcast sends an Administrator Message to all connected clients of the server
898 func HandleUserBroadcast(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
899 if !cc.Authorize(accessBroadcast) {
900 res = append(res, cc.NewErrReply(t, "You are not allowed to send broadcast messages."))
906 NewField(FieldData, t.GetField(TranGetMsgs).Data),
907 NewField(FieldChatOptions, []byte{0}),
910 res = append(res, cc.NewReply(t))
914 // HandleGetClientInfoText returns user information for the specific user.
916 // Fields used in the request:
919 // Fields used in the reply:
921 // 101 Data User info text string
922 func HandleGetClientInfoText(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
923 if !cc.Authorize(accessGetClientInfo) {
924 res = append(res, cc.NewErrReply(t, "You are not allowed to get client info."))
928 clientID, _ := byteToInt(t.GetField(FieldUserID).Data)
930 clientConn := cc.Server.Clients[uint16(clientID)]
931 if clientConn == nil {
932 return append(res, cc.NewErrReply(t, "User not found.")), err
935 res = append(res, cc.NewReply(t,
936 NewField(FieldData, []byte(clientConn.String())),
937 NewField(FieldUserName, clientConn.UserName),
942 func HandleGetUserNameList(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
943 res = append(res, cc.NewReply(t, cc.Server.connectedUsers()...))
948 func HandleTranAgreed(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
949 if t.GetField(FieldUserName).Data != nil {
950 if cc.Authorize(accessAnyName) {
951 cc.UserName = t.GetField(FieldUserName).Data
953 cc.UserName = []byte(cc.Account.Name)
957 cc.Icon = t.GetField(FieldUserIconID).Data
959 cc.logger = cc.logger.With("name", string(cc.UserName))
960 cc.logger.Infow("Login successful", "clientVersion", fmt.Sprintf("%v", func() int { i, _ := byteToInt(cc.Version); return i }()))
962 options := t.GetField(FieldOptions).Data
963 optBitmap := big.NewInt(int64(binary.BigEndian.Uint16(options)))
965 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(cc.Flags)))
967 // Check refuse private PM option
968 if optBitmap.Bit(refusePM) == 1 {
969 flagBitmap.SetBit(flagBitmap, UserFlagRefusePM, 1)
970 binary.BigEndian.PutUint16(cc.Flags, uint16(flagBitmap.Int64()))
973 // Check refuse private chat option
974 if optBitmap.Bit(refuseChat) == 1 {
975 flagBitmap.SetBit(flagBitmap, UserFlagRefusePChat, 1)
976 binary.BigEndian.PutUint16(cc.Flags, uint16(flagBitmap.Int64()))
979 // Check auto response
980 if optBitmap.Bit(autoResponse) == 1 {
981 cc.AutoReply = t.GetField(FieldAutomaticResponse).Data
983 cc.AutoReply = []byte{}
986 trans := cc.notifyOthers(
988 TranNotifyChangeUser, nil,
989 NewField(FieldUserName, cc.UserName),
990 NewField(FieldUserID, *cc.ID),
991 NewField(FieldUserIconID, cc.Icon),
992 NewField(FieldUserFlags, cc.Flags),
995 res = append(res, trans...)
997 if cc.Server.Config.BannerFile != "" {
998 res = append(res, *NewTransaction(TranServerBanner, cc.ID, NewField(FieldBannerType, []byte("JPEG"))))
1001 res = append(res, cc.NewReply(t))
1006 // HandleTranOldPostNews updates the flat news
1007 // Fields used in this request:
1009 func HandleTranOldPostNews(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1010 if !cc.Authorize(accessNewsPostArt) {
1011 res = append(res, cc.NewErrReply(t, "You are not allowed to post news."))
1015 cc.Server.flatNewsMux.Lock()
1016 defer cc.Server.flatNewsMux.Unlock()
1018 newsDateTemplate := defaultNewsDateFormat
1019 if cc.Server.Config.NewsDateFormat != "" {
1020 newsDateTemplate = cc.Server.Config.NewsDateFormat
1023 newsTemplate := defaultNewsTemplate
1024 if cc.Server.Config.NewsDelimiter != "" {
1025 newsTemplate = cc.Server.Config.NewsDelimiter
1028 newsPost := fmt.Sprintf(newsTemplate+"\r", cc.UserName, time.Now().Format(newsDateTemplate), t.GetField(FieldData).Data)
1029 newsPost = strings.ReplaceAll(newsPost, "\n", "\r")
1031 // update news in memory
1032 cc.Server.FlatNews = append([]byte(newsPost), cc.Server.FlatNews...)
1034 // update news on disk
1035 if err := cc.Server.FS.WriteFile(filepath.Join(cc.Server.ConfigDir, "MessageBoard.txt"), cc.Server.FlatNews, 0644); err != nil {
1039 // Notify all clients of updated news
1042 NewField(FieldData, []byte(newsPost)),
1045 res = append(res, cc.NewReply(t))
1049 func HandleDisconnectUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1050 if !cc.Authorize(accessDisconUser) {
1051 res = append(res, cc.NewErrReply(t, "You are not allowed to disconnect users."))
1055 clientConn := cc.Server.Clients[binary.BigEndian.Uint16(t.GetField(FieldUserID).Data)]
1057 if clientConn.Authorize(accessCannotBeDiscon) {
1058 res = append(res, cc.NewErrReply(t, clientConn.Account.Login+" is not allowed to be disconnected."))
1062 // If FieldOptions is set, then the client IP is banned in addition to disconnected.
1063 // 00 01 = temporary ban
1064 // 00 02 = permanent ban
1065 if t.GetField(FieldOptions).Data != nil {
1066 switch t.GetField(FieldOptions).Data[1] {
1068 // send message: "You are temporarily banned on this server"
1069 cc.logger.Infow("Disconnect & temporarily ban " + string(clientConn.UserName))
1071 res = append(res, *NewTransaction(
1074 NewField(FieldData, []byte("You are temporarily banned on this server")),
1075 NewField(FieldChatOptions, []byte{0, 0}),
1078 banUntil := time.Now().Add(tempBanDuration)
1079 cc.Server.banList[strings.Split(clientConn.RemoteAddr, ":")[0]] = &banUntil
1081 // send message: "You are permanently banned on this server"
1082 cc.logger.Infow("Disconnect & ban " + string(clientConn.UserName))
1084 res = append(res, *NewTransaction(
1087 NewField(FieldData, []byte("You are permanently banned on this server")),
1088 NewField(FieldChatOptions, []byte{0, 0}),
1091 cc.Server.banList[strings.Split(clientConn.RemoteAddr, ":")[0]] = nil
1094 err := cc.Server.writeBanList()
1100 // TODO: remove this awful hack
1102 time.Sleep(1 * time.Second)
1103 clientConn.Disconnect()
1106 return append(res, cc.NewReply(t)), err
1109 // HandleGetNewsCatNameList returns a list of news categories for a path
1110 // Fields used in the request:
1111 // 325 News path (Optional)
1112 func HandleGetNewsCatNameList(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1113 if !cc.Authorize(accessNewsReadArt) {
1114 res = append(res, cc.NewErrReply(t, "You are not allowed to read news."))
1118 pathStrs := ReadNewsPath(t.GetField(FieldNewsPath).Data)
1119 cats := cc.Server.GetNewsCatByPath(pathStrs)
1121 // To store the keys in slice in sorted order
1122 keys := make([]string, len(cats))
1124 for k := range cats {
1130 var fieldData []Field
1131 for _, k := range keys {
1133 b, _ := cat.MarshalBinary()
1134 fieldData = append(fieldData, NewField(
1135 FieldNewsCatListData15,
1140 res = append(res, cc.NewReply(t, fieldData...))
1144 func HandleNewNewsCat(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1145 if !cc.Authorize(accessNewsCreateCat) {
1146 res = append(res, cc.NewErrReply(t, "You are not allowed to create news categories."))
1150 name := string(t.GetField(FieldNewsCatName).Data)
1151 pathStrs := ReadNewsPath(t.GetField(FieldNewsPath).Data)
1153 cats := cc.Server.GetNewsCatByPath(pathStrs)
1154 cats[name] = NewsCategoryListData15{
1157 Articles: map[uint32]*NewsArtData{},
1158 SubCats: make(map[string]NewsCategoryListData15),
1161 if err := cc.Server.writeThreadedNews(); err != nil {
1164 res = append(res, cc.NewReply(t))
1168 // Fields used in the request:
1169 // 322 News category name
1171 func HandleNewNewsFldr(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1172 if !cc.Authorize(accessNewsCreateFldr) {
1173 res = append(res, cc.NewErrReply(t, "You are not allowed to create news folders."))
1177 name := string(t.GetField(FieldFileName).Data)
1178 pathStrs := ReadNewsPath(t.GetField(FieldNewsPath).Data)
1180 cc.logger.Infof("Creating new news folder %s", name)
1182 cats := cc.Server.GetNewsCatByPath(pathStrs)
1183 cats[name] = NewsCategoryListData15{
1186 Articles: map[uint32]*NewsArtData{},
1187 SubCats: make(map[string]NewsCategoryListData15),
1189 if err := cc.Server.writeThreadedNews(); err != nil {
1192 res = append(res, cc.NewReply(t))
1196 // HandleGetNewsArtData gets the list of article names at the specified news path.
1198 // Fields used in the request:
1199 // 325 News path Optional
1201 // Fields used in the reply:
1202 // 321 News article list data Optional
1203 func HandleGetNewsArtNameList(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1204 if !cc.Authorize(accessNewsReadArt) {
1205 res = append(res, cc.NewErrReply(t, "You are not allowed to read news."))
1208 pathStrs := ReadNewsPath(t.GetField(FieldNewsPath).Data)
1210 var cat NewsCategoryListData15
1211 cats := cc.Server.ThreadedNews.Categories
1213 for _, fp := range pathStrs {
1215 cats = cats[fp].SubCats
1218 nald := cat.GetNewsArtListData()
1220 res = append(res, cc.NewReply(t, NewField(FieldNewsArtListData, nald.Payload())))
1224 // HandleGetNewsArtData requests information about the specific news article.
1225 // Fields used in the request:
1229 // 326 News article ID
1230 // 327 News article data flavor
1232 // Fields used in the reply:
1233 // 328 News article title
1234 // 329 News article poster
1235 // 330 News article date
1236 // 331 Previous article ID
1237 // 332 Next article ID
1238 // 335 Parent article ID
1239 // 336 First child article ID
1240 // 327 News article data flavor "Should be “text/plain”
1241 // 333 News article data Optional (if data flavor is “text/plain”)
1242 func HandleGetNewsArtData(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1243 if !cc.Authorize(accessNewsReadArt) {
1244 res = append(res, cc.NewErrReply(t, "You are not allowed to read news."))
1248 var cat NewsCategoryListData15
1249 cats := cc.Server.ThreadedNews.Categories
1251 for _, fp := range ReadNewsPath(t.GetField(FieldNewsPath).Data) {
1253 cats = cats[fp].SubCats
1256 // The official Hotline clients will send the article ID as 2 bytes if possible, but
1257 // some third party clients such as Frogblast and Heildrun will always send 4 bytes
1258 convertedID, err := byteToInt(t.GetField(FieldNewsArtID).Data)
1263 art := cat.Articles[uint32(convertedID)]
1265 res = append(res, cc.NewReply(t))
1269 res = append(res, cc.NewReply(t,
1270 NewField(FieldNewsArtTitle, []byte(art.Title)),
1271 NewField(FieldNewsArtPoster, []byte(art.Poster)),
1272 NewField(FieldNewsArtDate, art.Date),
1273 NewField(FieldNewsArtPrevArt, art.PrevArt),
1274 NewField(FieldNewsArtNextArt, art.NextArt),
1275 NewField(FieldNewsArtParentArt, art.ParentArt),
1276 NewField(FieldNewsArt1stChildArt, art.FirstChildArt),
1277 NewField(FieldNewsArtDataFlav, []byte("text/plain")),
1278 NewField(FieldNewsArtData, []byte(art.Data)),
1283 // HandleDelNewsItem deletes an existing threaded news folder or category from the server.
1284 // Fields used in the request:
1286 // Fields used in the reply:
1288 func HandleDelNewsItem(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1289 pathStrs := ReadNewsPath(t.GetField(FieldNewsPath).Data)
1291 cats := cc.Server.ThreadedNews.Categories
1292 delName := pathStrs[len(pathStrs)-1]
1293 if len(pathStrs) > 1 {
1294 for _, fp := range pathStrs[0 : len(pathStrs)-1] {
1295 cats = cats[fp].SubCats
1299 if bytes.Equal(cats[delName].Type, []byte{0, 3}) {
1300 if !cc.Authorize(accessNewsDeleteCat) {
1301 return append(res, cc.NewErrReply(t, "You are not allowed to delete news categories.")), nil
1304 if !cc.Authorize(accessNewsDeleteFldr) {
1305 return append(res, cc.NewErrReply(t, "You are not allowed to delete news folders.")), nil
1309 delete(cats, delName)
1311 if err := cc.Server.writeThreadedNews(); err != nil {
1315 return append(res, cc.NewReply(t)), nil
1318 func HandleDelNewsArt(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1319 if !cc.Authorize(accessNewsDeleteArt) {
1320 res = append(res, cc.NewErrReply(t, "You are not allowed to delete news articles."))
1326 // 326 News article ID
1327 // 337 News article – recursive delete Delete child articles (1) or not (0)
1328 pathStrs := ReadNewsPath(t.GetField(FieldNewsPath).Data)
1329 ID, err := byteToInt(t.GetField(FieldNewsArtID).Data)
1334 // TODO: Delete recursive
1335 cats := cc.Server.GetNewsCatByPath(pathStrs[:len(pathStrs)-1])
1337 catName := pathStrs[len(pathStrs)-1]
1338 cat := cats[catName]
1340 delete(cat.Articles, uint32(ID))
1343 if err := cc.Server.writeThreadedNews(); err != nil {
1347 res = append(res, cc.NewReply(t))
1353 // 326 News article ID ID of the parent article?
1354 // 328 News article title
1355 // 334 News article flags
1356 // 327 News article data flavor Currently “text/plain”
1357 // 333 News article data
1358 func HandlePostNewsArt(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1359 if !cc.Authorize(accessNewsPostArt) {
1360 res = append(res, cc.NewErrReply(t, "You are not allowed to post news articles."))
1364 pathStrs := ReadNewsPath(t.GetField(FieldNewsPath).Data)
1365 cats := cc.Server.GetNewsCatByPath(pathStrs[:len(pathStrs)-1])
1367 catName := pathStrs[len(pathStrs)-1]
1368 cat := cats[catName]
1370 artID, err := byteToInt(t.GetField(FieldNewsArtID).Data)
1374 convertedArtID := uint32(artID)
1375 bs := make([]byte, 4)
1376 binary.BigEndian.PutUint32(bs, convertedArtID)
1378 newArt := NewsArtData{
1379 Title: string(t.GetField(FieldNewsArtTitle).Data),
1380 Poster: string(cc.UserName),
1381 Date: toHotlineTime(time.Now()),
1382 PrevArt: []byte{0, 0, 0, 0},
1383 NextArt: []byte{0, 0, 0, 0},
1385 FirstChildArt: []byte{0, 0, 0, 0},
1386 DataFlav: []byte("text/plain"),
1387 Data: string(t.GetField(FieldNewsArtData).Data),
1391 for k := range cat.Articles {
1392 keys = append(keys, int(k))
1398 prevID := uint32(keys[len(keys)-1])
1401 binary.BigEndian.PutUint32(newArt.PrevArt, prevID)
1403 // Set next article ID
1404 binary.BigEndian.PutUint32(cat.Articles[prevID].NextArt, nextID)
1407 // Update parent article with first child reply
1408 parentID := convertedArtID
1410 parentArt := cat.Articles[parentID]
1412 if bytes.Equal(parentArt.FirstChildArt, []byte{0, 0, 0, 0}) {
1413 binary.BigEndian.PutUint32(parentArt.FirstChildArt, nextID)
1417 cat.Articles[nextID] = &newArt
1420 if err := cc.Server.writeThreadedNews(); err != nil {
1424 res = append(res, cc.NewReply(t))
1428 // HandleGetMsgs returns the flat news data
1429 func HandleGetMsgs(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1430 if !cc.Authorize(accessNewsReadArt) {
1431 res = append(res, cc.NewErrReply(t, "You are not allowed to read news."))
1435 res = append(res, cc.NewReply(t, NewField(FieldData, cc.Server.FlatNews)))
1440 func HandleDownloadFile(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1441 if !cc.Authorize(accessDownloadFile) {
1442 res = append(res, cc.NewErrReply(t, "You are not allowed to download files."))
1446 fileName := t.GetField(FieldFileName).Data
1447 filePath := t.GetField(FieldFilePath).Data
1448 resumeData := t.GetField(FieldFileResumeData).Data
1450 var dataOffset int64
1451 var frd FileResumeData
1452 if resumeData != nil {
1453 if err := frd.UnmarshalBinary(t.GetField(FieldFileResumeData).Data); err != nil {
1456 // TODO: handle rsrc fork offset
1457 dataOffset = int64(binary.BigEndian.Uint32(frd.ForkInfoList[0].DataSize[:]))
1460 fullFilePath, err := readPath(cc.Server.Config.FileRoot, filePath, fileName)
1465 hlFile, err := newFileWrapper(cc.Server.FS, fullFilePath, dataOffset)
1470 xferSize := hlFile.ffo.TransferSize(0)
1472 ft := cc.newFileTransfer(FileDownload, fileName, filePath, xferSize)
1474 // TODO: refactor to remove this
1475 if resumeData != nil {
1476 var frd FileResumeData
1477 if err := frd.UnmarshalBinary(t.GetField(FieldFileResumeData).Data); err != nil {
1480 ft.fileResumeData = &frd
1483 // Optional field for when a HL v1.5+ client requests file preview
1484 // Used only for TEXT, JPEG, GIFF, BMP or PICT files
1485 // The value will always be 2
1486 if t.GetField(FieldFileTransferOptions).Data != nil {
1487 ft.options = t.GetField(FieldFileTransferOptions).Data
1488 xferSize = hlFile.ffo.FlatFileDataForkHeader.DataSize[:]
1491 res = append(res, cc.NewReply(t,
1492 NewField(FieldRefNum, ft.refNum[:]),
1493 NewField(FieldWaitingCount, []byte{0x00, 0x00}), // TODO: Implement waiting count
1494 NewField(FieldTransferSize, xferSize),
1495 NewField(FieldFileSize, hlFile.ffo.FlatFileDataForkHeader.DataSize[:]),
1501 // Download all files from the specified folder and sub-folders
1502 func HandleDownloadFolder(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1503 if !cc.Authorize(accessDownloadFile) {
1504 res = append(res, cc.NewErrReply(t, "You are not allowed to download folders."))
1508 fullFilePath, err := readPath(cc.Server.Config.FileRoot, t.GetField(FieldFilePath).Data, t.GetField(FieldFileName).Data)
1513 transferSize, err := CalcTotalSize(fullFilePath)
1517 itemCount, err := CalcItemCount(fullFilePath)
1522 fileTransfer := cc.newFileTransfer(FolderDownload, t.GetField(FieldFileName).Data, t.GetField(FieldFilePath).Data, transferSize)
1525 _, err = fp.Write(t.GetField(FieldFilePath).Data)
1530 res = append(res, cc.NewReply(t,
1531 NewField(FieldRefNum, fileTransfer.ReferenceNumber),
1532 NewField(FieldTransferSize, transferSize),
1533 NewField(FieldFolderItemCount, itemCount),
1534 NewField(FieldWaitingCount, []byte{0x00, 0x00}), // TODO: Implement waiting count
1539 // Upload all files from the local folder and its subfolders to the specified path on the server
1540 // Fields used in the request
1543 // 108 transfer size Total size of all items in the folder
1544 // 220 Folder item count
1545 // 204 File transfer options "Optional Currently set to 1" (TODO: ??)
1546 func HandleUploadFolder(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1548 if t.GetField(FieldFilePath).Data != nil {
1549 if _, err = fp.Write(t.GetField(FieldFilePath).Data); err != nil {
1554 // Handle special cases for Upload and Drop Box folders
1555 if !cc.Authorize(accessUploadAnywhere) {
1556 if !fp.IsUploadDir() && !fp.IsDropbox() {
1557 res = append(res, 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(FieldFileName).Data))))
1562 fileTransfer := cc.newFileTransfer(FolderUpload,
1563 t.GetField(FieldFileName).Data,
1564 t.GetField(FieldFilePath).Data,
1565 t.GetField(FieldTransferSize).Data,
1568 fileTransfer.FolderItemCount = t.GetField(FieldFolderItemCount).Data
1570 res = append(res, cc.NewReply(t, NewField(FieldRefNum, fileTransfer.ReferenceNumber)))
1575 // Fields used in the request:
1578 // 204 File transfer options "Optional
1579 // Used only to resume download, currently has value 2"
1580 // 108 File transfer size "Optional used if download is not resumed"
1581 func HandleUploadFile(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1582 if !cc.Authorize(accessUploadFile) {
1583 res = append(res, cc.NewErrReply(t, "You are not allowed to upload files."))
1587 fileName := t.GetField(FieldFileName).Data
1588 filePath := t.GetField(FieldFilePath).Data
1589 transferOptions := t.GetField(FieldFileTransferOptions).Data
1590 transferSize := t.GetField(FieldTransferSize).Data // not sent for resume
1593 if filePath != nil {
1594 if _, err = fp.Write(filePath); err != nil {
1599 // Handle special cases for Upload and Drop Box folders
1600 if !cc.Authorize(accessUploadAnywhere) {
1601 if !fp.IsUploadDir() && !fp.IsDropbox() {
1602 res = append(res, 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))))
1606 fullFilePath, err := readPath(cc.Server.Config.FileRoot, filePath, fileName)
1611 if _, err := cc.Server.FS.Stat(fullFilePath); err == nil {
1612 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))))
1616 ft := cc.newFileTransfer(FileUpload, fileName, filePath, transferSize)
1618 replyT := cc.NewReply(t, NewField(FieldRefNum, ft.ReferenceNumber))
1620 // client has requested to resume a partially transferred file
1621 if transferOptions != nil {
1622 fileInfo, err := cc.Server.FS.Stat(fullFilePath + incompleteFileSuffix)
1627 offset := make([]byte, 4)
1628 binary.BigEndian.PutUint32(offset, uint32(fileInfo.Size()))
1630 fileResumeData := NewFileResumeData([]ForkInfoList{
1631 *NewForkInfoList(offset),
1634 b, _ := fileResumeData.BinaryMarshal()
1636 ft.TransferSize = offset
1638 replyT.Fields = append(replyT.Fields, NewField(FieldFileResumeData, b))
1641 res = append(res, replyT)
1645 func HandleSetClientUserInfo(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1646 if len(t.GetField(FieldUserIconID).Data) == 4 {
1647 cc.Icon = t.GetField(FieldUserIconID).Data[2:]
1649 cc.Icon = t.GetField(FieldUserIconID).Data
1651 if cc.Authorize(accessAnyName) {
1652 cc.UserName = t.GetField(FieldUserName).Data
1655 // the options field is only passed by the client versions > 1.2.3.
1656 options := t.GetField(FieldOptions).Data
1658 optBitmap := big.NewInt(int64(binary.BigEndian.Uint16(options)))
1659 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(cc.Flags)))
1661 flagBitmap.SetBit(flagBitmap, UserFlagRefusePM, optBitmap.Bit(refusePM))
1662 binary.BigEndian.PutUint16(cc.Flags, uint16(flagBitmap.Int64()))
1664 flagBitmap.SetBit(flagBitmap, UserFlagRefusePChat, optBitmap.Bit(refuseChat))
1665 binary.BigEndian.PutUint16(cc.Flags, uint16(flagBitmap.Int64()))
1667 // Check auto response
1668 if optBitmap.Bit(autoResponse) == 1 {
1669 cc.AutoReply = t.GetField(FieldAutomaticResponse).Data
1671 cc.AutoReply = []byte{}
1675 for _, c := range sortedClients(cc.Server.Clients) {
1676 res = append(res, *NewTransaction(
1677 TranNotifyChangeUser,
1679 NewField(FieldUserID, *cc.ID),
1680 NewField(FieldUserIconID, cc.Icon),
1681 NewField(FieldUserFlags, cc.Flags),
1682 NewField(FieldUserName, cc.UserName),
1689 // HandleKeepAlive responds to keepalive transactions with an empty reply
1690 // * HL 1.9.2 Client sends keepalive msg every 3 minutes
1691 // * HL 1.2.3 Client doesn't send keepalives
1692 func HandleKeepAlive(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1693 res = append(res, cc.NewReply(t))
1698 func HandleGetFileNameList(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1699 fullPath, err := readPath(
1700 cc.Server.Config.FileRoot,
1701 t.GetField(FieldFilePath).Data,
1709 if t.GetField(FieldFilePath).Data != nil {
1710 if _, err = fp.Write(t.GetField(FieldFilePath).Data); err != nil {
1715 // Handle special case for drop box folders
1716 if fp.IsDropbox() && !cc.Authorize(accessViewDropBoxes) {
1717 res = append(res, cc.NewErrReply(t, "You are not allowed to view drop boxes."))
1721 fileNames, err := getFileNameList(fullPath, cc.Server.Config.IgnoreFiles)
1726 res = append(res, cc.NewReply(t, fileNames...))
1731 // =================================
1732 // Hotline private chat flow
1733 // =================================
1734 // 1. ClientA sends TranInviteNewChat to server with user ID to invite
1735 // 2. Server creates new ChatID
1736 // 3. Server sends TranInviteToChat to invitee
1737 // 4. Server replies to ClientA with new Chat ID
1739 // A dialog box pops up in the invitee client with options to accept or decline the invitation.
1740 // If Accepted is clicked:
1741 // 1. ClientB sends TranJoinChat with FieldChatID
1743 // HandleInviteNewChat invites users to new private chat
1744 func HandleInviteNewChat(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1745 if !cc.Authorize(accessOpenChat) {
1746 res = append(res, cc.NewErrReply(t, "You are not allowed to request private chat."))
1751 targetID := t.GetField(FieldUserID).Data
1752 newChatID := cc.Server.NewPrivateChat(cc)
1754 // Check if target user has "Refuse private chat" flag
1755 binary.BigEndian.Uint16(targetID)
1756 targetClient := cc.Server.Clients[binary.BigEndian.Uint16(targetID)]
1758 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(targetClient.Flags)))
1759 if flagBitmap.Bit(UserFlagRefusePChat) == 1 {
1764 NewField(FieldData, []byte(string(targetClient.UserName)+" does not accept private chats.")),
1765 NewField(FieldUserName, targetClient.UserName),
1766 NewField(FieldUserID, *targetClient.ID),
1767 NewField(FieldOptions, []byte{0, 2}),
1775 NewField(FieldChatID, newChatID),
1776 NewField(FieldUserName, cc.UserName),
1777 NewField(FieldUserID, *cc.ID),
1784 NewField(FieldChatID, newChatID),
1785 NewField(FieldUserName, cc.UserName),
1786 NewField(FieldUserID, *cc.ID),
1787 NewField(FieldUserIconID, cc.Icon),
1788 NewField(FieldUserFlags, cc.Flags),
1795 func HandleInviteToChat(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1796 if !cc.Authorize(accessOpenChat) {
1797 res = append(res, cc.NewErrReply(t, "You are not allowed to request private chat."))
1802 targetID := t.GetField(FieldUserID).Data
1803 chatID := t.GetField(FieldChatID).Data
1809 NewField(FieldChatID, chatID),
1810 NewField(FieldUserName, cc.UserName),
1811 NewField(FieldUserID, *cc.ID),
1817 NewField(FieldChatID, chatID),
1818 NewField(FieldUserName, cc.UserName),
1819 NewField(FieldUserID, *cc.ID),
1820 NewField(FieldUserIconID, cc.Icon),
1821 NewField(FieldUserFlags, cc.Flags),
1828 func HandleRejectChatInvite(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1829 chatID := t.GetField(FieldChatID).Data
1830 chatInt := binary.BigEndian.Uint32(chatID)
1832 privChat := cc.Server.PrivateChats[chatInt]
1834 resMsg := append(cc.UserName, []byte(" declined invitation to chat")...)
1836 for _, c := range sortedClients(privChat.ClientConn) {
1841 NewField(FieldChatID, chatID),
1842 NewField(FieldData, resMsg),
1850 // HandleJoinChat is sent from a v1.8+ Hotline client when the joins a private chat
1851 // Fields used in the reply:
1852 // * 115 Chat subject
1853 // * 300 User name with info (Optional)
1854 // * 300 (more user names with info)
1855 func HandleJoinChat(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1856 chatID := t.GetField(FieldChatID).Data
1857 chatInt := binary.BigEndian.Uint32(chatID)
1859 privChat := cc.Server.PrivateChats[chatInt]
1861 // Send TranNotifyChatChangeUser to current members of the chat to inform of new user
1862 for _, c := range sortedClients(privChat.ClientConn) {
1865 TranNotifyChatChangeUser,
1867 NewField(FieldChatID, chatID),
1868 NewField(FieldUserName, cc.UserName),
1869 NewField(FieldUserID, *cc.ID),
1870 NewField(FieldUserIconID, cc.Icon),
1871 NewField(FieldUserFlags, cc.Flags),
1876 privChat.ClientConn[cc.uint16ID()] = cc
1878 replyFields := []Field{NewField(FieldChatSubject, []byte(privChat.Subject))}
1879 for _, c := range sortedClients(privChat.ClientConn) {
1884 Name: string(c.UserName),
1887 replyFields = append(replyFields, NewField(FieldUsernameWithInfo, user.Payload()))
1890 res = append(res, cc.NewReply(t, replyFields...))
1894 // HandleLeaveChat is sent from a v1.8+ Hotline client when the user exits a private chat
1895 // Fields used in the request:
1896 // - 114 FieldChatID
1898 // Reply is not expected.
1899 func HandleLeaveChat(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1900 chatID := t.GetField(FieldChatID).Data
1901 chatInt := binary.BigEndian.Uint32(chatID)
1903 privChat, ok := cc.Server.PrivateChats[chatInt]
1908 delete(privChat.ClientConn, cc.uint16ID())
1910 // Notify members of the private chat that the user has left
1911 for _, c := range sortedClients(privChat.ClientConn) {
1914 TranNotifyChatDeleteUser,
1916 NewField(FieldChatID, chatID),
1917 NewField(FieldUserID, *cc.ID),
1925 // HandleSetChatSubject is sent from a v1.8+ Hotline client when the user sets a private chat subject
1926 // Fields used in the request:
1928 // * 115 Chat subject
1929 // Reply is not expected.
1930 func HandleSetChatSubject(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1931 chatID := t.GetField(FieldChatID).Data
1932 chatInt := binary.BigEndian.Uint32(chatID)
1934 privChat := cc.Server.PrivateChats[chatInt]
1935 privChat.Subject = string(t.GetField(FieldChatSubject).Data)
1937 for _, c := range sortedClients(privChat.ClientConn) {
1940 TranNotifyChatSubject,
1942 NewField(FieldChatID, chatID),
1943 NewField(FieldChatSubject, t.GetField(FieldChatSubject).Data),
1951 // HandleMakeAlias makes a file alias using the specified path.
1952 // Fields used in the request:
1955 // 212 File new path Destination path
1957 // Fields used in the reply:
1959 func HandleMakeAlias(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1960 if !cc.Authorize(accessMakeAlias) {
1961 res = append(res, cc.NewErrReply(t, "You are not allowed to make aliases."))
1964 fileName := t.GetField(FieldFileName).Data
1965 filePath := t.GetField(FieldFilePath).Data
1966 fileNewPath := t.GetField(FieldFileNewPath).Data
1968 fullFilePath, err := readPath(cc.Server.Config.FileRoot, filePath, fileName)
1973 fullNewFilePath, err := readPath(cc.Server.Config.FileRoot, fileNewPath, fileName)
1978 cc.logger.Debugw("Make alias", "src", fullFilePath, "dst", fullNewFilePath)
1980 if err := cc.Server.FS.Symlink(fullFilePath, fullNewFilePath); err != nil {
1981 res = append(res, cc.NewErrReply(t, "Error creating alias"))
1985 res = append(res, cc.NewReply(t))
1989 // HandleDownloadBanner handles requests for a new banner from the server
1990 // Fields used in the request:
1992 // Fields used in the reply:
1993 // 107 FieldRefNum Used later for transfer
1994 // 108 FieldTransferSize Size of data to be downloaded
1995 func HandleDownloadBanner(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1996 fi, err := cc.Server.FS.Stat(filepath.Join(cc.Server.ConfigDir, cc.Server.Config.BannerFile))
2001 ft := cc.newFileTransfer(bannerDownload, []byte{}, []byte{}, make([]byte, 4))
2003 binary.BigEndian.PutUint32(ft.TransferSize, uint32(fi.Size()))
2005 res = append(res, cc.NewReply(t,
2006 NewField(FieldRefNum, ft.refNum[:]),
2007 NewField(FieldTransferSize, ft.TransferSize),