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