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