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