]> git.r.bdr.sh - rbdr/mobius/blob - hotline/transaction_handlers.go
Misc cleanup
[rbdr/mobius] / hotline / transaction_handlers.go
1 package hotline
2
3 import (
4 "bytes"
5 "encoding/binary"
6 "errors"
7 "fmt"
8 "gopkg.in/yaml.v2"
9 "io/ioutil"
10 "math/big"
11 "os"
12 "path"
13 "sort"
14 "strings"
15 "time"
16 )
17
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
24 }
25
26 var TransactionHandlers = map[uint16]TransactionType{
27 // Server initiated
28 tranChatMsg: {
29 Name: "tranChatMsg",
30 },
31 // Server initiated
32 tranNotifyChangeUser: {
33 Name: "tranNotifyChangeUser",
34 },
35 tranError: {
36 Name: "tranError",
37 },
38 tranShowAgreement: {
39 Name: "tranShowAgreement",
40 },
41 tranUserAccess: {
42 Name: "tranUserAccess",
43 },
44 tranNotifyDeleteUser: {
45 Name: "tranNotifyDeleteUser",
46 },
47 tranAgreed: {
48 Access: accessAlwaysAllow,
49 Name: "tranAgreed",
50 Handler: HandleTranAgreed,
51 },
52 tranChatSend: {
53 Access: accessSendChat,
54 DenyMsg: "You are not allowed to participate in chat.",
55 Handler: HandleChatSend,
56 Name: "tranChatSend",
57 RequiredFields: []requiredField{
58 {
59 ID: fieldData,
60 minLen: 0,
61 },
62 },
63 },
64 tranDelNewsArt: {
65 Access: accessNewsDeleteArt,
66 DenyMsg: "You are not allowed to delete news articles.",
67 Name: "tranDelNewsArt",
68 Handler: HandleDelNewsArt,
69 },
70 tranDelNewsItem: {
71 Access: accessAlwaysAllow, // Granular access enforced inside the handler
72 // Has multiple access flags: News Delete Folder (37) or News Delete Category (35)
73 // TODO: Implement inside the handler
74 Name: "tranDelNewsItem",
75 Handler: HandleDelNewsItem,
76 },
77 tranDeleteFile: {
78 Access: accessAlwaysAllow, // Granular access enforced inside the handler
79 Name: "tranDeleteFile",
80 Handler: HandleDeleteFile,
81 },
82 tranDeleteUser: {
83 Access: accessDeleteUser,
84 DenyMsg: "You are not allowed to delete accounts.",
85 Name: "tranDeleteUser",
86 Handler: HandleDeleteUser,
87 },
88 tranDisconnectUser: {
89 Access: accessDisconUser,
90 DenyMsg: "You are not allowed to disconnect users.",
91 Name: "tranDisconnectUser",
92 Handler: HandleDisconnectUser,
93 },
94 tranDownloadFile: {
95 Access: accessDownloadFile,
96 DenyMsg: "You are not allowed to download files.",
97 Name: "tranDownloadFile",
98 Handler: HandleDownloadFile,
99 },
100 tranDownloadFldr: {
101 Access: accessDownloadFile, // There is no specific access flag for folder vs file download
102 DenyMsg: "You are not allowed to download files.",
103 Name: "tranDownloadFldr",
104 Handler: HandleDownloadFolder,
105 },
106 tranGetClientInfoText: {
107 Access: accessGetClientInfo,
108 DenyMsg: "You are not allowed to get client info",
109 Name: "tranGetClientInfoText",
110 Handler: HandleGetClientConnInfoText,
111 },
112 tranGetFileInfo: {
113 Access: accessAlwaysAllow,
114 Name: "tranGetFileInfo",
115 Handler: HandleGetFileInfo,
116 },
117 tranGetFileNameList: {
118 Access: accessAlwaysAllow,
119 Name: "tranGetFileNameList",
120 Handler: HandleGetFileNameList,
121 },
122 tranGetMsgs: {
123 Access: accessNewsReadArt,
124 DenyMsg: "You are not allowed to read news.",
125 Name: "tranGetMsgs",
126 Handler: HandleGetMsgs,
127 },
128 tranGetNewsArtData: {
129 Access: accessNewsReadArt,
130 DenyMsg: "You are not allowed to read news.",
131 Name: "tranGetNewsArtData",
132 Handler: HandleGetNewsArtData,
133 },
134 tranGetNewsArtNameList: {
135 Access: accessNewsReadArt,
136 DenyMsg: "You are not allowed to read news.",
137 Name: "tranGetNewsArtNameList",
138 Handler: HandleGetNewsArtNameList,
139 },
140 tranGetNewsCatNameList: {
141 Access: accessNewsReadArt,
142 DenyMsg: "You are not allowed to read news.",
143 Name: "tranGetNewsCatNameList",
144 Handler: HandleGetNewsCatNameList,
145 },
146 tranGetUser: {
147 Access: accessOpenUser,
148 DenyMsg: "You are not allowed to view accounts.",
149 Name: "tranGetUser",
150 Handler: HandleGetUser,
151 },
152 tranGetUserNameList: {
153 Access: accessAlwaysAllow,
154 Name: "tranHandleGetUserNameList",
155 Handler: HandleGetUserNameList,
156 },
157 tranInviteNewChat: {
158 Access: accessOpenChat,
159 DenyMsg: "You are not allowed to request private chat.",
160 Name: "tranInviteNewChat",
161 Handler: HandleInviteNewChat,
162 },
163 tranInviteToChat: {
164 Access: accessOpenChat,
165 DenyMsg: "You are not allowed to request private chat.",
166 Name: "tranInviteToChat",
167 Handler: HandleInviteToChat,
168 },
169 tranJoinChat: {
170 Access: accessAlwaysAllow,
171 Name: "tranJoinChat",
172 Handler: HandleJoinChat,
173 },
174 tranKeepAlive: {
175 Access: accessAlwaysAllow,
176 Name: "tranKeepAlive",
177 Handler: HandleKeepAlive,
178 },
179 tranLeaveChat: {
180 Access: accessAlwaysAllow,
181 Name: "tranJoinChat",
182 Handler: HandleLeaveChat,
183 },
184
185 tranListUsers: {
186 Access: accessOpenUser,
187 DenyMsg: "You are not allowed to view accounts.",
188 Name: "tranListUsers",
189 Handler: HandleListUsers,
190 },
191 tranMoveFile: {
192 Access: accessMoveFile,
193 DenyMsg: "You are not allowed to move files.",
194 Name: "tranMoveFile",
195 Handler: HandleMoveFile,
196 },
197 tranNewFolder: {
198 Access: accessCreateFolder,
199 DenyMsg: "You are not allow to create folders.",
200 Name: "tranNewFolder",
201 Handler: HandleNewFolder,
202 },
203 tranNewNewsCat: {
204 Access: accessNewsCreateCat,
205 DenyMsg: "You are not allowed to create news categories.",
206 Name: "tranNewNewsCat",
207 Handler: HandleNewNewsCat,
208 },
209 tranNewNewsFldr: {
210 Access: accessNewsCreateFldr,
211 DenyMsg: "You are not allowed to create news folders.",
212 Name: "tranNewNewsFldr",
213 Handler: HandleNewNewsFldr,
214 },
215 tranNewUser: {
216 Access: accessCreateUser,
217 DenyMsg: "You are not allowed to create new accounts.",
218 Name: "tranNewUser",
219 Handler: HandleNewUser,
220 },
221 tranOldPostNews: {
222 Access: accessNewsPostArt,
223 DenyMsg: "You are not allowed to post news.",
224 Name: "tranOldPostNews",
225 Handler: HandleTranOldPostNews,
226 },
227 tranPostNewsArt: {
228 Access: accessNewsPostArt,
229 DenyMsg: "You are not allowed to post news articles.",
230 Name: "tranPostNewsArt",
231 Handler: HandlePostNewsArt,
232 },
233 tranRejectChatInvite: {
234 Access: accessAlwaysAllow,
235 Name: "tranRejectChatInvite",
236 Handler: HandleRejectChatInvite,
237 },
238 tranSendInstantMsg: {
239 Access: accessAlwaysAllow,
240 // Access: accessSendPrivMsg,
241 // DenyMsg: "You are not allowed to send private messages",
242 Name: "tranSendInstantMsg",
243 Handler: HandleSendInstantMsg,
244 RequiredFields: []requiredField{
245 {
246 ID: fieldData,
247 minLen: 0,
248 },
249 {
250 ID: fieldUserID,
251 },
252 },
253 },
254 tranSetChatSubject: {
255 Access: accessAlwaysAllow,
256 Name: "tranSetChatSubject",
257 Handler: HandleSetChatSubject,
258 },
259 tranMakeFileAlias: {
260 Access: accessAlwaysAllow,
261 Name: "tranMakeFileAlias",
262 Handler: HandleMakeAlias,
263 RequiredFields: []requiredField{
264 {ID: fieldFileName, minLen: 1},
265 {ID: fieldFilePath, minLen: 1},
266 {ID: fieldFileNewPath, minLen: 1},
267 },
268 },
269 tranSetClientUserInfo: {
270 Access: accessAlwaysAllow,
271 Name: "tranSetClientUserInfo",
272 Handler: HandleSetClientUserInfo,
273 },
274 tranSetFileInfo: {
275 Access: accessAlwaysAllow, // granular access is in the handler
276 Name: "tranSetFileInfo",
277 Handler: HandleSetFileInfo,
278 },
279 tranSetUser: {
280 Access: accessModifyUser,
281 DenyMsg: "You are not allowed to modify accounts.",
282 Name: "tranSetUser",
283 Handler: HandleSetUser,
284 },
285 tranUploadFile: {
286 Access: accessAlwaysAllow,
287 DenyMsg: "You are not allowed to upload files.",
288 Name: "tranUploadFile",
289 Handler: HandleUploadFile,
290 },
291 tranUploadFldr: {
292 Access: accessAlwaysAllow, // TODO: what should this be?
293 Name: "tranUploadFldr",
294 Handler: HandleUploadFolder,
295 },
296 tranUserBroadcast: {
297 Access: accessBroadcast,
298 DenyMsg: "You are not allowed to send broadcast messages.",
299 Name: "tranUserBroadcast",
300 Handler: HandleUserBroadcast,
301 },
302 }
303
304 func HandleChatSend(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
305 // Truncate long usernames
306 trunc := fmt.Sprintf("%13s", cc.UserName)
307 formattedMsg := fmt.Sprintf("\r%.14s: %s", trunc, t.GetField(fieldData).Data)
308
309 // By holding the option key, Hotline chat allows users to send /me formatted messages like:
310 // *** Halcyon does stuff
311 // This is indicated by the presence of the optional field fieldChatOptions in the transaction payload
312 if t.GetField(fieldChatOptions).Data != nil {
313 formattedMsg = fmt.Sprintf("\r*** %s %s", cc.UserName, t.GetField(fieldData).Data)
314 }
315
316 if bytes.Equal(t.GetField(fieldData).Data, []byte("/stats")) {
317 formattedMsg = strings.Replace(cc.Server.Stats.String(), "\n", "\r", -1)
318 }
319
320 chatID := t.GetField(fieldChatID).Data
321 // a non-nil chatID indicates the message belongs to a private chat
322 if chatID != nil {
323 chatInt := binary.BigEndian.Uint32(chatID)
324 privChat := cc.Server.PrivateChats[chatInt]
325
326 // send the message to all connected clients of the private chat
327 for _, c := range privChat.ClientConn {
328 res = append(res, *NewTransaction(
329 tranChatMsg,
330 c.ID,
331 NewField(fieldChatID, chatID),
332 NewField(fieldData, []byte(formattedMsg)),
333 ))
334 }
335 return res, err
336 }
337
338 for _, c := range sortedClients(cc.Server.Clients) {
339 // Filter out clients that do not have the read chat permission
340 if authorize(c.Account.Access, accessReadChat) {
341 res = append(res, *NewTransaction(tranChatMsg, c.ID, NewField(fieldData, []byte(formattedMsg))))
342 }
343 }
344
345 return res, err
346 }
347
348 // HandleSendInstantMsg sends instant message to the user on the current server.
349 // Fields used in the request:
350 // 103 User ID
351 // 113 Options
352 // One of the following values:
353 // - User message (myOpt_UserMessage = 1)
354 // - Refuse message (myOpt_RefuseMessage = 2)
355 // - Refuse chat (myOpt_RefuseChat = 3)
356 // - Automatic response (myOpt_AutomaticResponse = 4)"
357 // 101 Data Optional
358 // 214 Quoting message Optional
359 //
360 // Fields used in the reply:
361 // None
362 func HandleSendInstantMsg(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
363 msg := t.GetField(fieldData)
364 ID := t.GetField(fieldUserID)
365 // TODO: Implement reply quoting
366 // options := transaction.GetField(hotline.fieldOptions)
367
368 res = append(res,
369 *NewTransaction(
370 tranServerMsg,
371 &ID.Data,
372 NewField(fieldData, msg.Data),
373 NewField(fieldUserName, cc.UserName),
374 NewField(fieldUserID, *cc.ID),
375 NewField(fieldOptions, []byte{0, 1}),
376 ),
377 )
378 id, _ := byteToInt(ID.Data)
379
380 otherClient := cc.Server.Clients[uint16(id)]
381 if otherClient == nil {
382 return res, errors.New("ohno")
383 }
384
385 // Respond with auto reply if other client has it enabled
386 if len(otherClient.AutoReply) > 0 {
387 res = append(res,
388 *NewTransaction(
389 tranServerMsg,
390 cc.ID,
391 NewField(fieldData, otherClient.AutoReply),
392 NewField(fieldUserName, otherClient.UserName),
393 NewField(fieldUserID, *otherClient.ID),
394 NewField(fieldOptions, []byte{0, 1}),
395 ),
396 )
397 }
398
399 res = append(res, cc.NewReply(t))
400
401 return res, err
402 }
403
404 func HandleGetFileInfo(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
405 fileName := t.GetField(fieldFileName).Data
406 filePath := t.GetField(fieldFilePath).Data
407
408 ffo, err := NewFlattenedFileObject(cc.Server.Config.FileRoot, filePath, fileName)
409 if err != nil {
410 return res, err
411 }
412
413 res = append(res, cc.NewReply(t,
414 NewField(fieldFileName, fileName),
415 NewField(fieldFileTypeString, ffo.FlatFileInformationFork.TypeSignature),
416 NewField(fieldFileCreatorString, ffo.FlatFileInformationFork.CreatorSignature),
417 NewField(fieldFileComment, ffo.FlatFileInformationFork.Comment),
418 NewField(fieldFileType, ffo.FlatFileInformationFork.TypeSignature),
419 NewField(fieldFileCreateDate, ffo.FlatFileInformationFork.CreateDate),
420 NewField(fieldFileModifyDate, ffo.FlatFileInformationFork.ModifyDate),
421 NewField(fieldFileSize, ffo.FlatFileDataForkHeader.DataSize),
422 ))
423 return res, err
424 }
425
426 // HandleSetFileInfo updates a file or folder name and/or comment from the Get Info window
427 // TODO: Implement support for comments
428 // Fields used in the request:
429 // * 201 File name
430 // * 202 File path Optional
431 // * 211 File new name Optional
432 // * 210 File comment Optional
433 // Fields used in the reply: None
434 func HandleSetFileInfo(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
435 fileName := t.GetField(fieldFileName).Data
436 filePath := t.GetField(fieldFilePath).Data
437
438 fullFilePath, err := readPath(cc.Server.Config.FileRoot, filePath, fileName)
439 if err != nil {
440 return res, err
441 }
442
443 fullNewFilePath, err := readPath(cc.Server.Config.FileRoot, filePath, t.GetField(fieldFileNewName).Data)
444 if err != nil {
445 return nil, err
446 }
447
448 // fileComment := t.GetField(fieldFileComment).Data
449 fileNewName := t.GetField(fieldFileNewName).Data
450
451 if fileNewName != nil {
452 fi, err := FS.Stat(fullFilePath)
453 if err != nil {
454 return res, err
455 }
456 switch mode := fi.Mode(); {
457 case mode.IsDir():
458 if !authorize(cc.Account.Access, accessRenameFolder) {
459 res = append(res, cc.NewErrReply(t, "You are not allowed to rename folders."))
460 return res, err
461 }
462 case mode.IsRegular():
463 if !authorize(cc.Account.Access, accessRenameFile) {
464 res = append(res, cc.NewErrReply(t, "You are not allowed to rename files."))
465 return res, err
466 }
467 }
468
469 err = os.Rename(fullFilePath, fullNewFilePath)
470 if os.IsNotExist(err) {
471 res = append(res, cc.NewErrReply(t, "Cannot rename file "+string(fileName)+" because it does not exist or cannot be found."))
472 return res, err
473 }
474 }
475
476 res = append(res, cc.NewReply(t))
477 return res, err
478 }
479
480 // HandleDeleteFile deletes a file or folder
481 // Fields used in the request:
482 // * 201 File name
483 // * 202 File path
484 // Fields used in the reply: none
485 func HandleDeleteFile(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
486 fileName := t.GetField(fieldFileName).Data
487 filePath := t.GetField(fieldFilePath).Data
488
489 fullFilePath, err := readPath(cc.Server.Config.FileRoot, filePath, fileName)
490 if err != nil {
491 return res, err
492 }
493
494 cc.Server.Logger.Debugw("Delete file", "src", fullFilePath)
495
496 fi, err := os.Stat(fullFilePath)
497 if err != nil {
498 res = append(res, cc.NewErrReply(t, "Cannot delete file "+string(fileName)+" because it does not exist or cannot be found."))
499 return res, nil
500 }
501 switch mode := fi.Mode(); {
502 case mode.IsDir():
503 if !authorize(cc.Account.Access, accessDeleteFolder) {
504 res = append(res, cc.NewErrReply(t, "You are not allowed to delete folders."))
505 return res, err
506 }
507 case mode.IsRegular():
508 if !authorize(cc.Account.Access, accessDeleteFile) {
509 res = append(res, cc.NewErrReply(t, "You are not allowed to delete files."))
510 return res, err
511 }
512 }
513
514 if err := os.RemoveAll(fullFilePath); err != nil {
515 return res, err
516 }
517
518 res = append(res, cc.NewReply(t))
519 return res, err
520 }
521
522 // HandleMoveFile moves files or folders. Note: seemingly not documented
523 func HandleMoveFile(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
524 fileName := string(t.GetField(fieldFileName).Data)
525 filePath := cc.Server.Config.FileRoot + ReadFilePath(t.GetField(fieldFilePath).Data)
526 fileNewPath := cc.Server.Config.FileRoot + ReadFilePath(t.GetField(fieldFileNewPath).Data)
527
528 cc.Server.Logger.Debugw("Move file", "src", filePath+"/"+fileName, "dst", fileNewPath+"/"+fileName)
529
530 path := filePath + "/" + fileName
531 fi, err := os.Stat(path)
532 if err != nil {
533 return res, err
534 }
535 switch mode := fi.Mode(); {
536 case mode.IsDir():
537 if !authorize(cc.Account.Access, accessMoveFolder) {
538 res = append(res, cc.NewErrReply(t, "You are not allowed to move folders."))
539 return res, err
540 }
541 case mode.IsRegular():
542 if !authorize(cc.Account.Access, accessMoveFile) {
543 res = append(res, cc.NewErrReply(t, "You are not allowed to move files."))
544 return res, err
545 }
546 }
547
548 err = os.Rename(filePath+"/"+fileName, fileNewPath+"/"+fileName)
549 if os.IsNotExist(err) {
550 res = append(res, cc.NewErrReply(t, "Cannot delete file "+fileName+" because it does not exist or cannot be found."))
551 return res, err
552 }
553 if err != nil {
554 return []Transaction{}, err
555 }
556 // TODO: handle other possible errors; e.g. file delete fails due to file permission issue
557
558 res = append(res, cc.NewReply(t))
559 return res, err
560 }
561
562 func HandleNewFolder(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
563 newFolderPath := cc.Server.Config.FileRoot
564 folderName := string(t.GetField(fieldFileName).Data)
565
566 folderName = path.Join("/", folderName)
567
568 // fieldFilePath is only present for nested paths
569 if t.GetField(fieldFilePath).Data != nil {
570 var newFp FilePath
571 err := newFp.UnmarshalBinary(t.GetField(fieldFilePath).Data)
572 if err != nil {
573 return nil, err
574 }
575 newFolderPath += newFp.String()
576 }
577 newFolderPath = path.Join(newFolderPath, folderName)
578
579 // TODO: check path and folder name lengths
580
581 if _, err := FS.Stat(newFolderPath); !os.IsNotExist(err) {
582 msg := fmt.Sprintf("Cannot create folder \"%s\" because there is already a file or folder with that name.", folderName)
583 return []Transaction{cc.NewErrReply(t, msg)}, nil
584 }
585
586 // TODO: check for disallowed characters to maintain compatibility for original client
587
588 if err := FS.Mkdir(newFolderPath, 0777); err != nil {
589 msg := fmt.Sprintf("Cannot create folder \"%s\" because an error occurred.", folderName)
590 return []Transaction{cc.NewErrReply(t, msg)}, nil
591 }
592
593 res = append(res, cc.NewReply(t))
594 return res, err
595 }
596
597 func HandleSetUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
598 login := DecodeUserString(t.GetField(fieldUserLogin).Data)
599 userName := string(t.GetField(fieldUserName).Data)
600
601 newAccessLvl := t.GetField(fieldUserAccess).Data
602
603 account := cc.Server.Accounts[login]
604 account.Access = &newAccessLvl
605 account.Name = userName
606
607 // If the password field is cleared in the Hotline edit user UI, the SetUser transaction does
608 // not include fieldUserPassword
609 if t.GetField(fieldUserPassword).Data == nil {
610 account.Password = hashAndSalt([]byte(""))
611 }
612 if len(t.GetField(fieldUserPassword).Data) > 1 {
613 account.Password = hashAndSalt(t.GetField(fieldUserPassword).Data)
614 }
615
616 file := cc.Server.ConfigDir + "Users/" + login + ".yaml"
617 out, err := yaml.Marshal(&account)
618 if err != nil {
619 return res, err
620 }
621 if err := ioutil.WriteFile(file, out, 0666); err != nil {
622 return res, err
623 }
624
625 // Notify connected clients logged in as the user of the new access level
626 for _, c := range cc.Server.Clients {
627 if c.Account.Login == login {
628 // Note: comment out these two lines to test server-side deny messages
629 newT := NewTransaction(tranUserAccess, c.ID, NewField(fieldUserAccess, newAccessLvl))
630 res = append(res, *newT)
631
632 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(*c.Flags)))
633 if authorize(c.Account.Access, accessDisconUser) {
634 flagBitmap.SetBit(flagBitmap, userFlagAdmin, 1)
635 } else {
636 flagBitmap.SetBit(flagBitmap, userFlagAdmin, 0)
637 }
638 binary.BigEndian.PutUint16(*c.Flags, uint16(flagBitmap.Int64()))
639
640 c.Account.Access = account.Access
641
642 cc.sendAll(
643 tranNotifyChangeUser,
644 NewField(fieldUserID, *c.ID),
645 NewField(fieldUserFlags, *c.Flags),
646 NewField(fieldUserName, c.UserName),
647 NewField(fieldUserIconID, *c.Icon),
648 )
649 }
650 }
651
652 // TODO: If we have just promoted a connected user to admin, notify
653 // connected clients to turn the user red
654
655 res = append(res, cc.NewReply(t))
656 return res, err
657 }
658
659 func HandleGetUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
660 // userLogin := string(t.GetField(fieldUserLogin).Data)
661 account := cc.Server.Accounts[string(t.GetField(fieldUserLogin).Data)]
662 if account == nil {
663 errorT := cc.NewErrReply(t, "Account does not exist.")
664 res = append(res, errorT)
665 return res, err
666 }
667
668 res = append(res, cc.NewReply(t,
669 NewField(fieldUserName, []byte(account.Name)),
670 NewField(fieldUserLogin, negateString(t.GetField(fieldUserLogin).Data)),
671 NewField(fieldUserPassword, []byte(account.Password)),
672 NewField(fieldUserAccess, *account.Access),
673 ))
674 return res, err
675 }
676
677 func HandleListUsers(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
678 var userFields []Field
679 // TODO: make order deterministic
680 for _, acc := range cc.Server.Accounts {
681 userField := acc.MarshalBinary()
682 userFields = append(userFields, NewField(fieldData, userField))
683 }
684
685 res = append(res, cc.NewReply(t, userFields...))
686 return res, err
687 }
688
689 // HandleNewUser creates a new user account
690 func HandleNewUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
691 login := DecodeUserString(t.GetField(fieldUserLogin).Data)
692
693 // If the account already exists, reply with an error
694 // TODO: make order deterministic
695 if _, ok := cc.Server.Accounts[login]; ok {
696 res = append(res, cc.NewErrReply(t, "Cannot create account "+login+" because there is already an account with that login."))
697 return res, err
698 }
699
700 if err := cc.Server.NewUser(
701 login,
702 string(t.GetField(fieldUserName).Data),
703 string(t.GetField(fieldUserPassword).Data),
704 t.GetField(fieldUserAccess).Data,
705 ); err != nil {
706 return []Transaction{}, err
707 }
708
709 res = append(res, cc.NewReply(t))
710 return res, err
711 }
712
713 func HandleDeleteUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
714 // TODO: Handle case where account doesn't exist; e.g. delete race condition
715 login := DecodeUserString(t.GetField(fieldUserLogin).Data)
716
717 if err := cc.Server.DeleteUser(login); err != nil {
718 return res, err
719 }
720
721 res = append(res, cc.NewReply(t))
722 return res, err
723 }
724
725 // HandleUserBroadcast sends an Administrator Message to all connected clients of the server
726 func HandleUserBroadcast(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
727 cc.sendAll(
728 tranServerMsg,
729 NewField(fieldData, t.GetField(tranGetMsgs).Data),
730 NewField(fieldChatOptions, []byte{0}),
731 )
732
733 res = append(res, cc.NewReply(t))
734 return res, err
735 }
736
737 func byteToInt(bytes []byte) (int, error) {
738 switch len(bytes) {
739 case 2:
740 return int(binary.BigEndian.Uint16(bytes)), nil
741 case 4:
742 return int(binary.BigEndian.Uint32(bytes)), nil
743 }
744
745 return 0, errors.New("unknown byte length")
746 }
747
748 func HandleGetClientConnInfoText(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
749 clientID, _ := byteToInt(t.GetField(fieldUserID).Data)
750
751 clientConn := cc.Server.Clients[uint16(clientID)]
752 if clientConn == nil {
753 return res, errors.New("invalid client")
754 }
755
756 // TODO: Implement non-hardcoded values
757 template := `Nickname: %s
758 Name: %s
759 Account: %s
760 Address: %s
761
762 -------- File Downloads ---------
763
764 %s
765
766 ------- Folder Downloads --------
767
768 None.
769
770 --------- File Uploads ----------
771
772 None.
773
774 -------- Folder Uploads ---------
775
776 None.
777
778 ------- Waiting Downloads -------
779
780 None.
781
782 `
783
784 activeDownloads := clientConn.Transfers[FileDownload]
785 activeDownloadList := "None."
786 for _, dl := range activeDownloads {
787 activeDownloadList += dl.String() + "\n"
788 }
789
790 template = fmt.Sprintf(
791 template,
792 clientConn.UserName,
793 clientConn.Account.Name,
794 clientConn.Account.Login,
795 clientConn.Connection.RemoteAddr().String(),
796 activeDownloadList,
797 )
798 template = strings.Replace(template, "\n", "\r", -1)
799
800 res = append(res, cc.NewReply(t,
801 NewField(fieldData, []byte(template)),
802 NewField(fieldUserName, clientConn.UserName),
803 ))
804 return res, err
805 }
806
807 func HandleGetUserNameList(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
808 res = append(res, cc.NewReply(t, cc.Server.connectedUsers()...))
809
810 return res, err
811 }
812
813 func (cc *ClientConn) notifyNewUserHasJoined() (res []Transaction, err error) {
814 // Notify other ccs that a new user has connected
815 cc.NotifyOthers(
816 *NewTransaction(
817 tranNotifyChangeUser, nil,
818 NewField(fieldUserName, cc.UserName),
819 NewField(fieldUserID, *cc.ID),
820 NewField(fieldUserIconID, *cc.Icon),
821 NewField(fieldUserFlags, *cc.Flags),
822 ),
823 )
824
825 return res, nil
826 }
827
828 func HandleTranAgreed(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
829 cc.Agreed = true
830 cc.UserName = t.GetField(fieldUserName).Data
831 *cc.Icon = t.GetField(fieldUserIconID).Data
832
833 options := t.GetField(fieldOptions).Data
834 optBitmap := big.NewInt(int64(binary.BigEndian.Uint16(options)))
835
836 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(*cc.Flags)))
837
838 // Check refuse private PM option
839 if optBitmap.Bit(refusePM) == 1 {
840 flagBitmap.SetBit(flagBitmap, userFlagRefusePM, 1)
841 binary.BigEndian.PutUint16(*cc.Flags, uint16(flagBitmap.Int64()))
842 }
843
844 // Check refuse private chat option
845 if optBitmap.Bit(refuseChat) == 1 {
846 flagBitmap.SetBit(flagBitmap, userFLagRefusePChat, 1)
847 binary.BigEndian.PutUint16(*cc.Flags, uint16(flagBitmap.Int64()))
848 }
849
850 // Check auto response
851 if optBitmap.Bit(autoResponse) == 1 {
852 cc.AutoReply = t.GetField(fieldAutomaticResponse).Data
853 } else {
854 cc.AutoReply = []byte{}
855 }
856
857 _, _ = cc.notifyNewUserHasJoined()
858
859 res = append(res, cc.NewReply(t))
860
861 return res, err
862 }
863
864 const defaultNewsDateFormat = "Jan02 15:04" // Jun23 20:49
865 // "Mon, 02 Jan 2006 15:04:05 MST"
866
867 const defaultNewsTemplate = `From %s (%s):
868
869 %s
870
871 __________________________________________________________`
872
873 // HandleTranOldPostNews updates the flat news
874 // Fields used in this request:
875 // 101 Data
876 func HandleTranOldPostNews(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
877 cc.Server.flatNewsMux.Lock()
878 defer cc.Server.flatNewsMux.Unlock()
879
880 newsDateTemplate := defaultNewsDateFormat
881 if cc.Server.Config.NewsDateFormat != "" {
882 newsDateTemplate = cc.Server.Config.NewsDateFormat
883 }
884
885 newsTemplate := defaultNewsTemplate
886 if cc.Server.Config.NewsDelimiter != "" {
887 newsTemplate = cc.Server.Config.NewsDelimiter
888 }
889
890 newsPost := fmt.Sprintf(newsTemplate+"\r", cc.UserName, time.Now().Format(newsDateTemplate), t.GetField(fieldData).Data)
891 newsPost = strings.Replace(newsPost, "\n", "\r", -1)
892
893 // update news in memory
894 cc.Server.FlatNews = append([]byte(newsPost), cc.Server.FlatNews...)
895
896 // update news on disk
897 if err := ioutil.WriteFile(cc.Server.ConfigDir+"MessageBoard.txt", cc.Server.FlatNews, 0644); err != nil {
898 return res, err
899 }
900
901 // Notify all clients of updated news
902 cc.sendAll(
903 tranNewMsg,
904 NewField(fieldData, []byte(newsPost)),
905 )
906
907 res = append(res, cc.NewReply(t))
908 return res, err
909 }
910
911 func HandleDisconnectUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
912 clientConn := cc.Server.Clients[binary.BigEndian.Uint16(t.GetField(fieldUserID).Data)]
913
914 if authorize(clientConn.Account.Access, accessCannotBeDiscon) {
915 res = append(res, cc.NewErrReply(t, clientConn.Account.Login+" is not allowed to be disconnected."))
916 return res, err
917 }
918
919 if err := clientConn.Connection.Close(); err != nil {
920 return res, err
921 }
922
923 res = append(res, cc.NewReply(t))
924 return res, err
925 }
926
927 func HandleGetNewsCatNameList(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
928 // Fields used in the request:
929 // 325 News path (Optional)
930
931 newsPath := t.GetField(fieldNewsPath).Data
932 cc.Server.Logger.Infow("NewsPath: ", "np", string(newsPath))
933
934 pathStrs := ReadNewsPath(t.GetField(fieldNewsPath).Data)
935 cats := cc.Server.GetNewsCatByPath(pathStrs)
936
937 // To store the keys in slice in sorted order
938 keys := make([]string, len(cats))
939 i := 0
940 for k := range cats {
941 keys[i] = k
942 i++
943 }
944 sort.Strings(keys)
945
946 var fieldData []Field
947 for _, k := range keys {
948 cat := cats[k]
949 b, _ := cat.MarshalBinary()
950 fieldData = append(fieldData, NewField(
951 fieldNewsCatListData15,
952 b,
953 ))
954 }
955
956 res = append(res, cc.NewReply(t, fieldData...))
957 return res, err
958 }
959
960 func HandleNewNewsCat(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
961 name := string(t.GetField(fieldNewsCatName).Data)
962 pathStrs := ReadNewsPath(t.GetField(fieldNewsPath).Data)
963
964 cats := cc.Server.GetNewsCatByPath(pathStrs)
965 cats[name] = NewsCategoryListData15{
966 Name: name,
967 Type: []byte{0, 3},
968 Articles: map[uint32]*NewsArtData{},
969 SubCats: make(map[string]NewsCategoryListData15),
970 }
971
972 if err := cc.Server.writeThreadedNews(); err != nil {
973 return res, err
974 }
975 res = append(res, cc.NewReply(t))
976 return res, err
977 }
978
979 func HandleNewNewsFldr(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
980 // Fields used in the request:
981 // 322 News category name
982 // 325 News path
983 name := string(t.GetField(fieldFileName).Data)
984 pathStrs := ReadNewsPath(t.GetField(fieldNewsPath).Data)
985
986 cc.Server.Logger.Infof("Creating new news folder %s", name)
987
988 cats := cc.Server.GetNewsCatByPath(pathStrs)
989 cats[name] = NewsCategoryListData15{
990 Name: name,
991 Type: []byte{0, 2},
992 Articles: map[uint32]*NewsArtData{},
993 SubCats: make(map[string]NewsCategoryListData15),
994 }
995 if err := cc.Server.writeThreadedNews(); err != nil {
996 return res, err
997 }
998 res = append(res, cc.NewReply(t))
999 return res, err
1000 }
1001
1002 // Fields used in the request:
1003 // 325 News path Optional
1004 //
1005 // Reply fields:
1006 // 321 News article list data Optional
1007 func HandleGetNewsArtNameList(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1008 pathStrs := ReadNewsPath(t.GetField(fieldNewsPath).Data)
1009
1010 var cat NewsCategoryListData15
1011 cats := cc.Server.ThreadedNews.Categories
1012
1013 for _, path := range pathStrs {
1014 cat = cats[path]
1015 cats = cats[path].SubCats
1016 }
1017
1018 nald := cat.GetNewsArtListData()
1019
1020 res = append(res, cc.NewReply(t, NewField(fieldNewsArtListData, nald.Payload())))
1021 return res, err
1022 }
1023
1024 func HandleGetNewsArtData(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1025 // Request fields
1026 // 325 News path
1027 // 326 News article ID
1028 // 327 News article data flavor
1029
1030 pathStrs := ReadNewsPath(t.GetField(fieldNewsPath).Data)
1031
1032 var cat NewsCategoryListData15
1033 cats := cc.Server.ThreadedNews.Categories
1034
1035 for _, path := range pathStrs {
1036 cat = cats[path]
1037 cats = cats[path].SubCats
1038 }
1039 newsArtID := t.GetField(fieldNewsArtID).Data
1040
1041 convertedArtID := binary.BigEndian.Uint16(newsArtID)
1042
1043 art := cat.Articles[uint32(convertedArtID)]
1044 if art == nil {
1045 res = append(res, cc.NewReply(t))
1046 return res, err
1047 }
1048
1049 // Reply fields
1050 // 328 News article title
1051 // 329 News article poster
1052 // 330 News article date
1053 // 331 Previous article ID
1054 // 332 Next article ID
1055 // 335 Parent article ID
1056 // 336 First child article ID
1057 // 327 News article data flavor "Should be “text/plain”
1058 // 333 News article data Optional (if data flavor is “text/plain”)
1059
1060 res = append(res, cc.NewReply(t,
1061 NewField(fieldNewsArtTitle, []byte(art.Title)),
1062 NewField(fieldNewsArtPoster, []byte(art.Poster)),
1063 NewField(fieldNewsArtDate, art.Date),
1064 NewField(fieldNewsArtPrevArt, art.PrevArt),
1065 NewField(fieldNewsArtNextArt, art.NextArt),
1066 NewField(fieldNewsArtParentArt, art.ParentArt),
1067 NewField(fieldNewsArt1stChildArt, art.FirstChildArt),
1068 NewField(fieldNewsArtDataFlav, []byte("text/plain")),
1069 NewField(fieldNewsArtData, []byte(art.Data)),
1070 ))
1071 return res, err
1072 }
1073
1074 func HandleDelNewsItem(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1075 // Access: News Delete Folder (37) or News Delete Category (35)
1076
1077 pathStrs := ReadNewsPath(t.GetField(fieldNewsPath).Data)
1078
1079 // TODO: determine if path is a Folder (Bundle) or Category and check for permission
1080
1081 cc.Server.Logger.Infof("DelNewsItem %v", pathStrs)
1082
1083 cats := cc.Server.ThreadedNews.Categories
1084
1085 delName := pathStrs[len(pathStrs)-1]
1086 if len(pathStrs) > 1 {
1087 for _, path := range pathStrs[0 : len(pathStrs)-1] {
1088 cats = cats[path].SubCats
1089 }
1090 }
1091
1092 delete(cats, delName)
1093
1094 err = cc.Server.writeThreadedNews()
1095 if err != nil {
1096 return res, err
1097 }
1098
1099 // Reply params: none
1100 res = append(res, cc.NewReply(t))
1101
1102 return res, err
1103 }
1104
1105 func HandleDelNewsArt(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1106 // Request Fields
1107 // 325 News path
1108 // 326 News article ID
1109 // 337 News article – recursive delete Delete child articles (1) or not (0)
1110 pathStrs := ReadNewsPath(t.GetField(fieldNewsPath).Data)
1111 ID := binary.BigEndian.Uint16(t.GetField(fieldNewsArtID).Data)
1112
1113 // TODO: Delete recursive
1114 cats := cc.Server.GetNewsCatByPath(pathStrs[:len(pathStrs)-1])
1115
1116 catName := pathStrs[len(pathStrs)-1]
1117 cat := cats[catName]
1118
1119 delete(cat.Articles, uint32(ID))
1120
1121 cats[catName] = cat
1122 if err := cc.Server.writeThreadedNews(); err != nil {
1123 return res, err
1124 }
1125
1126 res = append(res, cc.NewReply(t))
1127 return res, err
1128 }
1129
1130 func HandlePostNewsArt(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1131 // Request fields
1132 // 325 News path
1133 // 326 News article ID ID of the parent article?
1134 // 328 News article title
1135 // 334 News article flags
1136 // 327 News article data flavor Currently “text/plain”
1137 // 333 News article data
1138
1139 pathStrs := ReadNewsPath(t.GetField(fieldNewsPath).Data)
1140 cats := cc.Server.GetNewsCatByPath(pathStrs[:len(pathStrs)-1])
1141
1142 catName := pathStrs[len(pathStrs)-1]
1143 cat := cats[catName]
1144
1145 newArt := NewsArtData{
1146 Title: string(t.GetField(fieldNewsArtTitle).Data),
1147 Poster: string(cc.UserName),
1148 Date: toHotlineTime(time.Now()),
1149 PrevArt: []byte{0, 0, 0, 0},
1150 NextArt: []byte{0, 0, 0, 0},
1151 ParentArt: append([]byte{0, 0}, t.GetField(fieldNewsArtID).Data...),
1152 FirstChildArt: []byte{0, 0, 0, 0},
1153 DataFlav: []byte("text/plain"),
1154 Data: string(t.GetField(fieldNewsArtData).Data),
1155 }
1156
1157 var keys []int
1158 for k := range cat.Articles {
1159 keys = append(keys, int(k))
1160 }
1161
1162 nextID := uint32(1)
1163 if len(keys) > 0 {
1164 sort.Ints(keys)
1165 prevID := uint32(keys[len(keys)-1])
1166 nextID = prevID + 1
1167
1168 binary.BigEndian.PutUint32(newArt.PrevArt, prevID)
1169
1170 // Set next article ID
1171 binary.BigEndian.PutUint32(cat.Articles[prevID].NextArt, nextID)
1172 }
1173
1174 // Update parent article with first child reply
1175 parentID := binary.BigEndian.Uint16(t.GetField(fieldNewsArtID).Data)
1176 if parentID != 0 {
1177 parentArt := cat.Articles[uint32(parentID)]
1178
1179 if bytes.Equal(parentArt.FirstChildArt, []byte{0, 0, 0, 0}) {
1180 binary.BigEndian.PutUint32(parentArt.FirstChildArt, nextID)
1181 }
1182 }
1183
1184 cat.Articles[nextID] = &newArt
1185
1186 cats[catName] = cat
1187 if err := cc.Server.writeThreadedNews(); err != nil {
1188 return res, err
1189 }
1190
1191 res = append(res, cc.NewReply(t))
1192 return res, err
1193 }
1194
1195 // HandleGetMsgs returns the flat news data
1196 func HandleGetMsgs(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1197 res = append(res, cc.NewReply(t, NewField(fieldData, cc.Server.FlatNews)))
1198
1199 return res, err
1200 }
1201
1202 func HandleDownloadFile(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1203 fileName := t.GetField(fieldFileName).Data
1204 filePath := t.GetField(fieldFilePath).Data
1205
1206 var fp FilePath
1207 err = fp.UnmarshalBinary(filePath)
1208 if err != nil {
1209 return res, err
1210 }
1211
1212 ffo, err := NewFlattenedFileObject(cc.Server.Config.FileRoot, filePath, fileName)
1213 if err != nil {
1214 return res, err
1215 }
1216
1217 transactionRef := cc.Server.NewTransactionRef()
1218 data := binary.BigEndian.Uint32(transactionRef)
1219
1220 ft := &FileTransfer{
1221 FileName: fileName,
1222 FilePath: filePath,
1223 ReferenceNumber: transactionRef,
1224 Type: FileDownload,
1225 }
1226
1227 cc.Server.FileTransfers[data] = ft
1228 cc.Transfers[FileDownload] = append(cc.Transfers[FileDownload], ft)
1229
1230 res = append(res, cc.NewReply(t,
1231 NewField(fieldRefNum, transactionRef),
1232 NewField(fieldWaitingCount, []byte{0x00, 0x00}), // TODO: Implement waiting count
1233 NewField(fieldTransferSize, ffo.TransferSize()),
1234 NewField(fieldFileSize, ffo.FlatFileDataForkHeader.DataSize),
1235 ))
1236
1237 return res, err
1238 }
1239
1240 // Download all files from the specified folder and sub-folders
1241 // response example
1242 //
1243 // 00
1244 // 01
1245 // 00 00
1246 // 00 00 00 11
1247 // 00 00 00 00
1248 // 00 00 00 18
1249 // 00 00 00 18
1250 //
1251 // 00 03
1252 //
1253 // 00 6c // transfer size
1254 // 00 04 // len
1255 // 00 0f d5 ae
1256 //
1257 // 00 dc // field Folder item count
1258 // 00 02 // len
1259 // 00 02
1260 //
1261 // 00 6b // ref number
1262 // 00 04 // len
1263 // 00 03 64 b1
1264 func HandleDownloadFolder(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1265 transactionRef := cc.Server.NewTransactionRef()
1266 data := binary.BigEndian.Uint32(transactionRef)
1267
1268 fileTransfer := &FileTransfer{
1269 FileName: t.GetField(fieldFileName).Data,
1270 FilePath: t.GetField(fieldFilePath).Data,
1271 ReferenceNumber: transactionRef,
1272 Type: FolderDownload,
1273 }
1274 cc.Server.FileTransfers[data] = fileTransfer
1275 cc.Transfers[FolderDownload] = append(cc.Transfers[FolderDownload], fileTransfer)
1276
1277 var fp FilePath
1278 err = fp.UnmarshalBinary(t.GetField(fieldFilePath).Data)
1279 if err != nil {
1280 return res, err
1281 }
1282
1283 fullFilePath, err := readPath(cc.Server.Config.FileRoot, t.GetField(fieldFilePath).Data, t.GetField(fieldFileName).Data)
1284 if err != nil {
1285 return res, err
1286 }
1287
1288 transferSize, err := CalcTotalSize(fullFilePath)
1289 if err != nil {
1290 return res, err
1291 }
1292 itemCount, err := CalcItemCount(fullFilePath)
1293 if err != nil {
1294 return res, err
1295 }
1296 res = append(res, cc.NewReply(t,
1297 NewField(fieldRefNum, transactionRef),
1298 NewField(fieldTransferSize, transferSize),
1299 NewField(fieldFolderItemCount, itemCount),
1300 NewField(fieldWaitingCount, []byte{0x00, 0x00}), // TODO: Implement waiting count
1301 ))
1302 return res, err
1303 }
1304
1305 // Upload all files from the local folder and its subfolders to the specified path on the server
1306 // Fields used in the request
1307 // 201 File name
1308 // 202 File path
1309 // 108 transfer size Total size of all items in the folder
1310 // 220 Folder item count
1311 // 204 File transfer options "Optional Currently set to 1" (TODO: ??)
1312 func HandleUploadFolder(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1313 transactionRef := cc.Server.NewTransactionRef()
1314 data := binary.BigEndian.Uint32(transactionRef)
1315
1316 fileTransfer := &FileTransfer{
1317 FileName: t.GetField(fieldFileName).Data,
1318 FilePath: t.GetField(fieldFilePath).Data,
1319 ReferenceNumber: transactionRef,
1320 Type: FolderUpload,
1321 FolderItemCount: t.GetField(fieldFolderItemCount).Data,
1322 TransferSize: t.GetField(fieldTransferSize).Data,
1323 }
1324 cc.Server.FileTransfers[data] = fileTransfer
1325
1326 res = append(res, cc.NewReply(t, NewField(fieldRefNum, transactionRef)))
1327 return res, err
1328 }
1329
1330 func HandleUploadFile(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1331 // TODO: add permission handing for upload folders and drop boxes
1332 if !authorize(cc.Account.Access, accessUploadFile) {
1333 res = append(res, cc.NewErrReply(t, "You are not allowed to upload files."))
1334 return res, err
1335 }
1336
1337 fileName := t.GetField(fieldFileName).Data
1338 filePath := t.GetField(fieldFilePath).Data
1339
1340 transactionRef := cc.Server.NewTransactionRef()
1341 data := binary.BigEndian.Uint32(transactionRef)
1342
1343 cc.Server.FileTransfers[data] = &FileTransfer{
1344 FileName: fileName,
1345 FilePath: filePath,
1346 ReferenceNumber: transactionRef,
1347 Type: FileUpload,
1348 }
1349
1350 res = append(res, cc.NewReply(t, NewField(fieldRefNum, transactionRef)))
1351 return res, err
1352 }
1353
1354 // User options
1355 const (
1356 refusePM = 0
1357 refuseChat = 1
1358 autoResponse = 2
1359 )
1360
1361 func HandleSetClientUserInfo(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1362 var icon []byte
1363 if len(t.GetField(fieldUserIconID).Data) == 4 {
1364 icon = t.GetField(fieldUserIconID).Data[2:]
1365 } else {
1366 icon = t.GetField(fieldUserIconID).Data
1367 }
1368 *cc.Icon = icon
1369 cc.UserName = t.GetField(fieldUserName).Data
1370
1371 // the options field is only passed by the client versions > 1.2.3.
1372 options := t.GetField(fieldOptions).Data
1373
1374 if options != nil {
1375 optBitmap := big.NewInt(int64(binary.BigEndian.Uint16(options)))
1376 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(*cc.Flags)))
1377
1378 flagBitmap.SetBit(flagBitmap, userFlagRefusePM, optBitmap.Bit(refusePM))
1379 binary.BigEndian.PutUint16(*cc.Flags, uint16(flagBitmap.Int64()))
1380
1381 flagBitmap.SetBit(flagBitmap, userFLagRefusePChat, optBitmap.Bit(refuseChat))
1382 binary.BigEndian.PutUint16(*cc.Flags, uint16(flagBitmap.Int64()))
1383
1384 // Check auto response
1385 if optBitmap.Bit(autoResponse) == 1 {
1386 cc.AutoReply = t.GetField(fieldAutomaticResponse).Data
1387 } else {
1388 cc.AutoReply = []byte{}
1389 }
1390 }
1391
1392 // Notify all clients of updated user info
1393 cc.sendAll(
1394 tranNotifyChangeUser,
1395 NewField(fieldUserID, *cc.ID),
1396 NewField(fieldUserIconID, *cc.Icon),
1397 NewField(fieldUserFlags, *cc.Flags),
1398 NewField(fieldUserName, cc.UserName),
1399 )
1400
1401 return res, err
1402 }
1403
1404 // HandleKeepAlive responds to keepalive transactions with an empty reply
1405 // * HL 1.9.2 Client sends keepalive msg every 3 minutes
1406 // * HL 1.2.3 Client doesn't send keepalives
1407 func HandleKeepAlive(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1408 res = append(res, cc.NewReply(t))
1409
1410 return res, err
1411 }
1412
1413 func HandleGetFileNameList(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1414 fullPath, err := readPath(
1415 cc.Server.Config.FileRoot,
1416 t.GetField(fieldFilePath).Data,
1417 nil,
1418 )
1419 if err != nil {
1420 return res, err
1421 }
1422
1423 fileNames, err := getFileNameList(fullPath)
1424 if err != nil {
1425 return res, err
1426 }
1427
1428 res = append(res, cc.NewReply(t, fileNames...))
1429
1430 return res, err
1431 }
1432
1433 // =================================
1434 // Hotline private chat flow
1435 // =================================
1436 // 1. ClientA sends tranInviteNewChat to server with user ID to invite
1437 // 2. Server creates new ChatID
1438 // 3. Server sends tranInviteToChat to invitee
1439 // 4. Server replies to ClientA with new Chat ID
1440 //
1441 // A dialog box pops up in the invitee client with options to accept or decline the invitation.
1442 // If Accepted is clicked:
1443 // 1. ClientB sends tranJoinChat with fieldChatID
1444
1445 // HandleInviteNewChat invites users to new private chat
1446 func HandleInviteNewChat(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1447 // Client to Invite
1448 targetID := t.GetField(fieldUserID).Data
1449 newChatID := cc.Server.NewPrivateChat(cc)
1450
1451 res = append(res,
1452 *NewTransaction(
1453 tranInviteToChat,
1454 &targetID,
1455 NewField(fieldChatID, newChatID),
1456 NewField(fieldUserName, cc.UserName),
1457 NewField(fieldUserID, *cc.ID),
1458 ),
1459 )
1460
1461 res = append(res,
1462 cc.NewReply(t,
1463 NewField(fieldChatID, newChatID),
1464 NewField(fieldUserName, cc.UserName),
1465 NewField(fieldUserID, *cc.ID),
1466 NewField(fieldUserIconID, *cc.Icon),
1467 NewField(fieldUserFlags, *cc.Flags),
1468 ),
1469 )
1470
1471 return res, err
1472 }
1473
1474 func HandleInviteToChat(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1475 // Client to Invite
1476 targetID := t.GetField(fieldUserID).Data
1477 chatID := t.GetField(fieldChatID).Data
1478
1479 res = append(res,
1480 *NewTransaction(
1481 tranInviteToChat,
1482 &targetID,
1483 NewField(fieldChatID, chatID),
1484 NewField(fieldUserName, cc.UserName),
1485 NewField(fieldUserID, *cc.ID),
1486 ),
1487 )
1488 res = append(res,
1489 cc.NewReply(
1490 t,
1491 NewField(fieldChatID, chatID),
1492 NewField(fieldUserName, cc.UserName),
1493 NewField(fieldUserID, *cc.ID),
1494 NewField(fieldUserIconID, *cc.Icon),
1495 NewField(fieldUserFlags, *cc.Flags),
1496 ),
1497 )
1498
1499 return res, err
1500 }
1501
1502 func HandleRejectChatInvite(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1503 chatID := t.GetField(fieldChatID).Data
1504 chatInt := binary.BigEndian.Uint32(chatID)
1505
1506 privChat := cc.Server.PrivateChats[chatInt]
1507
1508 resMsg := append(cc.UserName, []byte(" declined invitation to chat")...)
1509
1510 for _, c := range sortedClients(privChat.ClientConn) {
1511 res = append(res,
1512 *NewTransaction(
1513 tranChatMsg,
1514 c.ID,
1515 NewField(fieldChatID, chatID),
1516 NewField(fieldData, resMsg),
1517 ),
1518 )
1519 }
1520
1521 return res, err
1522 }
1523
1524 // HandleJoinChat is sent from a v1.8+ Hotline client when the joins a private chat
1525 // Fields used in the reply:
1526 // * 115 Chat subject
1527 // * 300 User name with info (Optional)
1528 // * 300 (more user names with info)
1529 func HandleJoinChat(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1530 chatID := t.GetField(fieldChatID).Data
1531 chatInt := binary.BigEndian.Uint32(chatID)
1532
1533 privChat := cc.Server.PrivateChats[chatInt]
1534
1535 // Send tranNotifyChatChangeUser to current members of the chat to inform of new user
1536 for _, c := range sortedClients(privChat.ClientConn) {
1537 res = append(res,
1538 *NewTransaction(
1539 tranNotifyChatChangeUser,
1540 c.ID,
1541 NewField(fieldChatID, chatID),
1542 NewField(fieldUserName, cc.UserName),
1543 NewField(fieldUserID, *cc.ID),
1544 NewField(fieldUserIconID, *cc.Icon),
1545 NewField(fieldUserFlags, *cc.Flags),
1546 ),
1547 )
1548 }
1549
1550 privChat.ClientConn[cc.uint16ID()] = cc
1551
1552 replyFields := []Field{NewField(fieldChatSubject, []byte(privChat.Subject))}
1553 for _, c := range sortedClients(privChat.ClientConn) {
1554 user := User{
1555 ID: *c.ID,
1556 Icon: *c.Icon,
1557 Flags: *c.Flags,
1558 Name: string(c.UserName),
1559 }
1560
1561 replyFields = append(replyFields, NewField(fieldUsernameWithInfo, user.Payload()))
1562 }
1563
1564 res = append(res, cc.NewReply(t, replyFields...))
1565 return res, err
1566 }
1567
1568 // HandleLeaveChat is sent from a v1.8+ Hotline client when the user exits a private chat
1569 // Fields used in the request:
1570 // * 114 fieldChatID
1571 // Reply is not expected.
1572 func HandleLeaveChat(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1573 chatID := t.GetField(fieldChatID).Data
1574 chatInt := binary.BigEndian.Uint32(chatID)
1575
1576 privChat := cc.Server.PrivateChats[chatInt]
1577
1578 delete(privChat.ClientConn, cc.uint16ID())
1579
1580 // Notify members of the private chat that the user has left
1581 for _, c := range sortedClients(privChat.ClientConn) {
1582 res = append(res,
1583 *NewTransaction(
1584 tranNotifyChatDeleteUser,
1585 c.ID,
1586 NewField(fieldChatID, chatID),
1587 NewField(fieldUserID, *cc.ID),
1588 ),
1589 )
1590 }
1591
1592 return res, err
1593 }
1594
1595 // HandleSetChatSubject is sent from a v1.8+ Hotline client when the user sets a private chat subject
1596 // Fields used in the request:
1597 // * 114 Chat ID
1598 // * 115 Chat subject Chat subject string
1599 // Reply is not expected.
1600 func HandleSetChatSubject(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1601 chatID := t.GetField(fieldChatID).Data
1602 chatInt := binary.BigEndian.Uint32(chatID)
1603
1604 privChat := cc.Server.PrivateChats[chatInt]
1605 privChat.Subject = string(t.GetField(fieldChatSubject).Data)
1606
1607 for _, c := range sortedClients(privChat.ClientConn) {
1608 res = append(res,
1609 *NewTransaction(
1610 tranNotifyChatSubject,
1611 c.ID,
1612 NewField(fieldChatID, chatID),
1613 NewField(fieldChatSubject, t.GetField(fieldChatSubject).Data),
1614 ),
1615 )
1616 }
1617
1618 return res, err
1619 }
1620
1621 // HandleMakeAlias makes a file alias using the specified path.
1622 // Fields used in the request:
1623 // 201 File name
1624 // 202 File path
1625 // 212 File new path Destination path
1626 //
1627 // Fields used in the reply:
1628 // None
1629 func HandleMakeAlias(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1630 if !authorize(cc.Account.Access, accessMakeAlias) {
1631 res = append(res, cc.NewErrReply(t, "You are not allowed to make aliases."))
1632 return res, err
1633 }
1634 fileName := t.GetField(fieldFileName).Data
1635 filePath := t.GetField(fieldFilePath).Data
1636 fileNewPath := t.GetField(fieldFileNewPath).Data
1637
1638 fullFilePath, err := readPath(cc.Server.Config.FileRoot, filePath, fileName)
1639 if err != nil {
1640 return res, err
1641 }
1642
1643 fullNewFilePath, err := readPath(cc.Server.Config.FileRoot, fileNewPath, fileName)
1644 if err != nil {
1645 return res, err
1646 }
1647
1648 cc.Server.Logger.Debugw("Make alias", "src", fullFilePath, "dst", fullNewFilePath)
1649
1650 if err := FS.Symlink(fullFilePath, fullNewFilePath); err != nil {
1651 res = append(res, cc.NewErrReply(t, "Error creating alias"))
1652 return res, nil
1653 }
1654
1655 res = append(res, cc.NewReply(t))
1656 return res, err
1657 }