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