]> git.r.bdr.sh - rbdr/mobius/blame - hotline/transaction_handlers.go
Implement handling of special case Dropbox and Upload folders
[rbdr/mobius] / hotline / transaction_handlers.go
CommitLineData
6988a057
JH
1package hotline
2
3import (
4 "bytes"
5 "encoding/binary"
6 "errors"
7 "fmt"
6988a057
JH
8 "gopkg.in/yaml.v2"
9 "io/ioutil"
10 "math/big"
11 "os"
00d1ef67 12 "path"
6988a057
JH
13 "sort"
14 "strings"
15 "time"
16)
17
18type 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
26var 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 },
5454019c
JH
44 tranNotifyDeleteUser: {
45 Name: "tranNotifyDeleteUser",
46 },
6988a057 47 tranAgreed: {
6988a057
JH
48 Name: "tranAgreed",
49 Handler: HandleTranAgreed,
50 },
51 tranChatSend: {
6988a057
JH
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: {
6988a057
JH
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: {
6988a057
JH
74 Name: "tranDeleteFile",
75 Handler: HandleDeleteFile,
76 },
77 tranDeleteUser: {
6988a057
JH
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: {
6988a057
JH
106 Name: "tranGetFileInfo",
107 Handler: HandleGetFileInfo,
108 },
109 tranGetFileNameList: {
6988a057
JH
110 Name: "tranGetFileNameList",
111 Handler: HandleGetFileNameList,
112 },
113 tranGetMsgs: {
5454019c
JH
114 Access: accessNewsReadArt,
115 DenyMsg: "You are not allowed to read news.",
6988a057
JH
116 Name: "tranGetMsgs",
117 Handler: HandleGetMsgs,
118 },
119 tranGetNewsArtData: {
5454019c
JH
120 Access: accessNewsReadArt,
121 DenyMsg: "You are not allowed to read news.",
6988a057
JH
122 Name: "tranGetNewsArtData",
123 Handler: HandleGetNewsArtData,
124 },
125 tranGetNewsArtNameList: {
5454019c
JH
126 Access: accessNewsReadArt,
127 DenyMsg: "You are not allowed to read news.",
6988a057
JH
128 Name: "tranGetNewsArtNameList",
129 Handler: HandleGetNewsArtNameList,
130 },
131 tranGetNewsCatNameList: {
5454019c
JH
132 Access: accessNewsReadArt,
133 DenyMsg: "You are not allowed to read news.",
6988a057
JH
134 Name: "tranGetNewsCatNameList",
135 Handler: HandleGetNewsCatNameList,
136 },
137 tranGetUser: {
6988a057
JH
138 DenyMsg: "You are not allowed to view accounts.",
139 Name: "tranGetUser",
140 Handler: HandleGetUser,
141 },
142 tranGetUserNameList: {
6988a057
JH
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: {
5454019c
JH
153 Access: accessOpenChat,
154 DenyMsg: "You are not allowed to request private chat.",
6988a057
JH
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 },
5454019c 170
6988a057
JH
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: {
5454019c
JH
184 Access: accessCreateFolder,
185 DenyMsg: "You are not allow to create folders.",
6988a057
JH
186 Name: "tranNewFolder",
187 Handler: HandleNewFolder,
188 },
189 tranNewNewsCat: {
5454019c
JH
190 Access: accessNewsCreateCat,
191 DenyMsg: "You are not allowed to create news categories.",
6988a057
JH
192 Name: "tranNewNewsCat",
193 Handler: HandleNewNewsCat,
194 },
195 tranNewNewsFldr: {
5454019c
JH
196 Access: accessNewsCreateFldr,
197 DenyMsg: "You are not allowed to create news folders.",
6988a057
JH
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: {
5454019c
JH
208 Access: accessNewsPostArt,
209 DenyMsg: "You are not allowed to post news.",
6988a057
JH
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: {
5454019c 224 Access: accessAlwaysAllow,
aebc4d36
JH
225 // Access: accessSendPrivMsg,
226 // DenyMsg: "You are not allowed to send private messages",
6988a057
JH
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 },
decc2fbf 243 tranMakeFileAlias: {
decc2fbf
JH
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 },
6988a057 252 tranSetClientUserInfo: {
6988a057
JH
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: {
6988a057
JH
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
282func HandleChatSend(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
003a743e
JH
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
6988a057 288 // Truncate long usernames
72dd37f1 289 trunc := fmt.Sprintf("%13s", cc.UserName)
6988a057
JH
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 {
72dd37f1 296 formattedMsg = fmt.Sprintf("\r*** %s %s", cc.UserName, t.GetField(fieldData).Data)
6988a057
JH
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//
aebc4d36 343// Fields used in the reply:
6988a057
JH
344// None
345func HandleSendInstantMsg(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
346 msg := t.GetField(fieldData)
347 ID := t.GetField(fieldUserID)
348 // TODO: Implement reply quoting
aebc4d36 349 // options := transaction.GetField(hotline.fieldOptions)
6988a057
JH
350
351 res = append(res,
352 *NewTransaction(
353 tranServerMsg,
354 &ID.Data,
355 NewField(fieldData, msg.Data),
72dd37f1 356 NewField(fieldUserName, cc.UserName),
6988a057
JH
357 NewField(fieldUserID, *cc.ID),
358 NewField(fieldOptions, []byte{0, 1}),
359 ),
360 )
361 id, _ := byteToInt(ID.Data)
362
6988a057
JH
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
aebc4d36 369 if len(otherClient.AutoReply) > 0 {
6988a057
JH
370 res = append(res,
371 *NewTransaction(
372 tranServerMsg,
373 cc.ID,
aebc4d36 374 NewField(fieldData, otherClient.AutoReply),
72dd37f1 375 NewField(fieldUserName, otherClient.UserName),
6988a057
JH
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
387func HandleGetFileInfo(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
92a7e455
JH
388 fileName := t.GetField(fieldFileName).Data
389 filePath := t.GetField(fieldFilePath).Data
6988a057 390
92a7e455 391 ffo, err := NewFlattenedFileObject(cc.Server.Config.FileRoot, filePath, fileName)
6988a057
JH
392 if err != nil {
393 return res, err
394 }
395
396 res = append(res, cc.NewReply(t,
92a7e455 397 NewField(fieldFileName, fileName),
6988a057
JH
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
417func HandleSetFileInfo(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
92a7e455
JH
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
aebc4d36 431 // fileComment := t.GetField(fieldFileComment).Data
6988a057
JH
432 fileNewName := t.GetField(fieldFileNewName).Data
433
434 if fileNewName != nil {
92a7e455 435 fi, err := FS.Stat(fullFilePath)
6988a057
JH
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
92a7e455 452 err = os.Rename(fullFilePath, fullNewFilePath)
6988a057 453 if os.IsNotExist(err) {
92a7e455 454 res = append(res, cc.NewErrReply(t, "Cannot rename file "+string(fileName)+" because it does not exist or cannot be found."))
6988a057
JH
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
468func HandleDeleteFile(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
92a7e455
JH
469 fileName := t.GetField(fieldFileName).Data
470 filePath := t.GetField(fieldFilePath).Data
6988a057 471
92a7e455
JH
472 fullFilePath, err := readPath(cc.Server.Config.FileRoot, filePath, fileName)
473 if err != nil {
474 return res, err
475 }
6988a057 476
92a7e455 477 cc.Server.Logger.Debugw("Delete file", "src", fullFilePath)
6988a057 478
92a7e455 479 fi, err := os.Stat(fullFilePath)
6988a057 480 if err != nil {
92a7e455 481 res = append(res, cc.NewErrReply(t, "Cannot delete file "+string(fileName)+" because it does not exist or cannot be found."))
6988a057
JH
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
92a7e455 497 if err := os.RemoveAll(fullFilePath); err != nil {
6988a057
JH
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
506func HandleMoveFile(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
507 fileName := string(t.GetField(fieldFileName).Data)
00d1ef67
JH
508 filePath := cc.Server.Config.FileRoot + ReadFilePath(t.GetField(fieldFilePath).Data)
509 fileNewPath := cc.Server.Config.FileRoot + ReadFilePath(t.GetField(fieldFileNewPath).Data)
6988a057
JH
510
511 cc.Server.Logger.Debugw("Move file", "src", filePath+"/"+fileName, "dst", fileNewPath+"/"+fileName)
512
003a743e
JH
513 fp := filePath + "/" + fileName
514 fi, err := os.Stat(fp)
6988a057
JH
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
545func HandleNewFolder(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
546 newFolderPath := cc.Server.Config.FileRoot
00d1ef67
JH
547 folderName := string(t.GetField(fieldFileName).Data)
548
549 folderName = path.Join("/", folderName)
6988a057
JH
550
551 // fieldFilePath is only present for nested paths
552 if t.GetField(fieldFilePath).Data != nil {
72dd37f1 553 var newFp FilePath
00d1ef67
JH
554 err := newFp.UnmarshalBinary(t.GetField(fieldFilePath).Data)
555 if err != nil {
556 return nil, err
557 }
6988a057
JH
558 newFolderPath += newFp.String()
559 }
00d1ef67 560 newFolderPath = path.Join(newFolderPath, folderName)
6988a057 561
00d1ef67
JH
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
6988a057
JH
574 }
575
576 res = append(res, cc.NewReply(t))
577 return res, err
578}
579
580func 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),
72dd37f1 629 NewField(fieldUserName, c.UserName),
6988a057
JH
630 NewField(fieldUserIconID, *c.Icon),
631 )
632 }
633 }
634
6988a057
JH
635 res = append(res, cc.NewReply(t))
636 return res, err
637}
638
639func HandleGetUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
003a743e
JH
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
aebc4d36 645 account := cc.Server.Accounts[string(t.GetField(fieldUserLogin).Data)]
6988a057
JH
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)),
b25c4a19 654 NewField(fieldUserLogin, negateString(t.GetField(fieldUserLogin).Data)),
6988a057
JH
655 NewField(fieldUserPassword, []byte(account.Password)),
656 NewField(fieldUserAccess, *account.Access),
657 ))
658 return res, err
659}
660
661func 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 {
c5d9af5a 665 userField := acc.MarshalBinary()
6988a057
JH
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
674func 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
697func HandleDeleteUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
003a743e
JH
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
6988a057
JH
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
715func 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
726func 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
737func 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
747Name: %s
748Account: %s
749Address: %s
750
751-------- File Downloads ---------
752
753%s
754
755------- Folder Downloads --------
756
757None.
758
759--------- File Uploads ----------
760
761None.
762
763-------- Folder Uploads ---------
764
765None.
766
767------- Waiting Downloads -------
768
769None.
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,
72dd37f1 781 clientConn.UserName,
6988a057
JH
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)),
72dd37f1 791 NewField(fieldUserName, clientConn.UserName),
6988a057
JH
792 ))
793 return res, err
794}
795
796func 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
6988a057 802func HandleTranAgreed(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
bd1ce113 803 cc.Agreed = true
72dd37f1 804 cc.UserName = t.GetField(fieldUserName).Data
6988a057
JH
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 {
aebc4d36 826 cc.AutoReply = t.GetField(fieldAutomaticResponse).Data
6988a057 827 } else {
aebc4d36 828 cc.AutoReply = []byte{}
6988a057
JH
829 }
830
003a743e
JH
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 )
6988a057
JH
840
841 res = append(res, cc.NewReply(t))
842
843 return res, err
844}
845
846const defaultNewsDateFormat = "Jan02 15:04" // Jun23 20:49
847// "Mon, 02 Jan 2006 15:04:05 MST"
848
849const 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
858func 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
72dd37f1 872 newsPost := fmt.Sprintf(newsTemplate+"\r", cc.UserName, time.Now().Format(newsDateTemplate), t.GetField(fieldData).Data)
6988a057
JH
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
893func 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
909func 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]
72dd37f1 931 b, _ := cat.MarshalBinary()
6988a057
JH
932 fieldData = append(fieldData, NewField(
933 fieldNewsCatListData15,
72dd37f1 934 b,
6988a057
JH
935 ))
936 }
937
938 res = append(res, cc.NewReply(t, fieldData...))
939 return res, err
940}
941
942func 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
961func 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
989func 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
003a743e
JH
995 for _, fp := range pathStrs {
996 cat = cats[fp]
997 cats = cats[fp].SubCats
6988a057
JH
998 }
999
1000 nald := cat.GetNewsArtListData()
1001
1002 res = append(res, cc.NewReply(t, NewField(fieldNewsArtListData, nald.Payload())))
1003 return res, err
1004}
1005
1006func HandleGetNewsArtData(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1007 // Request fields
003a743e 1008 // 325 News fp
6988a057
JH
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
003a743e
JH
1017 for _, fp := range pathStrs {
1018 cat = cats[fp]
1019 cats = cats[fp].SubCats
6988a057
JH
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
1056func 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 {
7e2e07da
JH
1069 for _, fp := range pathStrs[0 : len(pathStrs)-1] {
1070 cats = cats[fp].SubCats
6988a057
JH
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
1087func 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
1112func 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),
72dd37f1 1129 Poster: string(cc.UserName),
3c9b1dcd 1130 Date: toHotlineTime(time.Now()),
6988a057
JH
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
1178func 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
1184func HandleDownloadFile(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1185 fileName := t.GetField(fieldFileName).Data
92a7e455 1186 filePath := t.GetField(fieldFilePath).Data
6988a057 1187
92a7e455
JH
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)
6988a057
JH
1195 if err != nil {
1196 return res, err
1197 }
1198
1199 transactionRef := cc.Server.NewTransactionRef()
1200 data := binary.BigEndian.Uint32(transactionRef)
1201
6988a057
JH
1202 ft := &FileTransfer{
1203 FileName: fileName,
92a7e455 1204 FilePath: filePath,
6988a057
JH
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
1246func 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
72dd37f1 1259 var fp FilePath
c5d9af5a
JH
1260 err = fp.UnmarshalBinary(t.GetField(fieldFilePath).Data)
1261 if err != nil {
1262 return res, err
1263 }
6988a057 1264
92a7e455 1265 fullFilePath, err := readPath(cc.Server.Config.FileRoot, t.GetField(fieldFilePath).Data, t.GetField(fieldFileName).Data)
aebc4d36
JH
1266 if err != nil {
1267 return res, err
1268 }
92a7e455 1269
6988a057
JH
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
df2735b2 1291// 108 transfer size Total size of all items in the folder
6988a057
JH
1292// 220 Folder item count
1293// 204 File transfer options "Optional Currently set to 1" (TODO: ??)
1294func HandleUploadFolder(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1295 transactionRef := cc.Server.NewTransactionRef()
1296 data := binary.BigEndian.Uint32(transactionRef)
1297
7e2e07da
JH
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
6988a057
JH
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
7e2e07da
JH
1327// HandleUploadFile
1328// Special cases:
1329// * If the target directory contains "uploads" (case insensitive)
6988a057 1330func HandleUploadFile(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
a0241c25
JH
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
6988a057
JH
1336 fileName := t.GetField(fieldFileName).Data
1337 filePath := t.GetField(fieldFilePath).Data
1338
7e2e07da
JH
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
6988a057
JH
1354 transactionRef := cc.Server.NewTransactionRef()
1355 data := binary.BigEndian.Uint32(transactionRef)
1356
a0241c25 1357 cc.Server.FileTransfers[data] = &FileTransfer{
6988a057
JH
1358 FileName: fileName,
1359 FilePath: filePath,
1360 ReferenceNumber: transactionRef,
1361 Type: FileUpload,
1362 }
1363
6988a057
JH
1364 res = append(res, cc.NewReply(t, NewField(fieldRefNum, transactionRef)))
1365 return res, err
1366}
1367
6988a057
JH
1368func 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
72dd37f1 1376 cc.UserName = t.GetField(fieldUserName).Data
6988a057
JH
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
7f12122f
JH
1385 flagBitmap.SetBit(flagBitmap, userFlagRefusePM, optBitmap.Bit(refusePM))
1386 binary.BigEndian.PutUint16(*cc.Flags, uint16(flagBitmap.Int64()))
6988a057 1387
7f12122f
JH
1388 flagBitmap.SetBit(flagBitmap, userFLagRefusePChat, optBitmap.Bit(refuseChat))
1389 binary.BigEndian.PutUint16(*cc.Flags, uint16(flagBitmap.Int64()))
6988a057
JH
1390
1391 // Check auto response
1392 if optBitmap.Bit(autoResponse) == 1 {
aebc4d36 1393 cc.AutoReply = t.GetField(fieldAutomaticResponse).Data
6988a057 1394 } else {
aebc4d36 1395 cc.AutoReply = []byte{}
6988a057
JH
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),
72dd37f1 1405 NewField(fieldUserName, cc.UserName),
6988a057
JH
1406 )
1407
1408 return res, err
1409}
1410
61c272e1
JH
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
6988a057
JH
1414func HandleKeepAlive(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1415 res = append(res, cc.NewReply(t))
1416
1417 return res, err
1418}
1419
1420func HandleGetFileNameList(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
92a7e455
JH
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
6988a057
JH
1428 }
1429
7e2e07da
JH
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
92a7e455 1443 fileNames, err := getFileNameList(fullPath)
6988a057
JH
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
1466func 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),
72dd37f1 1476 NewField(fieldUserName, cc.UserName),
6988a057
JH
1477 NewField(fieldUserID, *cc.ID),
1478 ),
1479 )
1480
1481 res = append(res,
1482 cc.NewReply(t,
1483 NewField(fieldChatID, newChatID),
72dd37f1 1484 NewField(fieldUserName, cc.UserName),
6988a057
JH
1485 NewField(fieldUserID, *cc.ID),
1486 NewField(fieldUserIconID, *cc.Icon),
1487 NewField(fieldUserFlags, *cc.Flags),
1488 ),
1489 )
1490
1491 return res, err
1492}
1493
1494func 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),
72dd37f1 1504 NewField(fieldUserName, cc.UserName),
6988a057
JH
1505 NewField(fieldUserID, *cc.ID),
1506 ),
1507 )
1508 res = append(res,
1509 cc.NewReply(
1510 t,
1511 NewField(fieldChatID, chatID),
72dd37f1 1512 NewField(fieldUserName, cc.UserName),
6988a057
JH
1513 NewField(fieldUserID, *cc.ID),
1514 NewField(fieldUserIconID, *cc.Icon),
1515 NewField(fieldUserFlags, *cc.Flags),
1516 ),
1517 )
1518
1519 return res, err
1520}
1521
1522func 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
72dd37f1 1528 resMsg := append(cc.UserName, []byte(" declined invitation to chat")...)
6988a057
JH
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)
1549func 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),
72dd37f1 1562 NewField(fieldUserName, cc.UserName),
6988a057
JH
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,
72dd37f1 1578 Name: string(c.UserName),
6988a057
JH
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.
1592func 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.
1620func 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}
decc2fbf
JH
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
1649func 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}