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