8 "github.com/davecgh/go-spew/spew"
18 type TransactionType struct {
19 Access int // Specifies access privilege required to perform the transaction
20 DenyMsg string // The error reply message when user does not have access
21 Handler func(*ClientConn, *Transaction) ([]Transaction, error) // 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",
45 Access: accessAlwaysAllow,
47 Handler: HandleTranAgreed,
50 Access: accessSendChat,
51 DenyMsg: "You are not allowed to participate in chat.",
52 Handler: HandleChatSend,
54 RequiredFields: []requiredField{
62 Access: accessNewsDeleteArt,
63 DenyMsg: "You are not allowed to delete news articles.",
64 Name: "tranDelNewsArt",
65 Handler: HandleDelNewsArt,
68 Access: accessAlwaysAllow, // Granular access enforced inside the handler
69 // Has multiple access flags: News Delete Folder (37) or News Delete Category (35)
70 // TODO: Implement inside the handler
71 Name: "tranDelNewsItem",
72 Handler: HandleDelNewsItem,
75 Access: accessAlwaysAllow, // Granular access enforced inside the handler
76 Name: "tranDeleteFile",
77 Handler: HandleDeleteFile,
80 Access: accessDeleteUser,
81 DenyMsg: "You are not allowed to delete accounts.",
82 Name: "tranDeleteUser",
83 Handler: HandleDeleteUser,
86 Access: accessDisconUser,
87 DenyMsg: "You are not allowed to disconnect users.",
88 Name: "tranDisconnectUser",
89 Handler: HandleDisconnectUser,
92 Access: accessDownloadFile,
93 DenyMsg: "You are not allowed to download files.",
94 Name: "tranDownloadFile",
95 Handler: HandleDownloadFile,
98 Access: accessDownloadFile, // There is no specific access flag for folder vs file download
99 DenyMsg: "You are not allowed to download files.",
100 Name: "tranDownloadFldr",
101 Handler: HandleDownloadFolder,
103 tranGetClientInfoText: {
104 Access: accessGetClientInfo,
105 DenyMsg: "You are not allowed to get client info",
106 Name: "tranGetClientInfoText",
107 Handler: HandleGetClientConnInfoText,
110 Access: accessAlwaysAllow,
111 Name: "tranGetFileInfo",
112 Handler: HandleGetFileInfo,
114 tranGetFileNameList: {
115 Access: accessAlwaysAllow,
116 Name: "tranGetFileNameList",
117 Handler: HandleGetFileNameList,
121 Handler: HandleGetMsgs,
123 tranGetNewsArtData: {
124 Name: "tranGetNewsArtData",
125 Handler: HandleGetNewsArtData,
127 tranGetNewsArtNameList: {
128 Name: "tranGetNewsArtNameList",
129 Handler: HandleGetNewsArtNameList,
131 tranGetNewsCatNameList: {
132 Name: "tranGetNewsCatNameList",
133 Handler: HandleGetNewsCatNameList,
136 Access: accessOpenUser,
137 DenyMsg: "You are not allowed to view accounts.",
139 Handler: HandleGetUser,
141 tranGetUserNameList: {
142 Access: accessAlwaysAllow,
143 Name: "tranHandleGetUserNameList",
144 Handler: HandleGetUserNameList,
147 Access: accessOpenChat,
148 DenyMsg: "You are not allowed to request private chat.",
149 Name: "tranInviteNewChat",
150 Handler: HandleInviteNewChat,
153 Name: "tranInviteToChat",
154 Handler: HandleInviteToChat,
157 Name: "tranJoinChat",
158 Handler: HandleJoinChat,
161 Name: "tranKeepAlive",
162 Handler: HandleKeepAlive,
165 Name: "tranJoinChat",
166 Handler: HandleLeaveChat,
168 tranNotifyDeleteUser: {
169 Name: "tranNotifyDeleteUser",
172 Access: accessOpenUser,
173 DenyMsg: "You are not allowed to view accounts.",
174 Name: "tranListUsers",
175 Handler: HandleListUsers,
178 Access: accessMoveFile,
179 DenyMsg: "You are not allowed to move files.",
180 Name: "tranMoveFile",
181 Handler: HandleMoveFile,
184 Name: "tranNewFolder",
185 Handler: HandleNewFolder,
188 Name: "tranNewNewsCat",
189 Handler: HandleNewNewsCat,
192 Name: "tranNewNewsFldr",
193 Handler: HandleNewNewsFldr,
196 Access: accessCreateUser,
197 DenyMsg: "You are not allowed to create new accounts.",
199 Handler: HandleNewUser,
202 Name: "tranOldPostNews",
203 Handler: HandleTranOldPostNews,
206 Access: accessNewsPostArt,
207 DenyMsg: "You are not allowed to post news articles.",
208 Name: "tranPostNewsArt",
209 Handler: HandlePostNewsArt,
211 tranRejectChatInvite: {
212 Name: "tranRejectChatInvite",
213 Handler: HandleRejectChatInvite,
215 tranSendInstantMsg: {
216 //Access: accessSendPrivMsg,
217 //DenyMsg: "You are not allowed to send private messages",
218 Name: "tranSendInstantMsg",
219 Handler: HandleSendInstantMsg,
220 RequiredFields: []requiredField{
230 tranSetChatSubject: {
231 Name: "tranSetChatSubject",
232 Handler: HandleSetChatSubject,
234 tranSetClientUserInfo: {
235 Access: accessAlwaysAllow,
236 Name: "tranSetClientUserInfo",
237 Handler: HandleSetClientUserInfo,
240 Name: "tranSetFileInfo",
241 Handler: HandleSetFileInfo,
244 Access: accessModifyUser,
245 DenyMsg: "You are not allowed to modify accounts.",
247 Handler: HandleSetUser,
250 Access: accessUploadFile,
251 DenyMsg: "You are not allowed to upload files.",
252 Name: "tranUploadFile",
253 Handler: HandleUploadFile,
256 Name: "tranUploadFldr",
257 Handler: HandleUploadFolder,
260 Access: accessBroadcast,
261 DenyMsg: "You are not allowed to send broadcast messages.",
262 Name: "tranUserBroadcast",
263 Handler: HandleUserBroadcast,
267 func HandleChatSend(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
268 // Truncate long usernames
269 trunc := fmt.Sprintf("%13s", *cc.UserName)
270 formattedMsg := fmt.Sprintf("\r%.14s: %s", trunc, t.GetField(fieldData).Data)
272 // By holding the option key, Hotline chat allows users to send /me formatted messages like:
273 // *** Halcyon does stuff
274 // This is indicated by the presence of the optional field fieldChatOptions in the transaction payload
275 if t.GetField(fieldChatOptions).Data != nil {
276 formattedMsg = fmt.Sprintf("*** %s %s\r", *cc.UserName, t.GetField(fieldData).Data)
279 if bytes.Equal(t.GetField(fieldData).Data, []byte("/stats")) {
280 formattedMsg = strings.Replace(cc.Server.Stats.String(), "\n", "\r", -1)
283 chatID := t.GetField(fieldChatID).Data
284 // a non-nil chatID indicates the message belongs to a private chat
286 chatInt := binary.BigEndian.Uint32(chatID)
287 privChat := cc.Server.PrivateChats[chatInt]
289 // send the message to all connected clients of the private chat
290 for _, c := range privChat.ClientConn {
291 res = append(res, *NewTransaction(
294 NewField(fieldChatID, chatID),
295 NewField(fieldData, []byte(formattedMsg)),
301 for _, c := range sortedClients(cc.Server.Clients) {
302 // Filter out clients that do not have the read chat permission
303 if authorize(c.Account.Access, accessReadChat) {
304 res = append(res, *NewTransaction(tranChatMsg, c.ID, NewField(fieldData, []byte(formattedMsg))))
311 // HandleSendInstantMsg sends instant message to the user on the current server.
312 // Fields used in the request:
315 // One of the following values:
316 // - User message (myOpt_UserMessage = 1)
317 // - Refuse message (myOpt_RefuseMessage = 2)
318 // - Refuse chat (myOpt_RefuseChat = 3)
319 // - Automatic response (myOpt_AutomaticResponse = 4)"
321 // 214 Quoting message Optional
323 //Fields used in the reply:
325 func HandleSendInstantMsg(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
326 msg := t.GetField(fieldData)
327 ID := t.GetField(fieldUserID)
328 // TODO: Implement reply quoting
329 //options := transaction.GetField(hotline.fieldOptions)
335 NewField(fieldData, msg.Data),
336 NewField(fieldUserName, *cc.UserName),
337 NewField(fieldUserID, *cc.ID),
338 NewField(fieldOptions, []byte{0, 1}),
341 id, _ := byteToInt(ID.Data)
343 //keys := make([]uint16, 0, len(cc.Server.Clients))
344 //for k := range cc.Server.Clients {
345 // keys = append(keys, k)
348 otherClient := cc.Server.Clients[uint16(id)]
349 if otherClient == nil {
350 return res, errors.New("ohno")
353 // Respond with auto reply if other client has it enabled
354 if len(*otherClient.AutoReply) > 0 {
359 NewField(fieldData, *otherClient.AutoReply),
360 NewField(fieldUserName, *otherClient.UserName),
361 NewField(fieldUserID, *otherClient.ID),
362 NewField(fieldOptions, []byte{0, 1}),
367 res = append(res, cc.NewReply(t))
372 func HandleGetFileInfo(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
373 fileName := string(t.GetField(fieldFileName).Data)
374 filePath := cc.Server.Config.FileRoot + ReadFilePath(t.GetField(fieldFilePath).Data)
375 spew.Dump(cc.Server.Config.FileRoot)
377 ffo, err := NewFlattenedFileObject(filePath, fileName)
382 res = append(res, cc.NewReply(t,
383 NewField(fieldFileName, []byte(fileName)),
384 NewField(fieldFileTypeString, ffo.FlatFileInformationFork.TypeSignature),
385 NewField(fieldFileCreatorString, ffo.FlatFileInformationFork.CreatorSignature),
386 NewField(fieldFileComment, ffo.FlatFileInformationFork.Comment),
387 NewField(fieldFileType, ffo.FlatFileInformationFork.TypeSignature),
388 NewField(fieldFileCreateDate, ffo.FlatFileInformationFork.CreateDate),
389 NewField(fieldFileModifyDate, ffo.FlatFileInformationFork.ModifyDate),
390 NewField(fieldFileSize, ffo.FlatFileDataForkHeader.DataSize),
395 // HandleSetFileInfo updates a file or folder name and/or comment from the Get Info window
396 // TODO: Implement support for comments
397 // Fields used in the request:
399 // * 202 File path Optional
400 // * 211 File new name Optional
401 // * 210 File comment Optional
402 // Fields used in the reply: None
403 func HandleSetFileInfo(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
404 fileName := string(t.GetField(fieldFileName).Data)
405 filePath := cc.Server.Config.FileRoot + ReadFilePath(t.GetField(fieldFilePath).Data)
406 //fileComment := t.GetField(fieldFileComment).Data
407 fileNewName := t.GetField(fieldFileNewName).Data
409 if fileNewName != nil {
410 path := filePath + "/" + fileName
411 fi, err := os.Stat(path)
415 switch mode := fi.Mode(); {
417 if !authorize(cc.Account.Access, accessRenameFolder) {
418 res = append(res, cc.NewErrReply(t, "You are not allowed to rename folders."))
421 case mode.IsRegular():
422 if !authorize(cc.Account.Access, accessRenameFile) {
423 res = append(res, cc.NewErrReply(t, "You are not allowed to rename files."))
428 err = os.Rename(filePath+"/"+fileName, filePath+"/"+string(fileNewName))
429 if os.IsNotExist(err) {
430 res = append(res, cc.NewErrReply(t, "Cannot rename file "+fileName+" because it does not exist or cannot be found."))
435 res = append(res, cc.NewReply(t))
439 // HandleDeleteFile deletes a file or folder
440 // Fields used in the request:
443 // Fields used in the reply: none
444 func HandleDeleteFile(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
445 fileName := string(t.GetField(fieldFileName).Data)
446 filePath := cc.Server.Config.FileRoot + ReadFilePath(t.GetField(fieldFilePath).Data)
448 path := "./" + filePath + "/" + fileName
450 cc.Server.Logger.Debugw("Delete file", "src", filePath+"/"+fileName)
452 fi, err := os.Stat(path)
454 res = append(res, cc.NewErrReply(t, "Cannot delete file "+fileName+" because it does not exist or cannot be found."))
457 switch mode := fi.Mode(); {
459 if !authorize(cc.Account.Access, accessDeleteFolder) {
460 res = append(res, cc.NewErrReply(t, "You are not allowed to delete folders."))
463 case mode.IsRegular():
464 if !authorize(cc.Account.Access, accessDeleteFile) {
465 res = append(res, cc.NewErrReply(t, "You are not allowed to delete files."))
470 if err := os.RemoveAll(path); err != nil {
474 res = append(res, cc.NewReply(t))
478 // HandleMoveFile moves files or folders. Note: seemingly not documented
479 func HandleMoveFile(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
480 fileName := string(t.GetField(fieldFileName).Data)
481 filePath := "./" + cc.Server.Config.FileRoot + ReadFilePath(t.GetField(fieldFilePath).Data)
482 fileNewPath := "./" + cc.Server.Config.FileRoot + ReadFilePath(t.GetField(fieldFileNewPath).Data)
484 cc.Server.Logger.Debugw("Move file", "src", filePath+"/"+fileName, "dst", fileNewPath+"/"+fileName)
486 path := filePath + "/" + fileName
487 fi, err := os.Stat(path)
491 switch mode := fi.Mode(); {
493 if !authorize(cc.Account.Access, accessMoveFolder) {
494 res = append(res, cc.NewErrReply(t, "You are not allowed to move folders."))
497 case mode.IsRegular():
498 if !authorize(cc.Account.Access, accessMoveFile) {
499 res = append(res, cc.NewErrReply(t, "You are not allowed to move files."))
504 err = os.Rename(filePath+"/"+fileName, fileNewPath+"/"+fileName)
505 if os.IsNotExist(err) {
506 res = append(res, cc.NewErrReply(t, "Cannot delete file "+fileName+" because it does not exist or cannot be found."))
510 return []Transaction{}, err
512 // TODO: handle other possible errors; e.g. file delete fails due to file permission issue
514 res = append(res, cc.NewReply(t))
518 func HandleNewFolder(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
519 newFolderPath := cc.Server.Config.FileRoot
521 // fieldFilePath is only present for nested paths
522 if t.GetField(fieldFilePath).Data != nil {
523 newFp := NewFilePath(t.GetField(fieldFilePath).Data)
524 newFolderPath += newFp.String()
526 newFolderPath += "/" + string(t.GetField(fieldFileName).Data)
528 if err := os.Mkdir(newFolderPath, 0777); err != nil {
529 // TODO: Send error response to client
530 return []Transaction{}, err
533 res = append(res, cc.NewReply(t))
537 func HandleSetUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
538 login := DecodeUserString(t.GetField(fieldUserLogin).Data)
539 userName := string(t.GetField(fieldUserName).Data)
541 newAccessLvl := t.GetField(fieldUserAccess).Data
543 account := cc.Server.Accounts[login]
544 account.Access = &newAccessLvl
545 account.Name = userName
547 // If the password field is cleared in the Hotline edit user UI, the SetUser transaction does
548 // not include fieldUserPassword
549 if t.GetField(fieldUserPassword).Data == nil {
550 account.Password = hashAndSalt([]byte(""))
552 if len(t.GetField(fieldUserPassword).Data) > 1 {
553 account.Password = hashAndSalt(t.GetField(fieldUserPassword).Data)
556 file := cc.Server.ConfigDir + "Users/" + login + ".yaml"
557 out, err := yaml.Marshal(&account)
561 if err := ioutil.WriteFile(file, out, 0666); err != nil {
565 // Notify connected clients logged in as the user of the new access level
566 for _, c := range cc.Server.Clients {
567 if c.Account.Login == login {
568 // Note: comment out these two lines to test server-side deny messages
569 newT := NewTransaction(tranUserAccess, c.ID, NewField(fieldUserAccess, newAccessLvl))
570 res = append(res, *newT)
572 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(*c.Flags)))
573 if authorize(c.Account.Access, accessDisconUser) {
574 flagBitmap.SetBit(flagBitmap, userFlagAdmin, 1)
576 flagBitmap.SetBit(flagBitmap, userFlagAdmin, 0)
578 binary.BigEndian.PutUint16(*c.Flags, uint16(flagBitmap.Int64()))
580 c.Account.Access = account.Access
583 tranNotifyChangeUser,
584 NewField(fieldUserID, *c.ID),
585 NewField(fieldUserFlags, *c.Flags),
586 NewField(fieldUserName, *c.UserName),
587 NewField(fieldUserIconID, *c.Icon),
592 // TODO: If we have just promoted a connected user to admin, notify
593 // connected clients to turn the user red
595 res = append(res, cc.NewReply(t))
599 func HandleGetUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
600 userLogin := string(t.GetField(fieldUserLogin).Data)
601 decodedUserLogin := NegatedUserString(t.GetField(fieldUserLogin).Data)
602 account := cc.Server.Accounts[userLogin]
604 errorT := cc.NewErrReply(t, "Account does not exist.")
605 res = append(res, errorT)
609 res = append(res, cc.NewReply(t,
610 NewField(fieldUserName, []byte(account.Name)),
611 NewField(fieldUserLogin, []byte(decodedUserLogin)),
612 NewField(fieldUserPassword, []byte(account.Password)),
613 NewField(fieldUserAccess, *account.Access),
618 func HandleListUsers(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
619 var userFields []Field
620 // TODO: make order deterministic
621 for _, acc := range cc.Server.Accounts {
622 userField := acc.Payload()
623 userFields = append(userFields, NewField(fieldData, userField))
626 res = append(res, cc.NewReply(t, userFields...))
630 // HandleNewUser creates a new user account
631 func HandleNewUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
632 login := DecodeUserString(t.GetField(fieldUserLogin).Data)
634 // If the account already exists, reply with an error
635 // TODO: make order deterministic
636 if _, ok := cc.Server.Accounts[login]; ok {
637 res = append(res, cc.NewErrReply(t, "Cannot create account "+login+" because there is already an account with that login."))
641 if err := cc.Server.NewUser(
643 string(t.GetField(fieldUserName).Data),
644 string(t.GetField(fieldUserPassword).Data),
645 t.GetField(fieldUserAccess).Data,
647 return []Transaction{}, err
650 res = append(res, cc.NewReply(t))
654 func HandleDeleteUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
655 // TODO: Handle case where account doesn't exist; e.g. delete race condition
656 login := DecodeUserString(t.GetField(fieldUserLogin).Data)
658 if err := cc.Server.DeleteUser(login); err != nil {
662 res = append(res, cc.NewReply(t))
666 // HandleUserBroadcast sends an Administrator Message to all connected clients of the server
667 func HandleUserBroadcast(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
670 NewField(fieldData, t.GetField(tranGetMsgs).Data),
671 NewField(fieldChatOptions, []byte{0}),
674 res = append(res, cc.NewReply(t))
678 func byteToInt(bytes []byte) (int, error) {
681 return int(binary.BigEndian.Uint16(bytes)), nil
683 return int(binary.BigEndian.Uint32(bytes)), nil
686 return 0, errors.New("unknown byte length")
689 func HandleGetClientConnInfoText(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
690 clientID, _ := byteToInt(t.GetField(fieldUserID).Data)
692 clientConn := cc.Server.Clients[uint16(clientID)]
693 if clientConn == nil {
694 return res, errors.New("invalid client")
697 // TODO: Implement non-hardcoded values
698 template := `Nickname: %s
703 -------- File Downloads ---------
707 ------- Folder Downloads --------
711 --------- File Uploads ----------
715 -------- Folder Uploads ---------
719 ------- Waiting Downloads -------
725 activeDownloads := clientConn.Transfers[FileDownload]
726 activeDownloadList := "None."
727 for _, dl := range activeDownloads {
728 activeDownloadList += dl.String() + "\n"
731 template = fmt.Sprintf(
733 *clientConn.UserName,
734 clientConn.Account.Name,
735 clientConn.Account.Login,
736 clientConn.Connection.RemoteAddr().String(),
739 template = strings.Replace(template, "\n", "\r", -1)
741 res = append(res, cc.NewReply(t,
742 NewField(fieldData, []byte(template)),
743 NewField(fieldUserName, *clientConn.UserName),
748 func HandleGetUserNameList(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
749 res = append(res, cc.NewReply(t, cc.Server.connectedUsers()...))
754 func (cc *ClientConn) notifyNewUserHasJoined() (res []Transaction, err error) {
755 // Notify other ccs that a new user has connected
758 tranNotifyChangeUser, nil,
759 NewField(fieldUserName, *cc.UserName),
760 NewField(fieldUserID, *cc.ID),
761 NewField(fieldUserIconID, *cc.Icon),
762 NewField(fieldUserFlags, *cc.Flags),
769 func HandleTranAgreed(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
770 bs := make([]byte, 2)
771 binary.BigEndian.PutUint16(bs, *cc.Server.NextGuestID)
773 *cc.UserName = t.GetField(fieldUserName).Data
775 *cc.Icon = t.GetField(fieldUserIconID).Data
777 options := t.GetField(fieldOptions).Data
778 optBitmap := big.NewInt(int64(binary.BigEndian.Uint16(options)))
780 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(*cc.Flags)))
782 // Check refuse private PM option
783 if optBitmap.Bit(refusePM) == 1 {
784 flagBitmap.SetBit(flagBitmap, userFlagRefusePM, 1)
785 binary.BigEndian.PutUint16(*cc.Flags, uint16(flagBitmap.Int64()))
788 // Check refuse private chat option
789 if optBitmap.Bit(refuseChat) == 1 {
790 flagBitmap.SetBit(flagBitmap, userFLagRefusePChat, 1)
791 binary.BigEndian.PutUint16(*cc.Flags, uint16(flagBitmap.Int64()))
794 // Check auto response
795 if optBitmap.Bit(autoResponse) == 1 {
796 *cc.AutoReply = t.GetField(fieldAutomaticResponse).Data
798 *cc.AutoReply = []byte{}
801 _, _ = cc.notifyNewUserHasJoined()
803 res = append(res, cc.NewReply(t))
808 const defaultNewsDateFormat = "Jan02 15:04" // Jun23 20:49
809 // "Mon, 02 Jan 2006 15:04:05 MST"
811 const defaultNewsTemplate = `From %s (%s):
815 __________________________________________________________`
817 // HandleTranOldPostNews updates the flat news
818 // Fields used in this request:
820 func HandleTranOldPostNews(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
821 cc.Server.flatNewsMux.Lock()
822 defer cc.Server.flatNewsMux.Unlock()
824 newsDateTemplate := defaultNewsDateFormat
825 if cc.Server.Config.NewsDateFormat != "" {
826 newsDateTemplate = cc.Server.Config.NewsDateFormat
829 newsTemplate := defaultNewsTemplate
830 if cc.Server.Config.NewsDelimiter != "" {
831 newsTemplate = cc.Server.Config.NewsDelimiter
834 newsPost := fmt.Sprintf(newsTemplate+"\r", *cc.UserName, time.Now().Format(newsDateTemplate), t.GetField(fieldData).Data)
835 newsPost = strings.Replace(newsPost, "\n", "\r", -1)
837 // update news in memory
838 cc.Server.FlatNews = append([]byte(newsPost), cc.Server.FlatNews...)
840 // update news on disk
841 if err := ioutil.WriteFile(cc.Server.ConfigDir+"MessageBoard.txt", cc.Server.FlatNews, 0644); err != nil {
845 // Notify all clients of updated news
848 NewField(fieldData, []byte(newsPost)),
851 res = append(res, cc.NewReply(t))
855 func HandleDisconnectUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
856 clientConn := cc.Server.Clients[binary.BigEndian.Uint16(t.GetField(fieldUserID).Data)]
858 if authorize(clientConn.Account.Access, accessCannotBeDiscon) {
859 res = append(res, cc.NewErrReply(t, clientConn.Account.Login+" is not allowed to be disconnected."))
863 if err := clientConn.Connection.Close(); err != nil {
867 res = append(res, cc.NewReply(t))
871 func HandleGetNewsCatNameList(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
872 // Fields used in the request:
873 // 325 News path (Optional)
875 newsPath := t.GetField(fieldNewsPath).Data
876 cc.Server.Logger.Infow("NewsPath: ", "np", string(newsPath))
878 pathStrs := ReadNewsPath(t.GetField(fieldNewsPath).Data)
879 cats := cc.Server.GetNewsCatByPath(pathStrs)
881 // To store the keys in slice in sorted order
882 keys := make([]string, len(cats))
884 for k := range cats {
890 var fieldData []Field
891 for _, k := range keys {
893 fieldData = append(fieldData, NewField(
894 fieldNewsCatListData15,
899 res = append(res, cc.NewReply(t, fieldData...))
903 func HandleNewNewsCat(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
904 name := string(t.GetField(fieldNewsCatName).Data)
905 pathStrs := ReadNewsPath(t.GetField(fieldNewsPath).Data)
907 cats := cc.Server.GetNewsCatByPath(pathStrs)
908 cats[name] = NewsCategoryListData15{
911 Articles: map[uint32]*NewsArtData{},
912 SubCats: make(map[string]NewsCategoryListData15),
915 if err := cc.Server.writeThreadedNews(); err != nil {
918 res = append(res, cc.NewReply(t))
922 func HandleNewNewsFldr(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
923 // Fields used in the request:
924 // 322 News category name
926 name := string(t.GetField(fieldFileName).Data)
927 pathStrs := ReadNewsPath(t.GetField(fieldNewsPath).Data)
929 cc.Server.Logger.Infof("Creating new news folder %s", name)
931 cats := cc.Server.GetNewsCatByPath(pathStrs)
932 cats[name] = NewsCategoryListData15{
935 Articles: map[uint32]*NewsArtData{},
936 SubCats: make(map[string]NewsCategoryListData15),
938 if err := cc.Server.writeThreadedNews(); err != nil {
941 res = append(res, cc.NewReply(t))
945 // Fields used in the request:
946 // 325 News path Optional
949 // 321 News article list data Optional
950 func HandleGetNewsArtNameList(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
951 pathStrs := ReadNewsPath(t.GetField(fieldNewsPath).Data)
953 var cat NewsCategoryListData15
954 cats := cc.Server.ThreadedNews.Categories
956 for _, path := range pathStrs {
958 cats = cats[path].SubCats
961 nald := cat.GetNewsArtListData()
963 res = append(res, cc.NewReply(t, NewField(fieldNewsArtListData, nald.Payload())))
967 func HandleGetNewsArtData(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
970 // 326 News article ID
971 // 327 News article data flavor
973 pathStrs := ReadNewsPath(t.GetField(fieldNewsPath).Data)
975 var cat NewsCategoryListData15
976 cats := cc.Server.ThreadedNews.Categories
978 for _, path := range pathStrs {
980 cats = cats[path].SubCats
982 newsArtID := t.GetField(fieldNewsArtID).Data
984 convertedArtID := binary.BigEndian.Uint16(newsArtID)
986 art := cat.Articles[uint32(convertedArtID)]
988 res = append(res, cc.NewReply(t))
993 // 328 News article title
994 // 329 News article poster
995 // 330 News article date
996 // 331 Previous article ID
997 // 332 Next article ID
998 // 335 Parent article ID
999 // 336 First child article ID
1000 // 327 News article data flavor "Should be “text/plain”
1001 // 333 News article data Optional (if data flavor is “text/plain”)
1003 res = append(res, cc.NewReply(t,
1004 NewField(fieldNewsArtTitle, []byte(art.Title)),
1005 NewField(fieldNewsArtPoster, []byte(art.Poster)),
1006 NewField(fieldNewsArtDate, art.Date),
1007 NewField(fieldNewsArtPrevArt, art.PrevArt),
1008 NewField(fieldNewsArtNextArt, art.NextArt),
1009 NewField(fieldNewsArtParentArt, art.ParentArt),
1010 NewField(fieldNewsArt1stChildArt, art.FirstChildArt),
1011 NewField(fieldNewsArtDataFlav, []byte("text/plain")),
1012 NewField(fieldNewsArtData, []byte(art.Data)),
1017 func HandleDelNewsItem(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1018 // Access: News Delete Folder (37) or News Delete Category (35)
1020 pathStrs := ReadNewsPath(t.GetField(fieldNewsPath).Data)
1022 // TODO: determine if path is a Folder (Bundle) or Category and check for permission
1024 cc.Server.Logger.Infof("DelNewsItem %v", pathStrs)
1026 cats := cc.Server.ThreadedNews.Categories
1028 delName := pathStrs[len(pathStrs)-1]
1029 if len(pathStrs) > 1 {
1030 for _, path := range pathStrs[0 : len(pathStrs)-1] {
1031 cats = cats[path].SubCats
1035 delete(cats, delName)
1037 err = cc.Server.writeThreadedNews()
1042 // Reply params: none
1043 res = append(res, cc.NewReply(t))
1048 func HandleDelNewsArt(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1051 // 326 News article ID
1052 // 337 News article – recursive delete Delete child articles (1) or not (0)
1053 pathStrs := ReadNewsPath(t.GetField(fieldNewsPath).Data)
1054 ID := binary.BigEndian.Uint16(t.GetField(fieldNewsArtID).Data)
1056 // TODO: Delete recursive
1057 cats := cc.Server.GetNewsCatByPath(pathStrs[:len(pathStrs)-1])
1059 catName := pathStrs[len(pathStrs)-1]
1060 cat := cats[catName]
1062 delete(cat.Articles, uint32(ID))
1065 if err := cc.Server.writeThreadedNews(); err != nil {
1069 res = append(res, cc.NewReply(t))
1073 func HandlePostNewsArt(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1076 // 326 News article ID ID of the parent article?
1077 // 328 News article title
1078 // 334 News article flags
1079 // 327 News article data flavor Currently “text/plain”
1080 // 333 News article data
1082 pathStrs := ReadNewsPath(t.GetField(fieldNewsPath).Data)
1083 cats := cc.Server.GetNewsCatByPath(pathStrs[:len(pathStrs)-1])
1085 catName := pathStrs[len(pathStrs)-1]
1086 cat := cats[catName]
1088 newArt := NewsArtData{
1089 Title: string(t.GetField(fieldNewsArtTitle).Data),
1090 Poster: string(*cc.UserName),
1092 PrevArt: []byte{0, 0, 0, 0},
1093 NextArt: []byte{0, 0, 0, 0},
1094 ParentArt: append([]byte{0, 0}, t.GetField(fieldNewsArtID).Data...),
1095 FirstChildArt: []byte{0, 0, 0, 0},
1096 DataFlav: []byte("text/plain"),
1097 Data: string(t.GetField(fieldNewsArtData).Data),
1101 for k := range cat.Articles {
1102 keys = append(keys, int(k))
1108 prevID := uint32(keys[len(keys)-1])
1111 binary.BigEndian.PutUint32(newArt.PrevArt, prevID)
1113 // Set next article ID
1114 binary.BigEndian.PutUint32(cat.Articles[prevID].NextArt, nextID)
1117 // Update parent article with first child reply
1118 parentID := binary.BigEndian.Uint16(t.GetField(fieldNewsArtID).Data)
1120 parentArt := cat.Articles[uint32(parentID)]
1122 if bytes.Equal(parentArt.FirstChildArt, []byte{0, 0, 0, 0}) {
1123 binary.BigEndian.PutUint32(parentArt.FirstChildArt, nextID)
1127 cat.Articles[nextID] = &newArt
1130 if err := cc.Server.writeThreadedNews(); err != nil {
1134 res = append(res, cc.NewReply(t))
1138 // HandleGetMsgs returns the flat news data
1139 func HandleGetMsgs(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1140 res = append(res, cc.NewReply(t, NewField(fieldData, cc.Server.FlatNews)))
1145 func HandleDownloadFile(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1146 fileName := t.GetField(fieldFileName).Data
1147 filePath := ReadFilePath(t.GetField(fieldFilePath).Data)
1149 ffo, err := NewFlattenedFileObject(cc.Server.Config.FileRoot+filePath, string(fileName))
1154 transactionRef := cc.Server.NewTransactionRef()
1155 data := binary.BigEndian.Uint32(transactionRef)
1157 cc.Server.Logger.Infow("File download", "path", filePath)
1159 ft := &FileTransfer{
1161 FilePath: []byte(filePath),
1162 ReferenceNumber: transactionRef,
1166 cc.Server.FileTransfers[data] = ft
1167 cc.Transfers[FileDownload] = append(cc.Transfers[FileDownload], ft)
1169 res = append(res, cc.NewReply(t,
1170 NewField(fieldRefNum, transactionRef),
1171 NewField(fieldWaitingCount, []byte{0x00, 0x00}), // TODO: Implement waiting count
1172 NewField(fieldTransferSize, ffo.TransferSize()),
1173 NewField(fieldFileSize, ffo.FlatFileDataForkHeader.DataSize),
1179 // Download all files from the specified folder and sub-folders
1192 // 00 6c // transfer size
1196 // 00 dc // field Folder item count
1200 // 00 6b // ref number
1203 func HandleDownloadFolder(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1204 transactionRef := cc.Server.NewTransactionRef()
1205 data := binary.BigEndian.Uint32(transactionRef)
1207 fileTransfer := &FileTransfer{
1208 FileName: t.GetField(fieldFileName).Data,
1209 FilePath: t.GetField(fieldFilePath).Data,
1210 ReferenceNumber: transactionRef,
1211 Type: FolderDownload,
1213 cc.Server.FileTransfers[data] = fileTransfer
1214 cc.Transfers[FolderDownload] = append(cc.Transfers[FolderDownload], fileTransfer)
1216 fp := NewFilePath(t.GetField(fieldFilePath).Data)
1218 fullFilePath := fmt.Sprintf("./%v/%v", cc.Server.Config.FileRoot+fp.String(), string(fileTransfer.FileName))
1219 transferSize, err := CalcTotalSize(fullFilePath)
1223 itemCount, err := CalcItemCount(fullFilePath)
1227 res = append(res, cc.NewReply(t,
1228 NewField(fieldRefNum, transactionRef),
1229 NewField(fieldTransferSize, transferSize),
1230 NewField(fieldFolderItemCount, itemCount),
1231 NewField(fieldWaitingCount, []byte{0x00, 0x00}), // TODO: Implement waiting count
1236 // Upload all files from the local folder and its subfolders to the specified path on the server
1237 // Fields used in the request
1240 // 108 Transfer size Total size of all items in the folder
1241 // 220 Folder item count
1242 // 204 File transfer options "Optional Currently set to 1" (TODO: ??)
1243 func HandleUploadFolder(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1244 transactionRef := cc.Server.NewTransactionRef()
1245 data := binary.BigEndian.Uint32(transactionRef)
1247 fileTransfer := &FileTransfer{
1248 FileName: t.GetField(fieldFileName).Data,
1249 FilePath: t.GetField(fieldFilePath).Data,
1250 ReferenceNumber: transactionRef,
1252 FolderItemCount: t.GetField(fieldFolderItemCount).Data,
1253 TransferSize: t.GetField(fieldTransferSize).Data,
1255 cc.Server.FileTransfers[data] = fileTransfer
1257 res = append(res, cc.NewReply(t, NewField(fieldRefNum, transactionRef)))
1261 func HandleUploadFile(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1262 fileName := t.GetField(fieldFileName).Data
1263 filePath := t.GetField(fieldFilePath).Data
1265 transactionRef := cc.Server.NewTransactionRef()
1266 data := binary.BigEndian.Uint32(transactionRef)
1268 fileTransfer := &FileTransfer{
1271 ReferenceNumber: transactionRef,
1275 cc.Server.FileTransfers[data] = fileTransfer
1277 res = append(res, cc.NewReply(t, NewField(fieldRefNum, transactionRef)))
1288 func HandleSetClientUserInfo(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1290 if len(t.GetField(fieldUserIconID).Data) == 4 {
1291 icon = t.GetField(fieldUserIconID).Data[2:]
1293 icon = t.GetField(fieldUserIconID).Data
1296 *cc.UserName = t.GetField(fieldUserName).Data
1298 // the options field is only passed by the client versions > 1.2.3.
1299 options := t.GetField(fieldOptions).Data
1302 optBitmap := big.NewInt(int64(binary.BigEndian.Uint16(options)))
1303 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(*cc.Flags)))
1305 // Check refuse private PM option
1306 if optBitmap.Bit(refusePM) == 1 {
1307 flagBitmap.SetBit(flagBitmap, userFlagRefusePM, 1)
1308 binary.BigEndian.PutUint16(*cc.Flags, uint16(flagBitmap.Int64()))
1311 // Check refuse private chat option
1312 if optBitmap.Bit(refuseChat) == 1 {
1313 flagBitmap.SetBit(flagBitmap, userFLagRefusePChat, 1)
1314 binary.BigEndian.PutUint16(*cc.Flags, uint16(flagBitmap.Int64()))
1317 // Check auto response
1318 if optBitmap.Bit(autoResponse) == 1 {
1319 *cc.AutoReply = t.GetField(fieldAutomaticResponse).Data
1321 *cc.AutoReply = []byte{}
1325 // Notify all clients of updated user info
1327 tranNotifyChangeUser,
1328 NewField(fieldUserID, *cc.ID),
1329 NewField(fieldUserIconID, *cc.Icon),
1330 NewField(fieldUserFlags, *cc.Flags),
1331 NewField(fieldUserName, *cc.UserName),
1337 // HandleKeepAlive response to keepalive transactions with an empty reply
1338 // HL 1.9.2 Client sends keepalive msg every 3 minutes
1339 // HL 1.2.3 Client doesn't send keepalives
1340 func HandleKeepAlive(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1341 res = append(res, cc.NewReply(t))
1346 func HandleGetFileNameList(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1347 filePath := cc.Server.Config.FileRoot
1349 path := t.GetField(fieldFilePath).Data
1351 filePath = cc.Server.Config.FileRoot + ReadFilePath(path)
1354 fileNames, err := getFileNameList(filePath)
1359 res = append(res, cc.NewReply(t, fileNames...))
1364 // =================================
1365 // Hotline private chat flow
1366 // =================================
1367 // 1. ClientA sends tranInviteNewChat to server with user ID to invite
1368 // 2. Server creates new ChatID
1369 // 3. Server sends tranInviteToChat to invitee
1370 // 4. Server replies to ClientA with new Chat ID
1372 // A dialog box pops up in the invitee client with options to accept or decline the invitation.
1373 // If Accepted is clicked:
1374 // 1. ClientB sends tranJoinChat with fieldChatID
1376 // HandleInviteNewChat invites users to new private chat
1377 func HandleInviteNewChat(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1379 targetID := t.GetField(fieldUserID).Data
1380 newChatID := cc.Server.NewPrivateChat(cc)
1386 NewField(fieldChatID, newChatID),
1387 NewField(fieldUserName, *cc.UserName),
1388 NewField(fieldUserID, *cc.ID),
1394 NewField(fieldChatID, newChatID),
1395 NewField(fieldUserName, *cc.UserName),
1396 NewField(fieldUserID, *cc.ID),
1397 NewField(fieldUserIconID, *cc.Icon),
1398 NewField(fieldUserFlags, *cc.Flags),
1405 func HandleInviteToChat(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1407 targetID := t.GetField(fieldUserID).Data
1408 chatID := t.GetField(fieldChatID).Data
1414 NewField(fieldChatID, chatID),
1415 NewField(fieldUserName, *cc.UserName),
1416 NewField(fieldUserID, *cc.ID),
1422 NewField(fieldChatID, chatID),
1423 NewField(fieldUserName, *cc.UserName),
1424 NewField(fieldUserID, *cc.ID),
1425 NewField(fieldUserIconID, *cc.Icon),
1426 NewField(fieldUserFlags, *cc.Flags),
1433 func HandleRejectChatInvite(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1434 chatID := t.GetField(fieldChatID).Data
1435 chatInt := binary.BigEndian.Uint32(chatID)
1437 privChat := cc.Server.PrivateChats[chatInt]
1439 resMsg := append(*cc.UserName, []byte(" declined invitation to chat")...)
1441 for _, c := range sortedClients(privChat.ClientConn) {
1446 NewField(fieldChatID, chatID),
1447 NewField(fieldData, resMsg),
1455 // HandleJoinChat is sent from a v1.8+ Hotline client when the joins a private chat
1456 // Fields used in the reply:
1457 // * 115 Chat subject
1458 // * 300 User name with info (Optional)
1459 // * 300 (more user names with info)
1460 func HandleJoinChat(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1461 chatID := t.GetField(fieldChatID).Data
1462 chatInt := binary.BigEndian.Uint32(chatID)
1464 privChat := cc.Server.PrivateChats[chatInt]
1466 // Send tranNotifyChatChangeUser to current members of the chat to inform of new user
1467 for _, c := range sortedClients(privChat.ClientConn) {
1470 tranNotifyChatChangeUser,
1472 NewField(fieldChatID, chatID),
1473 NewField(fieldUserName, *cc.UserName),
1474 NewField(fieldUserID, *cc.ID),
1475 NewField(fieldUserIconID, *cc.Icon),
1476 NewField(fieldUserFlags, *cc.Flags),
1481 privChat.ClientConn[cc.uint16ID()] = cc
1483 replyFields := []Field{NewField(fieldChatSubject, []byte(privChat.Subject))}
1484 for _, c := range sortedClients(privChat.ClientConn) {
1489 Name: string(*c.UserName),
1492 replyFields = append(replyFields, NewField(fieldUsernameWithInfo, user.Payload()))
1495 res = append(res, cc.NewReply(t, replyFields...))
1499 // HandleLeaveChat is sent from a v1.8+ Hotline client when the user exits a private chat
1500 // Fields used in the request:
1501 // * 114 fieldChatID
1502 // Reply is not expected.
1503 func HandleLeaveChat(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1504 chatID := t.GetField(fieldChatID).Data
1505 chatInt := binary.BigEndian.Uint32(chatID)
1507 privChat := cc.Server.PrivateChats[chatInt]
1509 delete(privChat.ClientConn, cc.uint16ID())
1511 // Notify members of the private chat that the user has left
1512 for _, c := range sortedClients(privChat.ClientConn) {
1515 tranNotifyChatDeleteUser,
1517 NewField(fieldChatID, chatID),
1518 NewField(fieldUserID, *cc.ID),
1526 // HandleSetChatSubject is sent from a v1.8+ Hotline client when the user sets a private chat subject
1527 // Fields used in the request:
1529 // * 115 Chat subject Chat subject string
1530 // Reply is not expected.
1531 func HandleSetChatSubject(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1532 chatID := t.GetField(fieldChatID).Data
1533 chatInt := binary.BigEndian.Uint32(chatID)
1535 privChat := cc.Server.PrivateChats[chatInt]
1536 privChat.Subject = string(t.GetField(fieldChatSubject).Data)
1538 for _, c := range sortedClients(privChat.ClientConn) {
1541 tranNotifyChatSubject,
1543 NewField(fieldChatID, chatID),
1544 NewField(fieldChatSubject, t.GetField(fieldChatSubject).Data),