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