]> git.r.bdr.sh - rbdr/mobius/blob - hotline/transaction_handlers.go
patch: v0.12.1
[rbdr/mobius] / hotline / transaction_handlers.go
1 package hotline
2
3 import (
4 "bytes"
5 "encoding/binary"
6 "errors"
7 "fmt"
8 "gopkg.in/yaml.v3"
9 "math/big"
10 "os"
11 "path"
12 "path/filepath"
13 "sort"
14 "strings"
15 "time"
16 )
17
18 type HandlerFunc func(*ClientConn, *Transaction) ([]Transaction, error)
19
20 type TransactionType struct {
21 Handler HandlerFunc // function for handling the transaction type
22 Name string // Name of transaction as it will appear in logging
23 RequiredFields []requiredField
24 }
25
26 var TransactionHandlers = map[uint16]TransactionType{
27 // Server initiated
28 TranChatMsg: {
29 Name: "TranChatMsg",
30 },
31 // Server initiated
32 TranNotifyChangeUser: {
33 Name: "TranNotifyChangeUser",
34 },
35 TranError: {
36 Name: "TranError",
37 },
38 TranShowAgreement: {
39 Name: "TranShowAgreement",
40 },
41 TranUserAccess: {
42 Name: "TranUserAccess",
43 },
44 TranNotifyDeleteUser: {
45 Name: "TranNotifyDeleteUser",
46 },
47 TranAgreed: {
48 Name: "TranAgreed",
49 Handler: HandleTranAgreed,
50 },
51 TranChatSend: {
52 Name: "TranChatSend",
53 Handler: HandleChatSend,
54 RequiredFields: []requiredField{
55 {
56 ID: FieldData,
57 minLen: 0,
58 },
59 },
60 },
61 TranDelNewsArt: {
62 Name: "TranDelNewsArt",
63 Handler: HandleDelNewsArt,
64 },
65 TranDelNewsItem: {
66 Name: "TranDelNewsItem",
67 Handler: HandleDelNewsItem,
68 },
69 TranDeleteFile: {
70 Name: "TranDeleteFile",
71 Handler: HandleDeleteFile,
72 },
73 TranDeleteUser: {
74 Name: "TranDeleteUser",
75 Handler: HandleDeleteUser,
76 },
77 TranDisconnectUser: {
78 Name: "TranDisconnectUser",
79 Handler: HandleDisconnectUser,
80 },
81 TranDownloadFile: {
82 Name: "TranDownloadFile",
83 Handler: HandleDownloadFile,
84 },
85 TranDownloadFldr: {
86 Name: "TranDownloadFldr",
87 Handler: HandleDownloadFolder,
88 },
89 TranGetClientInfoText: {
90 Name: "TranGetClientInfoText",
91 Handler: HandleGetClientInfoText,
92 },
93 TranGetFileInfo: {
94 Name: "TranGetFileInfo",
95 Handler: HandleGetFileInfo,
96 },
97 TranGetFileNameList: {
98 Name: "TranGetFileNameList",
99 Handler: HandleGetFileNameList,
100 },
101 TranGetMsgs: {
102 Name: "TranGetMsgs",
103 Handler: HandleGetMsgs,
104 },
105 TranGetNewsArtData: {
106 Name: "TranGetNewsArtData",
107 Handler: HandleGetNewsArtData,
108 },
109 TranGetNewsArtNameList: {
110 Name: "TranGetNewsArtNameList",
111 Handler: HandleGetNewsArtNameList,
112 },
113 TranGetNewsCatNameList: {
114 Name: "TranGetNewsCatNameList",
115 Handler: HandleGetNewsCatNameList,
116 },
117 TranGetUser: {
118 Name: "TranGetUser",
119 Handler: HandleGetUser,
120 },
121 TranGetUserNameList: {
122 Name: "tranHandleGetUserNameList",
123 Handler: HandleGetUserNameList,
124 },
125 TranInviteNewChat: {
126 Name: "TranInviteNewChat",
127 Handler: HandleInviteNewChat,
128 },
129 TranInviteToChat: {
130 Name: "TranInviteToChat",
131 Handler: HandleInviteToChat,
132 },
133 TranJoinChat: {
134 Name: "TranJoinChat",
135 Handler: HandleJoinChat,
136 },
137 TranKeepAlive: {
138 Name: "TranKeepAlive",
139 Handler: HandleKeepAlive,
140 },
141 TranLeaveChat: {
142 Name: "TranJoinChat",
143 Handler: HandleLeaveChat,
144 },
145 TranListUsers: {
146 Name: "TranListUsers",
147 Handler: HandleListUsers,
148 },
149 TranMoveFile: {
150 Name: "TranMoveFile",
151 Handler: HandleMoveFile,
152 },
153 TranNewFolder: {
154 Name: "TranNewFolder",
155 Handler: HandleNewFolder,
156 },
157 TranNewNewsCat: {
158 Name: "TranNewNewsCat",
159 Handler: HandleNewNewsCat,
160 },
161 TranNewNewsFldr: {
162 Name: "TranNewNewsFldr",
163 Handler: HandleNewNewsFldr,
164 },
165 TranNewUser: {
166 Name: "TranNewUser",
167 Handler: HandleNewUser,
168 },
169 TranUpdateUser: {
170 Name: "TranUpdateUser",
171 Handler: HandleUpdateUser,
172 },
173 TranOldPostNews: {
174 Name: "TranOldPostNews",
175 Handler: HandleTranOldPostNews,
176 },
177 TranPostNewsArt: {
178 Name: "TranPostNewsArt",
179 Handler: HandlePostNewsArt,
180 },
181 TranRejectChatInvite: {
182 Name: "TranRejectChatInvite",
183 Handler: HandleRejectChatInvite,
184 },
185 TranSendInstantMsg: {
186 Name: "TranSendInstantMsg",
187 Handler: HandleSendInstantMsg,
188 RequiredFields: []requiredField{
189 {
190 ID: FieldData,
191 minLen: 0,
192 },
193 {
194 ID: FieldUserID,
195 },
196 },
197 },
198 TranSetChatSubject: {
199 Name: "TranSetChatSubject",
200 Handler: HandleSetChatSubject,
201 },
202 TranMakeFileAlias: {
203 Name: "TranMakeFileAlias",
204 Handler: HandleMakeAlias,
205 RequiredFields: []requiredField{
206 {ID: FieldFileName, minLen: 1},
207 {ID: FieldFilePath, minLen: 1},
208 {ID: FieldFileNewPath, minLen: 1},
209 },
210 },
211 TranSetClientUserInfo: {
212 Name: "TranSetClientUserInfo",
213 Handler: HandleSetClientUserInfo,
214 },
215 TranSetFileInfo: {
216 Name: "TranSetFileInfo",
217 Handler: HandleSetFileInfo,
218 },
219 TranSetUser: {
220 Name: "TranSetUser",
221 Handler: HandleSetUser,
222 },
223 TranUploadFile: {
224 Name: "TranUploadFile",
225 Handler: HandleUploadFile,
226 },
227 TranUploadFldr: {
228 Name: "TranUploadFldr",
229 Handler: HandleUploadFolder,
230 },
231 TranUserBroadcast: {
232 Name: "TranUserBroadcast",
233 Handler: HandleUserBroadcast,
234 },
235 TranDownloadBanner: {
236 Name: "TranDownloadBanner",
237 Handler: HandleDownloadBanner,
238 },
239 }
240
241 func HandleChatSend(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
242 if !cc.Authorize(accessSendChat) {
243 res = append(res, cc.NewErrReply(t, "You are not allowed to participate in chat."))
244 return res, err
245 }
246
247 // Truncate long usernames
248 trunc := fmt.Sprintf("%13s", cc.UserName)
249 formattedMsg := fmt.Sprintf("\r%.14s: %s", trunc, t.GetField(FieldData).Data)
250
251 // By holding the option key, Hotline chat allows users to send /me formatted messages like:
252 // *** Halcyon does stuff
253 // This is indicated by the presence of the optional field FieldChatOptions set to a value of 1.
254 // Most clients do not send this option for normal chat messages.
255 if t.GetField(FieldChatOptions).Data != nil && bytes.Equal(t.GetField(FieldChatOptions).Data, []byte{0, 1}) {
256 formattedMsg = fmt.Sprintf("\r*** %s %s", cc.UserName, t.GetField(FieldData).Data)
257 }
258
259 // The ChatID field is used to identify messages as belonging to a private chat.
260 // All clients *except* Frogblast omit this field for public chat, but Frogblast sends a value of 00 00 00 00.
261 chatID := t.GetField(FieldChatID).Data
262 if chatID != nil && !bytes.Equal([]byte{0, 0, 0, 0}, chatID) {
263 chatInt := binary.BigEndian.Uint32(chatID)
264 privChat := cc.Server.PrivateChats[chatInt]
265
266 clients := sortedClients(privChat.ClientConn)
267
268 // send the message to all connected clients of the private chat
269 for _, c := range clients {
270 res = append(res, *NewTransaction(
271 TranChatMsg,
272 c.ID,
273 NewField(FieldChatID, chatID),
274 NewField(FieldData, []byte(formattedMsg)),
275 ))
276 }
277 return res, err
278 }
279
280 for _, c := range sortedClients(cc.Server.Clients) {
281 // Filter out clients that do not have the read chat permission
282 if c.Authorize(accessReadChat) {
283 res = append(res, *NewTransaction(TranChatMsg, c.ID, NewField(FieldData, []byte(formattedMsg))))
284 }
285 }
286
287 return res, err
288 }
289
290 // HandleSendInstantMsg sends instant message to the user on the current server.
291 // Fields used in the request:
292 //
293 // 103 User ID
294 // 113 Options
295 // One of the following values:
296 // - User message (myOpt_UserMessage = 1)
297 // - Refuse message (myOpt_RefuseMessage = 2)
298 // - Refuse chat (myOpt_RefuseChat = 3)
299 // - Automatic response (myOpt_AutomaticResponse = 4)"
300 // 101 Data Optional
301 // 214 Quoting message Optional
302 //
303 // Fields used in the reply:
304 // None
305 func HandleSendInstantMsg(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
306 if !cc.Authorize(accessSendPrivMsg) {
307 res = append(res, cc.NewErrReply(t, "You are not allowed to send private messages."))
308 return res, errors.New("user is not allowed to send private messages")
309 }
310
311 msg := t.GetField(FieldData)
312 ID := t.GetField(FieldUserID)
313
314 reply := NewTransaction(
315 TranServerMsg,
316 &ID.Data,
317 NewField(FieldData, msg.Data),
318 NewField(FieldUserName, cc.UserName),
319 NewField(FieldUserID, *cc.ID),
320 NewField(FieldOptions, []byte{0, 1}),
321 )
322
323 // Later versions of Hotline include the original message in the FieldQuotingMsg field so
324 // the receiving client can display both the received message and what it is in reply to
325 if t.GetField(FieldQuotingMsg).Data != nil {
326 reply.Fields = append(reply.Fields, NewField(FieldQuotingMsg, t.GetField(FieldQuotingMsg).Data))
327 }
328
329 id, err := byteToInt(ID.Data)
330 if err != nil {
331 return res, errors.New("invalid client ID")
332 }
333 otherClient, ok := cc.Server.Clients[uint16(id)]
334 if !ok {
335 return res, errors.New("invalid client ID")
336 }
337
338 // Check if target user has "Refuse private messages" flag
339 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(otherClient.Flags)))
340 if flagBitmap.Bit(UserFlagRefusePChat) == 1 {
341 res = append(res,
342 *NewTransaction(
343 TranServerMsg,
344 cc.ID,
345 NewField(FieldData, []byte(string(otherClient.UserName)+" does not accept private messages.")),
346 NewField(FieldUserName, otherClient.UserName),
347 NewField(FieldUserID, *otherClient.ID),
348 NewField(FieldOptions, []byte{0, 2}),
349 ),
350 )
351 } else {
352 res = append(res, *reply)
353 }
354
355 // Respond with auto reply if other client has it enabled
356 if len(otherClient.AutoReply) > 0 {
357 res = append(res,
358 *NewTransaction(
359 TranServerMsg,
360 cc.ID,
361 NewField(FieldData, otherClient.AutoReply),
362 NewField(FieldUserName, otherClient.UserName),
363 NewField(FieldUserID, *otherClient.ID),
364 NewField(FieldOptions, []byte{0, 1}),
365 ),
366 )
367 }
368
369 res = append(res, cc.NewReply(t))
370
371 return res, err
372 }
373
374 func HandleGetFileInfo(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
375 fileName := t.GetField(FieldFileName).Data
376 filePath := t.GetField(FieldFilePath).Data
377
378 fullFilePath, err := readPath(cc.Server.Config.FileRoot, filePath, fileName)
379 if err != nil {
380 return res, err
381 }
382
383 fw, err := newFileWrapper(cc.Server.FS, fullFilePath, 0)
384 if err != nil {
385 return res, err
386 }
387
388 encodedName, err := txtEncoder.String(fw.name)
389 if err != nil {
390 return res, fmt.Errorf("invalid filepath encoding: %w", err)
391 }
392
393 res = append(res, cc.NewReply(t,
394 NewField(FieldFileName, []byte(encodedName)),
395 NewField(FieldFileTypeString, fw.ffo.FlatFileInformationFork.friendlyType()),
396 NewField(FieldFileCreatorString, fw.ffo.FlatFileInformationFork.friendlyCreator()),
397 NewField(FieldFileComment, fw.ffo.FlatFileInformationFork.Comment),
398 NewField(FieldFileType, fw.ffo.FlatFileInformationFork.TypeSignature),
399 NewField(FieldFileCreateDate, fw.ffo.FlatFileInformationFork.CreateDate),
400 NewField(FieldFileModifyDate, fw.ffo.FlatFileInformationFork.ModifyDate),
401 NewField(FieldFileSize, fw.totalSize()),
402 ))
403 return res, err
404 }
405
406 // HandleSetFileInfo updates a file or folder name and/or comment from the Get Info window
407 // Fields used in the request:
408 // * 201 File name
409 // * 202 File path Optional
410 // * 211 File new name Optional
411 // * 210 File comment Optional
412 // Fields used in the reply: None
413 func HandleSetFileInfo(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
414 fileName := t.GetField(FieldFileName).Data
415 filePath := t.GetField(FieldFilePath).Data
416
417 fullFilePath, err := readPath(cc.Server.Config.FileRoot, filePath, fileName)
418 if err != nil {
419 return res, err
420 }
421
422 fi, err := cc.Server.FS.Stat(fullFilePath)
423 if err != nil {
424 return res, err
425 }
426
427 hlFile, err := newFileWrapper(cc.Server.FS, fullFilePath, 0)
428 if err != nil {
429 return res, err
430 }
431 if t.GetField(FieldFileComment).Data != nil {
432 switch mode := fi.Mode(); {
433 case mode.IsDir():
434 if !cc.Authorize(accessSetFolderComment) {
435 res = append(res, cc.NewErrReply(t, "You are not allowed to set comments for folders."))
436 return res, err
437 }
438 case mode.IsRegular():
439 if !cc.Authorize(accessSetFileComment) {
440 res = append(res, cc.NewErrReply(t, "You are not allowed to set comments for files."))
441 return res, err
442 }
443 }
444
445 if err := hlFile.ffo.FlatFileInformationFork.setComment(t.GetField(FieldFileComment).Data); err != nil {
446 return res, err
447 }
448 w, err := hlFile.infoForkWriter()
449 if err != nil {
450 return res, err
451 }
452 _, err = w.Write(hlFile.ffo.FlatFileInformationFork.MarshalBinary())
453 if err != nil {
454 return res, err
455 }
456 }
457
458 fullNewFilePath, err := readPath(cc.Server.Config.FileRoot, filePath, t.GetField(FieldFileNewName).Data)
459 if err != nil {
460 return nil, err
461 }
462
463 fileNewName := t.GetField(FieldFileNewName).Data
464
465 if fileNewName != nil {
466 switch mode := fi.Mode(); {
467 case mode.IsDir():
468 if !cc.Authorize(accessRenameFolder) {
469 res = append(res, cc.NewErrReply(t, "You are not allowed to rename folders."))
470 return res, err
471 }
472 err = os.Rename(fullFilePath, fullNewFilePath)
473 if os.IsNotExist(err) {
474 res = append(res, cc.NewErrReply(t, "Cannot rename folder "+string(fileName)+" because it does not exist or cannot be found."))
475 return res, err
476 }
477 case mode.IsRegular():
478 if !cc.Authorize(accessRenameFile) {
479 res = append(res, cc.NewErrReply(t, "You are not allowed to rename files."))
480 return res, err
481 }
482 fileDir, err := readPath(cc.Server.Config.FileRoot, filePath, []byte{})
483 if err != nil {
484 return nil, err
485 }
486 hlFile.name, err = txtDecoder.String(string(fileNewName))
487 if err != nil {
488 return res, fmt.Errorf("invalid filepath encoding: %w", err)
489 }
490
491 err = hlFile.move(fileDir)
492 if os.IsNotExist(err) {
493 res = append(res, cc.NewErrReply(t, "Cannot rename file "+string(fileName)+" because it does not exist or cannot be found."))
494 return res, err
495 }
496 if err != nil {
497 return res, err
498 }
499 }
500 }
501
502 res = append(res, cc.NewReply(t))
503 return res, err
504 }
505
506 // HandleDeleteFile deletes a file or folder
507 // Fields used in the request:
508 // * 201 File name
509 // * 202 File path
510 // Fields used in the reply: none
511 func HandleDeleteFile(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
512 fileName := t.GetField(FieldFileName).Data
513 filePath := t.GetField(FieldFilePath).Data
514
515 fullFilePath, err := readPath(cc.Server.Config.FileRoot, filePath, fileName)
516 if err != nil {
517 return res, err
518 }
519
520 hlFile, err := newFileWrapper(cc.Server.FS, fullFilePath, 0)
521 if err != nil {
522 return res, err
523 }
524
525 fi, err := hlFile.dataFile()
526 if err != nil {
527 res = append(res, cc.NewErrReply(t, "Cannot delete file "+string(fileName)+" because it does not exist or cannot be found."))
528 return res, nil
529 }
530
531 switch mode := fi.Mode(); {
532 case mode.IsDir():
533 if !cc.Authorize(accessDeleteFolder) {
534 res = append(res, cc.NewErrReply(t, "You are not allowed to delete folders."))
535 return res, err
536 }
537 case mode.IsRegular():
538 if !cc.Authorize(accessDeleteFile) {
539 res = append(res, cc.NewErrReply(t, "You are not allowed to delete files."))
540 return res, err
541 }
542 }
543
544 if err := hlFile.delete(); err != nil {
545 return res, err
546 }
547
548 res = append(res, cc.NewReply(t))
549 return res, err
550 }
551
552 // HandleMoveFile moves files or folders. Note: seemingly not documented
553 func HandleMoveFile(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
554 fileName := string(t.GetField(FieldFileName).Data)
555
556 filePath, err := readPath(cc.Server.Config.FileRoot, t.GetField(FieldFilePath).Data, t.GetField(FieldFileName).Data)
557 if err != nil {
558 return res, err
559 }
560
561 fileNewPath, err := readPath(cc.Server.Config.FileRoot, t.GetField(FieldFileNewPath).Data, nil)
562 if err != nil {
563 return res, err
564 }
565
566 cc.logger.Infow("Move file", "src", filePath+"/"+fileName, "dst", fileNewPath+"/"+fileName)
567
568 hlFile, err := newFileWrapper(cc.Server.FS, filePath, 0)
569 if err != nil {
570 return res, err
571 }
572
573 fi, err := hlFile.dataFile()
574 if err != nil {
575 res = append(res, cc.NewErrReply(t, "Cannot delete file "+fileName+" because it does not exist or cannot be found."))
576 return res, err
577 }
578 switch mode := fi.Mode(); {
579 case mode.IsDir():
580 if !cc.Authorize(accessMoveFolder) {
581 res = append(res, cc.NewErrReply(t, "You are not allowed to move folders."))
582 return res, err
583 }
584 case mode.IsRegular():
585 if !cc.Authorize(accessMoveFile) {
586 res = append(res, cc.NewErrReply(t, "You are not allowed to move files."))
587 return res, err
588 }
589 }
590 if err := hlFile.move(fileNewPath); err != nil {
591 return res, err
592 }
593 // TODO: handle other possible errors; e.g. fileWrapper delete fails due to fileWrapper permission issue
594
595 res = append(res, cc.NewReply(t))
596 return res, err
597 }
598
599 func HandleNewFolder(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
600 if !cc.Authorize(accessCreateFolder) {
601 res = append(res, cc.NewErrReply(t, "You are not allowed to create folders."))
602 return res, err
603 }
604 folderName := string(t.GetField(FieldFileName).Data)
605
606 folderName = path.Join("/", folderName)
607
608 var subPath string
609
610 // FieldFilePath is only present for nested paths
611 if t.GetField(FieldFilePath).Data != nil {
612 var newFp FilePath
613 _, err := newFp.Write(t.GetField(FieldFilePath).Data)
614 if err != nil {
615 return nil, err
616 }
617
618 for _, pathItem := range newFp.Items {
619 subPath = filepath.Join("/", subPath, string(pathItem.Name))
620 }
621 }
622 newFolderPath := path.Join(cc.Server.Config.FileRoot, subPath, folderName)
623 newFolderPath, err = txtDecoder.String(newFolderPath)
624 if err != nil {
625 return res, fmt.Errorf("invalid filepath encoding: %w", err)
626 }
627
628 // TODO: check path and folder name lengths
629
630 if _, err := cc.Server.FS.Stat(newFolderPath); !os.IsNotExist(err) {
631 msg := fmt.Sprintf("Cannot create folder \"%s\" because there is already a file or folder with that name.", folderName)
632 return []Transaction{cc.NewErrReply(t, msg)}, nil
633 }
634
635 if err := cc.Server.FS.Mkdir(newFolderPath, 0777); err != nil {
636 msg := fmt.Sprintf("Cannot create folder \"%s\" because an error occurred.", folderName)
637 return []Transaction{cc.NewErrReply(t, msg)}, nil
638 }
639
640 res = append(res, cc.NewReply(t))
641 return res, err
642 }
643
644 func HandleSetUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
645 if !cc.Authorize(accessModifyUser) {
646 res = append(res, cc.NewErrReply(t, "You are not allowed to modify accounts."))
647 return res, err
648 }
649
650 login := decodeString(t.GetField(FieldUserLogin).Data)
651 userName := string(t.GetField(FieldUserName).Data)
652
653 newAccessLvl := t.GetField(FieldUserAccess).Data
654
655 account := cc.Server.Accounts[login]
656 account.Name = userName
657 copy(account.Access[:], newAccessLvl)
658
659 // If the password field is cleared in the Hotline edit user UI, the SetUser transaction does
660 // not include FieldUserPassword
661 if t.GetField(FieldUserPassword).Data == nil {
662 account.Password = hashAndSalt([]byte(""))
663 }
664 if len(t.GetField(FieldUserPassword).Data) > 1 {
665 account.Password = hashAndSalt(t.GetField(FieldUserPassword).Data)
666 }
667
668 out, err := yaml.Marshal(&account)
669 if err != nil {
670 return res, err
671 }
672 if err := os.WriteFile(filepath.Join(cc.Server.ConfigDir, "Users", login+".yaml"), out, 0666); err != nil {
673 return res, err
674 }
675
676 // Notify connected clients logged in as the user of the new access level
677 for _, c := range cc.Server.Clients {
678 if c.Account.Login == login {
679 // Note: comment out these two lines to test server-side deny messages
680 newT := NewTransaction(TranUserAccess, c.ID, NewField(FieldUserAccess, newAccessLvl))
681 res = append(res, *newT)
682
683 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(c.Flags)))
684 if c.Authorize(accessDisconUser) {
685 flagBitmap.SetBit(flagBitmap, UserFlagAdmin, 1)
686 } else {
687 flagBitmap.SetBit(flagBitmap, UserFlagAdmin, 0)
688 }
689 binary.BigEndian.PutUint16(c.Flags, uint16(flagBitmap.Int64()))
690
691 c.Account.Access = account.Access
692
693 cc.sendAll(
694 TranNotifyChangeUser,
695 NewField(FieldUserID, *c.ID),
696 NewField(FieldUserFlags, c.Flags),
697 NewField(FieldUserName, c.UserName),
698 NewField(FieldUserIconID, c.Icon),
699 )
700 }
701 }
702
703 res = append(res, cc.NewReply(t))
704 return res, err
705 }
706
707 func HandleGetUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
708 if !cc.Authorize(accessOpenUser) {
709 res = append(res, cc.NewErrReply(t, "You are not allowed to view accounts."))
710 return res, err
711 }
712
713 account := cc.Server.Accounts[string(t.GetField(FieldUserLogin).Data)]
714 if account == nil {
715 res = append(res, cc.NewErrReply(t, "Account does not exist."))
716 return res, err
717 }
718
719 res = append(res, cc.NewReply(t,
720 NewField(FieldUserName, []byte(account.Name)),
721 NewField(FieldUserLogin, encodeString(t.GetField(FieldUserLogin).Data)),
722 NewField(FieldUserPassword, []byte(account.Password)),
723 NewField(FieldUserAccess, account.Access[:]),
724 ))
725 return res, err
726 }
727
728 func HandleListUsers(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
729 if !cc.Authorize(accessOpenUser) {
730 res = append(res, cc.NewErrReply(t, "You are not allowed to view accounts."))
731 return res, err
732 }
733
734 var userFields []Field
735 for _, acc := range cc.Server.Accounts {
736 b := make([]byte, 0, 100)
737 n, err := acc.Read(b)
738 if err != nil {
739 return res, err
740 }
741
742 userFields = append(userFields, NewField(FieldData, b[:n]))
743 }
744
745 res = append(res, cc.NewReply(t, userFields...))
746 return res, err
747 }
748
749 // HandleUpdateUser is used by the v1.5+ multi-user editor to perform account editing for multiple users at a time.
750 // An update can be a mix of these actions:
751 // * Create user
752 // * Delete user
753 // * Modify user (including renaming the account login)
754 //
755 // The Transaction sent by the client includes one data field per user that was modified. This data field in turn
756 // contains another data field encoded in its payload with a varying number of sub fields depending on which action is
757 // performed. This seems to be the only place in the Hotline protocol where a data field contains another data field.
758 func HandleUpdateUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
759 for _, field := range t.Fields {
760 subFields, err := ReadFields(field.Data[0:2], field.Data[2:])
761 if err != nil {
762 return res, err
763 }
764
765 if len(subFields) == 1 {
766 login := decodeString(getField(FieldData, &subFields).Data)
767 cc.logger.Infow("DeleteUser", "login", login)
768
769 if !cc.Authorize(accessDeleteUser) {
770 res = append(res, cc.NewErrReply(t, "You are not allowed to delete accounts."))
771 return res, err
772 }
773
774 if err := cc.Server.DeleteUser(login); err != nil {
775 return res, err
776 }
777 continue
778 }
779
780 login := decodeString(getField(FieldUserLogin, &subFields).Data)
781
782 // check if the login dataFile; if so, we know we are updating an existing user
783 if acc, ok := cc.Server.Accounts[login]; ok {
784 cc.logger.Infow("UpdateUser", "login", login)
785
786 // account dataFile, so this is an update action
787 if !cc.Authorize(accessModifyUser) {
788 res = append(res, cc.NewErrReply(t, "You are not allowed to modify accounts."))
789 return res, err
790 }
791
792 if getField(FieldUserPassword, &subFields) != nil {
793 newPass := getField(FieldUserPassword, &subFields).Data
794 acc.Password = hashAndSalt(newPass)
795 } else {
796 acc.Password = hashAndSalt([]byte(""))
797 }
798
799 if getField(FieldUserAccess, &subFields) != nil {
800 copy(acc.Access[:], getField(FieldUserAccess, &subFields).Data)
801 }
802
803 err = cc.Server.UpdateUser(
804 decodeString(getField(FieldData, &subFields).Data),
805 decodeString(getField(FieldUserLogin, &subFields).Data),
806 string(getField(FieldUserName, &subFields).Data),
807 acc.Password,
808 acc.Access,
809 )
810 if err != nil {
811 return res, err
812 }
813 } else {
814 cc.logger.Infow("CreateUser", "login", login)
815
816 if !cc.Authorize(accessCreateUser) {
817 res = append(res, cc.NewErrReply(t, "You are not allowed to create new accounts."))
818 return res, err
819 }
820
821 newAccess := accessBitmap{}
822 copy(newAccess[:], getField(FieldUserAccess, &subFields).Data)
823
824 // Prevent account from creating new account with greater permission
825 for i := 0; i < 64; i++ {
826 if newAccess.IsSet(i) {
827 if !cc.Authorize(i) {
828 return append(res, cc.NewErrReply(t, "Cannot create account with more access than yourself.")), err
829 }
830 }
831 }
832
833 err := cc.Server.NewUser(login, string(getField(FieldUserName, &subFields).Data), string(getField(FieldUserPassword, &subFields).Data), newAccess)
834 if err != nil {
835 return []Transaction{}, err
836 }
837 }
838 }
839
840 res = append(res, cc.NewReply(t))
841 return res, err
842 }
843
844 // HandleNewUser creates a new user account
845 func HandleNewUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
846 if !cc.Authorize(accessCreateUser) {
847 res = append(res, cc.NewErrReply(t, "You are not allowed to create new accounts."))
848 return res, err
849 }
850
851 login := decodeString(t.GetField(FieldUserLogin).Data)
852
853 // If the account already dataFile, reply with an error
854 if _, ok := cc.Server.Accounts[login]; ok {
855 res = append(res, cc.NewErrReply(t, "Cannot create account "+login+" because there is already an account with that login."))
856 return res, err
857 }
858
859 newAccess := accessBitmap{}
860 copy(newAccess[:], t.GetField(FieldUserAccess).Data)
861
862 // Prevent account from creating new account with greater permission
863 for i := 0; i < 64; i++ {
864 if newAccess.IsSet(i) {
865 if !cc.Authorize(i) {
866 res = append(res, cc.NewErrReply(t, "Cannot create account with more access than yourself."))
867 return res, err
868 }
869 }
870 }
871
872 if err := cc.Server.NewUser(login, string(t.GetField(FieldUserName).Data), string(t.GetField(FieldUserPassword).Data), newAccess); err != nil {
873 return []Transaction{}, err
874 }
875
876 res = append(res, cc.NewReply(t))
877 return res, err
878 }
879
880 func HandleDeleteUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
881 if !cc.Authorize(accessDeleteUser) {
882 res = append(res, cc.NewErrReply(t, "You are not allowed to delete accounts."))
883 return res, err
884 }
885
886 // TODO: Handle case where account doesn't exist; e.g. delete race condition
887 login := decodeString(t.GetField(FieldUserLogin).Data)
888
889 if err := cc.Server.DeleteUser(login); err != nil {
890 return res, err
891 }
892
893 res = append(res, cc.NewReply(t))
894 return res, err
895 }
896
897 // HandleUserBroadcast sends an Administrator Message to all connected clients of the server
898 func HandleUserBroadcast(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
899 if !cc.Authorize(accessBroadcast) {
900 res = append(res, cc.NewErrReply(t, "You are not allowed to send broadcast messages."))
901 return res, err
902 }
903
904 cc.sendAll(
905 TranServerMsg,
906 NewField(FieldData, t.GetField(TranGetMsgs).Data),
907 NewField(FieldChatOptions, []byte{0}),
908 )
909
910 res = append(res, cc.NewReply(t))
911 return res, err
912 }
913
914 // HandleGetClientInfoText returns user information for the specific user.
915 //
916 // Fields used in the request:
917 // 103 User ID
918 //
919 // Fields used in the reply:
920 // 102 User name
921 // 101 Data User info text string
922 func HandleGetClientInfoText(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
923 if !cc.Authorize(accessGetClientInfo) {
924 res = append(res, cc.NewErrReply(t, "You are not allowed to get client info."))
925 return res, err
926 }
927
928 clientID, _ := byteToInt(t.GetField(FieldUserID).Data)
929
930 clientConn := cc.Server.Clients[uint16(clientID)]
931 if clientConn == nil {
932 return append(res, cc.NewErrReply(t, "User not found.")), err
933 }
934
935 res = append(res, cc.NewReply(t,
936 NewField(FieldData, []byte(clientConn.String())),
937 NewField(FieldUserName, clientConn.UserName),
938 ))
939 return res, err
940 }
941
942 func HandleGetUserNameList(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
943 res = append(res, cc.NewReply(t, cc.Server.connectedUsers()...))
944
945 return res, err
946 }
947
948 func HandleTranAgreed(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
949 if t.GetField(FieldUserName).Data != nil {
950 if cc.Authorize(accessAnyName) {
951 cc.UserName = t.GetField(FieldUserName).Data
952 } else {
953 cc.UserName = []byte(cc.Account.Name)
954 }
955 }
956
957 cc.Icon = t.GetField(FieldUserIconID).Data
958
959 cc.logger = cc.logger.With("name", string(cc.UserName))
960 cc.logger.Infow("Login successful", "clientVersion", fmt.Sprintf("%v", func() int { i, _ := byteToInt(cc.Version); return i }()))
961
962 options := t.GetField(FieldOptions).Data
963 optBitmap := big.NewInt(int64(binary.BigEndian.Uint16(options)))
964
965 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(cc.Flags)))
966
967 // Check refuse private PM option
968 if optBitmap.Bit(refusePM) == 1 {
969 flagBitmap.SetBit(flagBitmap, UserFlagRefusePM, 1)
970 binary.BigEndian.PutUint16(cc.Flags, uint16(flagBitmap.Int64()))
971 }
972
973 // Check refuse private chat option
974 if optBitmap.Bit(refuseChat) == 1 {
975 flagBitmap.SetBit(flagBitmap, UserFlagRefusePChat, 1)
976 binary.BigEndian.PutUint16(cc.Flags, uint16(flagBitmap.Int64()))
977 }
978
979 // Check auto response
980 if optBitmap.Bit(autoResponse) == 1 {
981 cc.AutoReply = t.GetField(FieldAutomaticResponse).Data
982 } else {
983 cc.AutoReply = []byte{}
984 }
985
986 trans := cc.notifyOthers(
987 *NewTransaction(
988 TranNotifyChangeUser, nil,
989 NewField(FieldUserName, cc.UserName),
990 NewField(FieldUserID, *cc.ID),
991 NewField(FieldUserIconID, cc.Icon),
992 NewField(FieldUserFlags, cc.Flags),
993 ),
994 )
995 res = append(res, trans...)
996
997 if cc.Server.Config.BannerFile != "" {
998 res = append(res, *NewTransaction(TranServerBanner, cc.ID, NewField(FieldBannerType, []byte("JPEG"))))
999 }
1000
1001 res = append(res, cc.NewReply(t))
1002
1003 return res, err
1004 }
1005
1006 // HandleTranOldPostNews updates the flat news
1007 // Fields used in this request:
1008 // 101 Data
1009 func HandleTranOldPostNews(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1010 if !cc.Authorize(accessNewsPostArt) {
1011 res = append(res, cc.NewErrReply(t, "You are not allowed to post news."))
1012 return res, err
1013 }
1014
1015 cc.Server.flatNewsMux.Lock()
1016 defer cc.Server.flatNewsMux.Unlock()
1017
1018 newsDateTemplate := defaultNewsDateFormat
1019 if cc.Server.Config.NewsDateFormat != "" {
1020 newsDateTemplate = cc.Server.Config.NewsDateFormat
1021 }
1022
1023 newsTemplate := defaultNewsTemplate
1024 if cc.Server.Config.NewsDelimiter != "" {
1025 newsTemplate = cc.Server.Config.NewsDelimiter
1026 }
1027
1028 newsPost := fmt.Sprintf(newsTemplate+"\r", cc.UserName, time.Now().Format(newsDateTemplate), t.GetField(FieldData).Data)
1029 newsPost = strings.ReplaceAll(newsPost, "\n", "\r")
1030
1031 // update news in memory
1032 cc.Server.FlatNews = append([]byte(newsPost), cc.Server.FlatNews...)
1033
1034 // update news on disk
1035 if err := cc.Server.FS.WriteFile(filepath.Join(cc.Server.ConfigDir, "MessageBoard.txt"), cc.Server.FlatNews, 0644); err != nil {
1036 return res, err
1037 }
1038
1039 // Notify all clients of updated news
1040 cc.sendAll(
1041 TranNewMsg,
1042 NewField(FieldData, []byte(newsPost)),
1043 )
1044
1045 res = append(res, cc.NewReply(t))
1046 return res, err
1047 }
1048
1049 func HandleDisconnectUser(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1050 if !cc.Authorize(accessDisconUser) {
1051 res = append(res, cc.NewErrReply(t, "You are not allowed to disconnect users."))
1052 return res, err
1053 }
1054
1055 clientConn := cc.Server.Clients[binary.BigEndian.Uint16(t.GetField(FieldUserID).Data)]
1056
1057 if clientConn.Authorize(accessCannotBeDiscon) {
1058 res = append(res, cc.NewErrReply(t, clientConn.Account.Login+" is not allowed to be disconnected."))
1059 return res, err
1060 }
1061
1062 // If FieldOptions is set, then the client IP is banned in addition to disconnected.
1063 // 00 01 = temporary ban
1064 // 00 02 = permanent ban
1065 if t.GetField(FieldOptions).Data != nil {
1066 switch t.GetField(FieldOptions).Data[1] {
1067 case 1:
1068 // send message: "You are temporarily banned on this server"
1069 cc.logger.Infow("Disconnect & temporarily ban " + string(clientConn.UserName))
1070
1071 res = append(res, *NewTransaction(
1072 TranServerMsg,
1073 clientConn.ID,
1074 NewField(FieldData, []byte("You are temporarily banned on this server")),
1075 NewField(FieldChatOptions, []byte{0, 0}),
1076 ))
1077
1078 banUntil := time.Now().Add(tempBanDuration)
1079 cc.Server.banList[strings.Split(clientConn.RemoteAddr, ":")[0]] = &banUntil
1080 case 2:
1081 // send message: "You are permanently banned on this server"
1082 cc.logger.Infow("Disconnect & ban " + string(clientConn.UserName))
1083
1084 res = append(res, *NewTransaction(
1085 TranServerMsg,
1086 clientConn.ID,
1087 NewField(FieldData, []byte("You are permanently banned on this server")),
1088 NewField(FieldChatOptions, []byte{0, 0}),
1089 ))
1090
1091 cc.Server.banList[strings.Split(clientConn.RemoteAddr, ":")[0]] = nil
1092 }
1093
1094 err := cc.Server.writeBanList()
1095 if err != nil {
1096 return res, err
1097 }
1098 }
1099
1100 // TODO: remove this awful hack
1101 go func() {
1102 time.Sleep(1 * time.Second)
1103 clientConn.Disconnect()
1104 }()
1105
1106 return append(res, cc.NewReply(t)), err
1107 }
1108
1109 // HandleGetNewsCatNameList returns a list of news categories for a path
1110 // Fields used in the request:
1111 // 325 News path (Optional)
1112 func HandleGetNewsCatNameList(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1113 if !cc.Authorize(accessNewsReadArt) {
1114 res = append(res, cc.NewErrReply(t, "You are not allowed to read news."))
1115 return res, err
1116 }
1117
1118 pathStrs := ReadNewsPath(t.GetField(FieldNewsPath).Data)
1119 cats := cc.Server.GetNewsCatByPath(pathStrs)
1120
1121 // To store the keys in slice in sorted order
1122 keys := make([]string, len(cats))
1123 i := 0
1124 for k := range cats {
1125 keys[i] = k
1126 i++
1127 }
1128 sort.Strings(keys)
1129
1130 var fieldData []Field
1131 for _, k := range keys {
1132 cat := cats[k]
1133 b, _ := cat.MarshalBinary()
1134 fieldData = append(fieldData, NewField(
1135 FieldNewsCatListData15,
1136 b,
1137 ))
1138 }
1139
1140 res = append(res, cc.NewReply(t, fieldData...))
1141 return res, err
1142 }
1143
1144 func HandleNewNewsCat(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1145 if !cc.Authorize(accessNewsCreateCat) {
1146 res = append(res, cc.NewErrReply(t, "You are not allowed to create news categories."))
1147 return res, err
1148 }
1149
1150 name := string(t.GetField(FieldNewsCatName).Data)
1151 pathStrs := ReadNewsPath(t.GetField(FieldNewsPath).Data)
1152
1153 cats := cc.Server.GetNewsCatByPath(pathStrs)
1154 cats[name] = NewsCategoryListData15{
1155 Name: name,
1156 Type: []byte{0, 3},
1157 Articles: map[uint32]*NewsArtData{},
1158 SubCats: make(map[string]NewsCategoryListData15),
1159 }
1160
1161 if err := cc.Server.writeThreadedNews(); err != nil {
1162 return res, err
1163 }
1164 res = append(res, cc.NewReply(t))
1165 return res, err
1166 }
1167
1168 // Fields used in the request:
1169 // 322 News category name
1170 // 325 News path
1171 func HandleNewNewsFldr(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1172 if !cc.Authorize(accessNewsCreateFldr) {
1173 res = append(res, cc.NewErrReply(t, "You are not allowed to create news folders."))
1174 return res, err
1175 }
1176
1177 name := string(t.GetField(FieldFileName).Data)
1178 pathStrs := ReadNewsPath(t.GetField(FieldNewsPath).Data)
1179
1180 cc.logger.Infof("Creating new news folder %s", name)
1181
1182 cats := cc.Server.GetNewsCatByPath(pathStrs)
1183 cats[name] = NewsCategoryListData15{
1184 Name: name,
1185 Type: []byte{0, 2},
1186 Articles: map[uint32]*NewsArtData{},
1187 SubCats: make(map[string]NewsCategoryListData15),
1188 }
1189 if err := cc.Server.writeThreadedNews(); err != nil {
1190 return res, err
1191 }
1192 res = append(res, cc.NewReply(t))
1193 return res, err
1194 }
1195
1196 // HandleGetNewsArtData gets the list of article names at the specified news path.
1197
1198 // Fields used in the request:
1199 // 325 News path Optional
1200
1201 // Fields used in the reply:
1202 // 321 News article list data Optional
1203 func HandleGetNewsArtNameList(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1204 if !cc.Authorize(accessNewsReadArt) {
1205 res = append(res, cc.NewErrReply(t, "You are not allowed to read news."))
1206 return res, err
1207 }
1208 pathStrs := ReadNewsPath(t.GetField(FieldNewsPath).Data)
1209
1210 var cat NewsCategoryListData15
1211 cats := cc.Server.ThreadedNews.Categories
1212
1213 for _, fp := range pathStrs {
1214 cat = cats[fp]
1215 cats = cats[fp].SubCats
1216 }
1217
1218 nald := cat.GetNewsArtListData()
1219
1220 res = append(res, cc.NewReply(t, NewField(FieldNewsArtListData, nald.Payload())))
1221 return res, err
1222 }
1223
1224 // HandleGetNewsArtData requests information about the specific news article.
1225 // Fields used in the request:
1226 //
1227 // Request fields
1228 // 325 News path
1229 // 326 News article ID
1230 // 327 News article data flavor
1231 //
1232 // Fields used in the reply:
1233 // 328 News article title
1234 // 329 News article poster
1235 // 330 News article date
1236 // 331 Previous article ID
1237 // 332 Next article ID
1238 // 335 Parent article ID
1239 // 336 First child article ID
1240 // 327 News article data flavor "Should be “text/plain”
1241 // 333 News article data Optional (if data flavor is “text/plain”)
1242 func HandleGetNewsArtData(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1243 if !cc.Authorize(accessNewsReadArt) {
1244 res = append(res, cc.NewErrReply(t, "You are not allowed to read news."))
1245 return res, err
1246 }
1247
1248 var cat NewsCategoryListData15
1249 cats := cc.Server.ThreadedNews.Categories
1250
1251 for _, fp := range ReadNewsPath(t.GetField(FieldNewsPath).Data) {
1252 cat = cats[fp]
1253 cats = cats[fp].SubCats
1254 }
1255
1256 // The official Hotline clients will send the article ID as 2 bytes if possible, but
1257 // some third party clients such as Frogblast and Heildrun will always send 4 bytes
1258 convertedID, err := byteToInt(t.GetField(FieldNewsArtID).Data)
1259 if err != nil {
1260 return res, err
1261 }
1262
1263 art := cat.Articles[uint32(convertedID)]
1264 if art == nil {
1265 res = append(res, cc.NewReply(t))
1266 return res, err
1267 }
1268
1269 res = append(res, cc.NewReply(t,
1270 NewField(FieldNewsArtTitle, []byte(art.Title)),
1271 NewField(FieldNewsArtPoster, []byte(art.Poster)),
1272 NewField(FieldNewsArtDate, art.Date),
1273 NewField(FieldNewsArtPrevArt, art.PrevArt),
1274 NewField(FieldNewsArtNextArt, art.NextArt),
1275 NewField(FieldNewsArtParentArt, art.ParentArt),
1276 NewField(FieldNewsArt1stChildArt, art.FirstChildArt),
1277 NewField(FieldNewsArtDataFlav, []byte("text/plain")),
1278 NewField(FieldNewsArtData, []byte(art.Data)),
1279 ))
1280 return res, err
1281 }
1282
1283 // HandleDelNewsItem deletes an existing threaded news folder or category from the server.
1284 // Fields used in the request:
1285 // 325 News path
1286 // Fields used in the reply:
1287 // None
1288 func HandleDelNewsItem(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1289 pathStrs := ReadNewsPath(t.GetField(FieldNewsPath).Data)
1290
1291 cats := cc.Server.ThreadedNews.Categories
1292 delName := pathStrs[len(pathStrs)-1]
1293 if len(pathStrs) > 1 {
1294 for _, fp := range pathStrs[0 : len(pathStrs)-1] {
1295 cats = cats[fp].SubCats
1296 }
1297 }
1298
1299 if bytes.Equal(cats[delName].Type, []byte{0, 3}) {
1300 if !cc.Authorize(accessNewsDeleteCat) {
1301 return append(res, cc.NewErrReply(t, "You are not allowed to delete news categories.")), nil
1302 }
1303 } else {
1304 if !cc.Authorize(accessNewsDeleteFldr) {
1305 return append(res, cc.NewErrReply(t, "You are not allowed to delete news folders.")), nil
1306 }
1307 }
1308
1309 delete(cats, delName)
1310
1311 if err := cc.Server.writeThreadedNews(); err != nil {
1312 return res, err
1313 }
1314
1315 return append(res, cc.NewReply(t)), nil
1316 }
1317
1318 func HandleDelNewsArt(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1319 if !cc.Authorize(accessNewsDeleteArt) {
1320 res = append(res, cc.NewErrReply(t, "You are not allowed to delete news articles."))
1321 return res, err
1322 }
1323
1324 // Request Fields
1325 // 325 News path
1326 // 326 News article ID
1327 // 337 News article – recursive delete Delete child articles (1) or not (0)
1328 pathStrs := ReadNewsPath(t.GetField(FieldNewsPath).Data)
1329 ID, err := byteToInt(t.GetField(FieldNewsArtID).Data)
1330 if err != nil {
1331 return res, err
1332 }
1333
1334 // TODO: Delete recursive
1335 cats := cc.Server.GetNewsCatByPath(pathStrs[:len(pathStrs)-1])
1336
1337 catName := pathStrs[len(pathStrs)-1]
1338 cat := cats[catName]
1339
1340 delete(cat.Articles, uint32(ID))
1341
1342 cats[catName] = cat
1343 if err := cc.Server.writeThreadedNews(); err != nil {
1344 return res, err
1345 }
1346
1347 res = append(res, cc.NewReply(t))
1348 return res, err
1349 }
1350
1351 // Request fields
1352 // 325 News path
1353 // 326 News article ID ID of the parent article?
1354 // 328 News article title
1355 // 334 News article flags
1356 // 327 News article data flavor Currently “text/plain”
1357 // 333 News article data
1358 func HandlePostNewsArt(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1359 if !cc.Authorize(accessNewsPostArt) {
1360 res = append(res, cc.NewErrReply(t, "You are not allowed to post news articles."))
1361 return res, err
1362 }
1363
1364 pathStrs := ReadNewsPath(t.GetField(FieldNewsPath).Data)
1365 cats := cc.Server.GetNewsCatByPath(pathStrs[:len(pathStrs)-1])
1366
1367 catName := pathStrs[len(pathStrs)-1]
1368 cat := cats[catName]
1369
1370 artID, err := byteToInt(t.GetField(FieldNewsArtID).Data)
1371 if err != nil {
1372 return res, err
1373 }
1374 convertedArtID := uint32(artID)
1375 bs := make([]byte, 4)
1376 binary.BigEndian.PutUint32(bs, convertedArtID)
1377
1378 newArt := NewsArtData{
1379 Title: string(t.GetField(FieldNewsArtTitle).Data),
1380 Poster: string(cc.UserName),
1381 Date: toHotlineTime(time.Now()),
1382 PrevArt: []byte{0, 0, 0, 0},
1383 NextArt: []byte{0, 0, 0, 0},
1384 ParentArt: bs,
1385 FirstChildArt: []byte{0, 0, 0, 0},
1386 DataFlav: []byte("text/plain"),
1387 Data: string(t.GetField(FieldNewsArtData).Data),
1388 }
1389
1390 var keys []int
1391 for k := range cat.Articles {
1392 keys = append(keys, int(k))
1393 }
1394
1395 nextID := uint32(1)
1396 if len(keys) > 0 {
1397 sort.Ints(keys)
1398 prevID := uint32(keys[len(keys)-1])
1399 nextID = prevID + 1
1400
1401 binary.BigEndian.PutUint32(newArt.PrevArt, prevID)
1402
1403 // Set next article ID
1404 binary.BigEndian.PutUint32(cat.Articles[prevID].NextArt, nextID)
1405 }
1406
1407 // Update parent article with first child reply
1408 parentID := convertedArtID
1409 if parentID != 0 {
1410 parentArt := cat.Articles[parentID]
1411
1412 if bytes.Equal(parentArt.FirstChildArt, []byte{0, 0, 0, 0}) {
1413 binary.BigEndian.PutUint32(parentArt.FirstChildArt, nextID)
1414 }
1415 }
1416
1417 cat.Articles[nextID] = &newArt
1418
1419 cats[catName] = cat
1420 if err := cc.Server.writeThreadedNews(); err != nil {
1421 return res, err
1422 }
1423
1424 res = append(res, cc.NewReply(t))
1425 return res, err
1426 }
1427
1428 // HandleGetMsgs returns the flat news data
1429 func HandleGetMsgs(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1430 if !cc.Authorize(accessNewsReadArt) {
1431 res = append(res, cc.NewErrReply(t, "You are not allowed to read news."))
1432 return res, err
1433 }
1434
1435 res = append(res, cc.NewReply(t, NewField(FieldData, cc.Server.FlatNews)))
1436
1437 return res, err
1438 }
1439
1440 func HandleDownloadFile(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1441 if !cc.Authorize(accessDownloadFile) {
1442 res = append(res, cc.NewErrReply(t, "You are not allowed to download files."))
1443 return res, err
1444 }
1445
1446 fileName := t.GetField(FieldFileName).Data
1447 filePath := t.GetField(FieldFilePath).Data
1448 resumeData := t.GetField(FieldFileResumeData).Data
1449
1450 var dataOffset int64
1451 var frd FileResumeData
1452 if resumeData != nil {
1453 if err := frd.UnmarshalBinary(t.GetField(FieldFileResumeData).Data); err != nil {
1454 return res, err
1455 }
1456 // TODO: handle rsrc fork offset
1457 dataOffset = int64(binary.BigEndian.Uint32(frd.ForkInfoList[0].DataSize[:]))
1458 }
1459
1460 fullFilePath, err := readPath(cc.Server.Config.FileRoot, filePath, fileName)
1461 if err != nil {
1462 return res, err
1463 }
1464
1465 hlFile, err := newFileWrapper(cc.Server.FS, fullFilePath, dataOffset)
1466 if err != nil {
1467 return res, err
1468 }
1469
1470 xferSize := hlFile.ffo.TransferSize(0)
1471
1472 ft := cc.newFileTransfer(FileDownload, fileName, filePath, xferSize)
1473
1474 // TODO: refactor to remove this
1475 if resumeData != nil {
1476 var frd FileResumeData
1477 if err := frd.UnmarshalBinary(t.GetField(FieldFileResumeData).Data); err != nil {
1478 return res, err
1479 }
1480 ft.fileResumeData = &frd
1481 }
1482
1483 // Optional field for when a HL v1.5+ client requests file preview
1484 // Used only for TEXT, JPEG, GIFF, BMP or PICT files
1485 // The value will always be 2
1486 if t.GetField(FieldFileTransferOptions).Data != nil {
1487 ft.options = t.GetField(FieldFileTransferOptions).Data
1488 xferSize = hlFile.ffo.FlatFileDataForkHeader.DataSize[:]
1489 }
1490
1491 res = append(res, cc.NewReply(t,
1492 NewField(FieldRefNum, ft.refNum[:]),
1493 NewField(FieldWaitingCount, []byte{0x00, 0x00}), // TODO: Implement waiting count
1494 NewField(FieldTransferSize, xferSize),
1495 NewField(FieldFileSize, hlFile.ffo.FlatFileDataForkHeader.DataSize[:]),
1496 ))
1497
1498 return res, err
1499 }
1500
1501 // Download all files from the specified folder and sub-folders
1502 func HandleDownloadFolder(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1503 if !cc.Authorize(accessDownloadFile) {
1504 res = append(res, cc.NewErrReply(t, "You are not allowed to download folders."))
1505 return res, err
1506 }
1507
1508 fullFilePath, err := readPath(cc.Server.Config.FileRoot, t.GetField(FieldFilePath).Data, t.GetField(FieldFileName).Data)
1509 if err != nil {
1510 return res, err
1511 }
1512
1513 transferSize, err := CalcTotalSize(fullFilePath)
1514 if err != nil {
1515 return res, err
1516 }
1517 itemCount, err := CalcItemCount(fullFilePath)
1518 if err != nil {
1519 return res, err
1520 }
1521
1522 fileTransfer := cc.newFileTransfer(FolderDownload, t.GetField(FieldFileName).Data, t.GetField(FieldFilePath).Data, transferSize)
1523
1524 var fp FilePath
1525 _, err = fp.Write(t.GetField(FieldFilePath).Data)
1526 if err != nil {
1527 return res, err
1528 }
1529
1530 res = append(res, cc.NewReply(t,
1531 NewField(FieldRefNum, fileTransfer.ReferenceNumber),
1532 NewField(FieldTransferSize, transferSize),
1533 NewField(FieldFolderItemCount, itemCount),
1534 NewField(FieldWaitingCount, []byte{0x00, 0x00}), // TODO: Implement waiting count
1535 ))
1536 return res, err
1537 }
1538
1539 // Upload all files from the local folder and its subfolders to the specified path on the server
1540 // Fields used in the request
1541 // 201 File name
1542 // 202 File path
1543 // 108 transfer size Total size of all items in the folder
1544 // 220 Folder item count
1545 // 204 File transfer options "Optional Currently set to 1" (TODO: ??)
1546 func HandleUploadFolder(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1547 var fp FilePath
1548 if t.GetField(FieldFilePath).Data != nil {
1549 if _, err = fp.Write(t.GetField(FieldFilePath).Data); err != nil {
1550 return res, err
1551 }
1552 }
1553
1554 // Handle special cases for Upload and Drop Box folders
1555 if !cc.Authorize(accessUploadAnywhere) {
1556 if !fp.IsUploadDir() && !fp.IsDropbox() {
1557 res = append(res, cc.NewErrReply(t, fmt.Sprintf("Cannot accept upload of the folder \"%v\" because you are only allowed to upload to the \"Uploads\" folder.", string(t.GetField(FieldFileName).Data))))
1558 return res, err
1559 }
1560 }
1561
1562 fileTransfer := cc.newFileTransfer(FolderUpload,
1563 t.GetField(FieldFileName).Data,
1564 t.GetField(FieldFilePath).Data,
1565 t.GetField(FieldTransferSize).Data,
1566 )
1567
1568 fileTransfer.FolderItemCount = t.GetField(FieldFolderItemCount).Data
1569
1570 res = append(res, cc.NewReply(t, NewField(FieldRefNum, fileTransfer.ReferenceNumber)))
1571 return res, err
1572 }
1573
1574 // HandleUploadFile
1575 // Fields used in the request:
1576 // 201 File name
1577 // 202 File path
1578 // 204 File transfer options "Optional
1579 // Used only to resume download, currently has value 2"
1580 // 108 File transfer size "Optional used if download is not resumed"
1581 func HandleUploadFile(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1582 if !cc.Authorize(accessUploadFile) {
1583 res = append(res, cc.NewErrReply(t, "You are not allowed to upload files."))
1584 return res, err
1585 }
1586
1587 fileName := t.GetField(FieldFileName).Data
1588 filePath := t.GetField(FieldFilePath).Data
1589 transferOptions := t.GetField(FieldFileTransferOptions).Data
1590 transferSize := t.GetField(FieldTransferSize).Data // not sent for resume
1591
1592 var fp FilePath
1593 if filePath != nil {
1594 if _, err = fp.Write(filePath); err != nil {
1595 return res, err
1596 }
1597 }
1598
1599 // Handle special cases for Upload and Drop Box folders
1600 if !cc.Authorize(accessUploadAnywhere) {
1601 if !fp.IsUploadDir() && !fp.IsDropbox() {
1602 res = append(res, cc.NewErrReply(t, fmt.Sprintf("Cannot accept upload of the file \"%v\" because you are only allowed to upload to the \"Uploads\" folder.", string(fileName))))
1603 return res, err
1604 }
1605 }
1606 fullFilePath, err := readPath(cc.Server.Config.FileRoot, filePath, fileName)
1607 if err != nil {
1608 return res, err
1609 }
1610
1611 if _, err := cc.Server.FS.Stat(fullFilePath); err == nil {
1612 res = append(res, cc.NewErrReply(t, fmt.Sprintf("Cannot accept upload because there is already a file named \"%v\". Try choosing a different name.", string(fileName))))
1613 return res, err
1614 }
1615
1616 ft := cc.newFileTransfer(FileUpload, fileName, filePath, transferSize)
1617
1618 replyT := cc.NewReply(t, NewField(FieldRefNum, ft.ReferenceNumber))
1619
1620 // client has requested to resume a partially transferred file
1621 if transferOptions != nil {
1622 fileInfo, err := cc.Server.FS.Stat(fullFilePath + incompleteFileSuffix)
1623 if err != nil {
1624 return res, err
1625 }
1626
1627 offset := make([]byte, 4)
1628 binary.BigEndian.PutUint32(offset, uint32(fileInfo.Size()))
1629
1630 fileResumeData := NewFileResumeData([]ForkInfoList{
1631 *NewForkInfoList(offset),
1632 })
1633
1634 b, _ := fileResumeData.BinaryMarshal()
1635
1636 ft.TransferSize = offset
1637
1638 replyT.Fields = append(replyT.Fields, NewField(FieldFileResumeData, b))
1639 }
1640
1641 res = append(res, replyT)
1642 return res, err
1643 }
1644
1645 func HandleSetClientUserInfo(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1646 if len(t.GetField(FieldUserIconID).Data) == 4 {
1647 cc.Icon = t.GetField(FieldUserIconID).Data[2:]
1648 } else {
1649 cc.Icon = t.GetField(FieldUserIconID).Data
1650 }
1651 if cc.Authorize(accessAnyName) {
1652 cc.UserName = t.GetField(FieldUserName).Data
1653 }
1654
1655 // the options field is only passed by the client versions > 1.2.3.
1656 options := t.GetField(FieldOptions).Data
1657 if options != nil {
1658 optBitmap := big.NewInt(int64(binary.BigEndian.Uint16(options)))
1659 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(cc.Flags)))
1660
1661 flagBitmap.SetBit(flagBitmap, UserFlagRefusePM, optBitmap.Bit(refusePM))
1662 binary.BigEndian.PutUint16(cc.Flags, uint16(flagBitmap.Int64()))
1663
1664 flagBitmap.SetBit(flagBitmap, UserFlagRefusePChat, optBitmap.Bit(refuseChat))
1665 binary.BigEndian.PutUint16(cc.Flags, uint16(flagBitmap.Int64()))
1666
1667 // Check auto response
1668 if optBitmap.Bit(autoResponse) == 1 {
1669 cc.AutoReply = t.GetField(FieldAutomaticResponse).Data
1670 } else {
1671 cc.AutoReply = []byte{}
1672 }
1673 }
1674
1675 for _, c := range sortedClients(cc.Server.Clients) {
1676 res = append(res, *NewTransaction(
1677 TranNotifyChangeUser,
1678 c.ID,
1679 NewField(FieldUserID, *cc.ID),
1680 NewField(FieldUserIconID, cc.Icon),
1681 NewField(FieldUserFlags, cc.Flags),
1682 NewField(FieldUserName, cc.UserName),
1683 ))
1684 }
1685
1686 return res, err
1687 }
1688
1689 // HandleKeepAlive responds to keepalive transactions with an empty reply
1690 // * HL 1.9.2 Client sends keepalive msg every 3 minutes
1691 // * HL 1.2.3 Client doesn't send keepalives
1692 func HandleKeepAlive(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1693 res = append(res, cc.NewReply(t))
1694
1695 return res, err
1696 }
1697
1698 func HandleGetFileNameList(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1699 fullPath, err := readPath(
1700 cc.Server.Config.FileRoot,
1701 t.GetField(FieldFilePath).Data,
1702 nil,
1703 )
1704 if err != nil {
1705 return res, err
1706 }
1707
1708 var fp FilePath
1709 if t.GetField(FieldFilePath).Data != nil {
1710 if _, err = fp.Write(t.GetField(FieldFilePath).Data); err != nil {
1711 return res, err
1712 }
1713 }
1714
1715 // Handle special case for drop box folders
1716 if fp.IsDropbox() && !cc.Authorize(accessViewDropBoxes) {
1717 res = append(res, cc.NewErrReply(t, "You are not allowed to view drop boxes."))
1718 return res, err
1719 }
1720
1721 fileNames, err := getFileNameList(fullPath, cc.Server.Config.IgnoreFiles)
1722 if err != nil {
1723 return res, err
1724 }
1725
1726 res = append(res, cc.NewReply(t, fileNames...))
1727
1728 return res, err
1729 }
1730
1731 // =================================
1732 // Hotline private chat flow
1733 // =================================
1734 // 1. ClientA sends TranInviteNewChat to server with user ID to invite
1735 // 2. Server creates new ChatID
1736 // 3. Server sends TranInviteToChat to invitee
1737 // 4. Server replies to ClientA with new Chat ID
1738 //
1739 // A dialog box pops up in the invitee client with options to accept or decline the invitation.
1740 // If Accepted is clicked:
1741 // 1. ClientB sends TranJoinChat with FieldChatID
1742
1743 // HandleInviteNewChat invites users to new private chat
1744 func HandleInviteNewChat(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1745 if !cc.Authorize(accessOpenChat) {
1746 res = append(res, cc.NewErrReply(t, "You are not allowed to request private chat."))
1747 return res, err
1748 }
1749
1750 // Client to Invite
1751 targetID := t.GetField(FieldUserID).Data
1752 newChatID := cc.Server.NewPrivateChat(cc)
1753
1754 // Check if target user has "Refuse private chat" flag
1755 binary.BigEndian.Uint16(targetID)
1756 targetClient := cc.Server.Clients[binary.BigEndian.Uint16(targetID)]
1757
1758 flagBitmap := big.NewInt(int64(binary.BigEndian.Uint16(targetClient.Flags)))
1759 if flagBitmap.Bit(UserFlagRefusePChat) == 1 {
1760 res = append(res,
1761 *NewTransaction(
1762 TranServerMsg,
1763 cc.ID,
1764 NewField(FieldData, []byte(string(targetClient.UserName)+" does not accept private chats.")),
1765 NewField(FieldUserName, targetClient.UserName),
1766 NewField(FieldUserID, *targetClient.ID),
1767 NewField(FieldOptions, []byte{0, 2}),
1768 ),
1769 )
1770 } else {
1771 res = append(res,
1772 *NewTransaction(
1773 TranInviteToChat,
1774 &targetID,
1775 NewField(FieldChatID, newChatID),
1776 NewField(FieldUserName, cc.UserName),
1777 NewField(FieldUserID, *cc.ID),
1778 ),
1779 )
1780 }
1781
1782 res = append(res,
1783 cc.NewReply(t,
1784 NewField(FieldChatID, newChatID),
1785 NewField(FieldUserName, cc.UserName),
1786 NewField(FieldUserID, *cc.ID),
1787 NewField(FieldUserIconID, cc.Icon),
1788 NewField(FieldUserFlags, cc.Flags),
1789 ),
1790 )
1791
1792 return res, err
1793 }
1794
1795 func HandleInviteToChat(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1796 if !cc.Authorize(accessOpenChat) {
1797 res = append(res, cc.NewErrReply(t, "You are not allowed to request private chat."))
1798 return res, err
1799 }
1800
1801 // Client to Invite
1802 targetID := t.GetField(FieldUserID).Data
1803 chatID := t.GetField(FieldChatID).Data
1804
1805 res = append(res,
1806 *NewTransaction(
1807 TranInviteToChat,
1808 &targetID,
1809 NewField(FieldChatID, chatID),
1810 NewField(FieldUserName, cc.UserName),
1811 NewField(FieldUserID, *cc.ID),
1812 ),
1813 )
1814 res = append(res,
1815 cc.NewReply(
1816 t,
1817 NewField(FieldChatID, chatID),
1818 NewField(FieldUserName, cc.UserName),
1819 NewField(FieldUserID, *cc.ID),
1820 NewField(FieldUserIconID, cc.Icon),
1821 NewField(FieldUserFlags, cc.Flags),
1822 ),
1823 )
1824
1825 return res, err
1826 }
1827
1828 func HandleRejectChatInvite(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1829 chatID := t.GetField(FieldChatID).Data
1830 chatInt := binary.BigEndian.Uint32(chatID)
1831
1832 privChat := cc.Server.PrivateChats[chatInt]
1833
1834 resMsg := append(cc.UserName, []byte(" declined invitation to chat")...)
1835
1836 for _, c := range sortedClients(privChat.ClientConn) {
1837 res = append(res,
1838 *NewTransaction(
1839 TranChatMsg,
1840 c.ID,
1841 NewField(FieldChatID, chatID),
1842 NewField(FieldData, resMsg),
1843 ),
1844 )
1845 }
1846
1847 return res, err
1848 }
1849
1850 // HandleJoinChat is sent from a v1.8+ Hotline client when the joins a private chat
1851 // Fields used in the reply:
1852 // * 115 Chat subject
1853 // * 300 User name with info (Optional)
1854 // * 300 (more user names with info)
1855 func HandleJoinChat(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1856 chatID := t.GetField(FieldChatID).Data
1857 chatInt := binary.BigEndian.Uint32(chatID)
1858
1859 privChat := cc.Server.PrivateChats[chatInt]
1860
1861 // Send TranNotifyChatChangeUser to current members of the chat to inform of new user
1862 for _, c := range sortedClients(privChat.ClientConn) {
1863 res = append(res,
1864 *NewTransaction(
1865 TranNotifyChatChangeUser,
1866 c.ID,
1867 NewField(FieldChatID, chatID),
1868 NewField(FieldUserName, cc.UserName),
1869 NewField(FieldUserID, *cc.ID),
1870 NewField(FieldUserIconID, cc.Icon),
1871 NewField(FieldUserFlags, cc.Flags),
1872 ),
1873 )
1874 }
1875
1876 privChat.ClientConn[cc.uint16ID()] = cc
1877
1878 replyFields := []Field{NewField(FieldChatSubject, []byte(privChat.Subject))}
1879 for _, c := range sortedClients(privChat.ClientConn) {
1880 user := User{
1881 ID: *c.ID,
1882 Icon: c.Icon,
1883 Flags: c.Flags,
1884 Name: string(c.UserName),
1885 }
1886
1887 replyFields = append(replyFields, NewField(FieldUsernameWithInfo, user.Payload()))
1888 }
1889
1890 res = append(res, cc.NewReply(t, replyFields...))
1891 return res, err
1892 }
1893
1894 // HandleLeaveChat is sent from a v1.8+ Hotline client when the user exits a private chat
1895 // Fields used in the request:
1896 // - 114 FieldChatID
1897 //
1898 // Reply is not expected.
1899 func HandleLeaveChat(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1900 chatID := t.GetField(FieldChatID).Data
1901 chatInt := binary.BigEndian.Uint32(chatID)
1902
1903 privChat, ok := cc.Server.PrivateChats[chatInt]
1904 if !ok {
1905 return res, nil
1906 }
1907
1908 delete(privChat.ClientConn, cc.uint16ID())
1909
1910 // Notify members of the private chat that the user has left
1911 for _, c := range sortedClients(privChat.ClientConn) {
1912 res = append(res,
1913 *NewTransaction(
1914 TranNotifyChatDeleteUser,
1915 c.ID,
1916 NewField(FieldChatID, chatID),
1917 NewField(FieldUserID, *cc.ID),
1918 ),
1919 )
1920 }
1921
1922 return res, err
1923 }
1924
1925 // HandleSetChatSubject is sent from a v1.8+ Hotline client when the user sets a private chat subject
1926 // Fields used in the request:
1927 // * 114 Chat ID
1928 // * 115 Chat subject
1929 // Reply is not expected.
1930 func HandleSetChatSubject(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1931 chatID := t.GetField(FieldChatID).Data
1932 chatInt := binary.BigEndian.Uint32(chatID)
1933
1934 privChat := cc.Server.PrivateChats[chatInt]
1935 privChat.Subject = string(t.GetField(FieldChatSubject).Data)
1936
1937 for _, c := range sortedClients(privChat.ClientConn) {
1938 res = append(res,
1939 *NewTransaction(
1940 TranNotifyChatSubject,
1941 c.ID,
1942 NewField(FieldChatID, chatID),
1943 NewField(FieldChatSubject, t.GetField(FieldChatSubject).Data),
1944 ),
1945 )
1946 }
1947
1948 return res, err
1949 }
1950
1951 // HandleMakeAlias makes a file alias using the specified path.
1952 // Fields used in the request:
1953 // 201 File name
1954 // 202 File path
1955 // 212 File new path Destination path
1956 //
1957 // Fields used in the reply:
1958 // None
1959 func HandleMakeAlias(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1960 if !cc.Authorize(accessMakeAlias) {
1961 res = append(res, cc.NewErrReply(t, "You are not allowed to make aliases."))
1962 return res, err
1963 }
1964 fileName := t.GetField(FieldFileName).Data
1965 filePath := t.GetField(FieldFilePath).Data
1966 fileNewPath := t.GetField(FieldFileNewPath).Data
1967
1968 fullFilePath, err := readPath(cc.Server.Config.FileRoot, filePath, fileName)
1969 if err != nil {
1970 return res, err
1971 }
1972
1973 fullNewFilePath, err := readPath(cc.Server.Config.FileRoot, fileNewPath, fileName)
1974 if err != nil {
1975 return res, err
1976 }
1977
1978 cc.logger.Debugw("Make alias", "src", fullFilePath, "dst", fullNewFilePath)
1979
1980 if err := cc.Server.FS.Symlink(fullFilePath, fullNewFilePath); err != nil {
1981 res = append(res, cc.NewErrReply(t, "Error creating alias"))
1982 return res, nil
1983 }
1984
1985 res = append(res, cc.NewReply(t))
1986 return res, err
1987 }
1988
1989 // HandleDownloadBanner handles requests for a new banner from the server
1990 // Fields used in the request:
1991 // None
1992 // Fields used in the reply:
1993 // 107 FieldRefNum Used later for transfer
1994 // 108 FieldTransferSize Size of data to be downloaded
1995 func HandleDownloadBanner(cc *ClientConn, t *Transaction) (res []Transaction, err error) {
1996 fi, err := cc.Server.FS.Stat(filepath.Join(cc.Server.ConfigDir, cc.Server.Config.BannerFile))
1997 if err != nil {
1998 return res, err
1999 }
2000
2001 ft := cc.newFileTransfer(bannerDownload, []byte{}, []byte{}, make([]byte, 4))
2002
2003 binary.BigEndian.PutUint32(ft.TransferSize, uint32(fi.Size()))
2004
2005 res = append(res, cc.NewReply(t,
2006 NewField(FieldRefNum, ft.refNum[:]),
2007 NewField(FieldTransferSize, ft.TransferSize),
2008 ))
2009
2010 return res, err
2011 }