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