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