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