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