]> git.r.bdr.sh - rbdr/mobius/blame - hotline/server.go
Add client connection rate limit
[rbdr/mobius] / hotline / server.go
CommitLineData
6988a057
JH
1package hotline
2
3import (
7cd900d6 4 "bufio"
5cc444c8 5 "bytes"
6988a057 6 "context"
f8e4cd54 7 "crypto/rand"
6988a057
JH
8 "encoding/binary"
9 "errors"
10 "fmt"
2e1aec0f 11 "golang.org/x/text/encoding/charmap"
23c1295a 12 "golang.org/x/time/rate"
6988a057 13 "io"
a6216dd8
JH
14 "log"
15 "log/slog"
6988a057 16 "net"
b6e3be94 17 "os"
6988a057
JH
18 "strings"
19 "sync"
20 "time"
6988a057
JH
21)
22
7cd900d6
JH
23type contextKey string
24
25var contextKeyReq = contextKey("req")
26
27type requestCtx struct {
28 remoteAddr string
7cd900d6
JH
29}
30
2e1aec0f
JH
31// Converts bytes from Mac Roman encoding to UTF-8
32var txtDecoder = charmap.Macintosh.NewDecoder()
33
34// Converts bytes from UTF-8 to Mac Roman encoding
35var txtEncoder = charmap.Macintosh.NewEncoder()
36
6988a057 37type Server struct {
a2ef262a
JH
38 NetInterface string
39 Port int
a2ef262a 40
23c1295a
JH
41 rateLimiters map[string]*rate.Limiter
42
fd740bc4
JH
43 handlers map[TranType]HandlerFunc
44
45 Config Config
46 Logger *slog.Logger
c1c44744 47
40414f92 48 TrackerPassID [4]byte
00913df3 49
d9bc63a1 50 Stats Counter
6988a057 51
7cd900d6 52 FS FileStore // Storage backend to use for File storage
6988a057
JH
53
54 outbox chan Transaction
55
fd740bc4
JH
56 Agreement io.ReadSeeker
57 Banner []byte
8eb43f95 58
d9bc63a1
JH
59 FileTransferMgr FileTransferMgr
60 ChatMgr ChatManager
61 ClientMgr ClientManager
62 AccountManager AccountManager
63 ThreadedNewsMgr ThreadedNewsMgr
64 BanList BanMgr
46862572 65
d9bc63a1 66 MessageBoard io.ReadWriteSeeker
6988a057
JH
67}
68
fd740bc4 69type Option = func(s *Server)
d9bc63a1 70
fd740bc4
JH
71func WithConfig(config Config) func(s *Server) {
72 return func(s *Server) {
73 s.Config = config
d9bc63a1 74 }
fd740bc4 75}
d9bc63a1 76
fd740bc4
JH
77func WithLogger(logger *slog.Logger) func(s *Server) {
78 return func(s *Server) {
79 s.Logger = logger
d9bc63a1 80 }
fd740bc4 81}
d9bc63a1 82
fd740bc4
JH
83// WithPort optionally overrides the default TCP port.
84func WithPort(port int) func(s *Server) {
85 return func(s *Server) {
86 s.Port = port
d9bc63a1 87 }
fd740bc4 88}
d9bc63a1 89
fd740bc4
JH
90// WithInterface optionally sets a specific interface to listen on.
91func WithInterface(netInterface string) func(s *Server) {
92 return func(s *Server) {
93 s.NetInterface = netInterface
d9bc63a1 94 }
fd740bc4 95}
d9bc63a1 96
fd740bc4
JH
97type ServerConfig struct {
98}
d9bc63a1 99
fd740bc4
JH
100func NewServer(options ...Option) (*Server, error) {
101 server := Server{
102 handlers: make(map[TranType]HandlerFunc),
103 outbox: make(chan Transaction),
23c1295a 104 rateLimiters: make(map[string]*rate.Limiter),
fd740bc4
JH
105 FS: &OSFileStore{},
106 ChatMgr: NewMemChatManager(),
107 ClientMgr: NewMemClientMgr(),
108 FileTransferMgr: NewMemFileTransferMgr(),
109 Stats: NewStats(),
110 }
d9bc63a1 111
fd740bc4
JH
112 for _, opt := range options {
113 opt(&server)
d9bc63a1 114 }
00913df3 115
fd740bc4
JH
116 // generate a new random passID for tracker registration
117 _, err := rand.Read(server.TrackerPassID[:])
118 if err != nil {
119 return nil, err
120 }
00913df3 121
d9bc63a1 122 return &server, nil
00913df3
JH
123}
124
d9bc63a1
JH
125func (s *Server) CurrentStats() map[string]interface{} {
126 return s.Stats.Values()
6988a057
JH
127}
128
a2ef262a 129func (s *Server) ListenAndServe(ctx context.Context) error {
fd740bc4
JH
130 go s.registerWithTrackers(ctx)
131 go s.keepaliveHandler(ctx)
132 go s.processOutbox()
133
6988a057
JH
134 var wg sync.WaitGroup
135
136 wg.Add(1)
8168522a 137 go func() {
2d0f2abe 138 ln, err := net.Listen("tcp", fmt.Sprintf("%s:%v", s.NetInterface, s.Port))
8168522a 139 if err != nil {
a6216dd8 140 log.Fatal(err)
8168522a
JH
141 }
142
a6216dd8 143 log.Fatal(s.Serve(ctx, ln))
8168522a 144 }()
6988a057
JH
145
146 wg.Add(1)
8168522a 147 go func() {
2d0f2abe 148 ln, err := net.Listen("tcp", fmt.Sprintf("%s:%v", s.NetInterface, s.Port+1))
8168522a 149 if err != nil {
a6216dd8 150 log.Fatal(err)
8168522a
JH
151 }
152
a6216dd8 153 log.Fatal(s.ServeFileTransfers(ctx, ln))
8168522a 154 }()
6988a057
JH
155
156 wg.Wait()
157
158 return nil
159}
160
7cd900d6 161func (s *Server) ServeFileTransfers(ctx context.Context, ln net.Listener) error {
6988a057
JH
162 for {
163 conn, err := ln.Accept()
164 if err != nil {
165 return err
166 }
167
168 go func() {
7cd900d6
JH
169 defer func() { _ = conn.Close() }()
170
171 err = s.handleFileTransfer(
a2ef262a 172 context.WithValue(ctx, contextKeyReq, requestCtx{remoteAddr: conn.RemoteAddr().String()}),
7cd900d6
JH
173 conn,
174 )
175
176 if err != nil {
a6216dd8 177 s.Logger.Error("file transfer error", "reason", err)
6988a057
JH
178 }
179 }()
180 }
181}
182
183func (s *Server) sendTransaction(t Transaction) error {
fd740bc4 184 client := s.ClientMgr.Get(t.ClientID)
a2ef262a 185
d9bc63a1 186 if client == nil {
a2ef262a 187 return nil
6988a057 188 }
6988a057 189
a2ef262a 190 _, err := io.Copy(client.Connection, &t)
75e4191b 191 if err != nil {
fd740bc4 192 return fmt.Errorf("failed to send transaction to client %v: %v", t.ClientID, err)
6988a057 193 }
3178ae58 194
6988a057
JH
195 return nil
196}
197
67db911d
JH
198func (s *Server) processOutbox() {
199 for {
200 t := <-s.outbox
201 go func() {
202 if err := s.sendTransaction(t); err != nil {
a6216dd8 203 s.Logger.Error("error sending transaction", "err", err)
67db911d
JH
204 }
205 }()
206 }
207}
208
23c1295a
JH
209// perIPRateLimit controls how frequently an IP address can connect before being throttled.
210// 0.5 = 1 connection every 2 seconds
211const perIPRateLimit = rate.Limit(0.5)
212
7cd900d6 213func (s *Server) Serve(ctx context.Context, ln net.Listener) error {
6988a057 214 for {
fd740bc4
JH
215 select {
216 case <-ctx.Done():
217 s.Logger.Info("Server shutting down")
218 return ctx.Err()
219 default:
220 conn, err := ln.Accept()
221 if err != nil {
222 s.Logger.Error("Error accepting connection", "err", err)
223 continue
224 }
6988a057 225
fd740bc4 226 go func() {
23c1295a
JH
227 ipAddr := strings.Split(conn.RemoteAddr().(*net.TCPAddr).String(), ":")[0]
228
fd740bc4
JH
229 connCtx := context.WithValue(ctx, contextKeyReq, requestCtx{
230 remoteAddr: conn.RemoteAddr().String(),
231 })
232
23c1295a 233 s.Logger.Info("Connection established", "ip", ipAddr)
fd740bc4
JH
234 defer conn.Close()
235
23c1295a
JH
236 // Check if we have an existing rate limit for the IP and create one if we do not.
237 rl, ok := s.rateLimiters[ipAddr]
238 if !ok {
239 rl = rate.NewLimiter(perIPRateLimit, 1)
240 s.rateLimiters[ipAddr] = rl
241 }
242
243 // Check if the rate limit is exceeded and close the connection if so.
244 if !rl.Allow() {
245 s.Logger.Info("Rate limit exceeded", "RemoteAddr", conn.RemoteAddr())
246 conn.Close()
247 return
248 }
249
fd740bc4
JH
250 if err := s.handleNewConnection(connCtx, conn, conn.RemoteAddr().String()); err != nil {
251 if err == io.EOF {
252 s.Logger.Info("Client disconnected", "RemoteAddr", conn.RemoteAddr())
253 } else {
254 s.Logger.Error("Error serving request", "RemoteAddr", conn.RemoteAddr(), "err", err)
255 }
6988a057 256 }
fd740bc4
JH
257 }()
258 }
6988a057
JH
259 }
260}
261
fd740bc4
JH
262// time in seconds between tracker re-registration
263const trackerUpdateFrequency = 300
264
265// registerWithTrackers runs every trackerUpdateFrequency seconds to update the server's tracker entry on all configured
266// trackers.
267func (s *Server) registerWithTrackers(ctx context.Context) {
d9bc63a1 268 for {
6eaf9391
JH
269 if s.Config.EnableTrackerRegistration {
270 for _, t := range s.Config.Trackers {
fd740bc4
JH
271 tr := &TrackerRegistration{
272 UserCount: len(s.ClientMgr.List()),
273 PassID: s.TrackerPassID,
274 Name: s.Config.Name,
275 Description: s.Config.Description,
276 }
277 binary.BigEndian.PutUint16(tr.Port[:], uint16(s.Port))
278
6eaf9391
JH
279 // Check the tracker string for a password. This is janky but avoids a breaking change to the Config
280 // Trackers field.
281 splitAddr := strings.Split(":", t)
282 if len(splitAddr) == 3 {
283 tr.Password = splitAddr[2]
284 }
285
286 if err := register(&RealDialer{}, t, tr); err != nil {
287 s.Logger.Error(fmt.Sprintf("Unable to register with tracker %v", t), "error", err)
fd740bc4 288 }
6988a057 289 }
d9bc63a1 290 }
6eaf9391
JH
291 // Using time.Ticker with for/select would be more idiomatic, but it's super annoying that it doesn't tick on
292 // first pass. Revist, maybe.
293 // https://github.com/golang/go/issues/17601
294 time.Sleep(trackerUpdateFrequency * time.Second)
d9bc63a1 295 }
fd740bc4 296}
d9bc63a1 297
fd740bc4
JH
298const (
299 userIdleSeconds = 300 // time in seconds before an inactive user is marked idle
300 idleCheckInterval = 10 // time in seconds to check for idle users
301)
6988a057 302
fd740bc4
JH
303// keepaliveHandler runs every idleCheckInterval seconds and increments a user's idle time by idleCheckInterval seconds.
304// If the updated idle time exceeds userIdleSeconds and the user was not previously idle, we notify all connected clients
305// that the user has gone idle. For most clients, this turns the user grey in the user list.
306func (s *Server) keepaliveHandler(ctx context.Context) {
307 ticker := time.NewTicker(idleCheckInterval * time.Second)
308 defer ticker.Stop()
d9bc63a1 309
fd740bc4
JH
310 for {
311 select {
312 case <-ctx.Done():
313 return
314 case <-ticker.C:
315 for _, c := range s.ClientMgr.List() {
316 c.mu.Lock()
317 c.IdleTime += idleCheckInterval
318
319 // Check if the user
320 if c.IdleTime > userIdleSeconds && !c.Flags.IsSet(UserFlagAway) {
321 c.Flags.Set(UserFlagAway, 1)
322
323 c.SendAll(
324 TranNotifyChangeUser,
325 NewField(FieldUserID, c.ID[:]),
326 NewField(FieldUserFlags, c.Flags[:]),
327 NewField(FieldUserName, c.UserName),
328 NewField(FieldUserIconID, c.Icon),
329 )
330 }
331 c.mu.Unlock()
6988a057
JH
332 }
333 }
46862572 334 }
6988a057
JH
335}
336
67db911d 337func (s *Server) NewClientConn(conn io.ReadWriteCloser, remoteAddr string) *ClientConn {
6988a057 338 clientConn := &ClientConn{
a2ef262a 339 Icon: []byte{0, 0}, // TODO: make array type
6988a057
JH
340 Connection: conn,
341 Server: s,
d4c152a4 342 RemoteAddr: remoteAddr,
d2810ae9 343
d9bc63a1 344 ClientFileTransferMgr: NewClientFileTransferMgr(),
180d6544
JH
345 }
346
d9bc63a1 347 s.ClientMgr.Add(clientConn)
6988a057 348
d9bc63a1 349 return clientConn
6988a057
JH
350}
351
a2ef262a
JH
352func sendBanMessage(rwc io.Writer, message string) {
353 t := NewTransaction(
354 TranServerMsg,
355 [2]byte{0, 0},
356 NewField(FieldData, []byte(message)),
357 NewField(FieldChatOptions, []byte{0, 0}),
358 )
359 _, _ = io.Copy(rwc, &t)
360 time.Sleep(1 * time.Second)
361}
362
6988a057 363// handleNewConnection takes a new net.Conn and performs the initial login sequence
3178ae58 364func (s *Server) handleNewConnection(ctx context.Context, rwc io.ReadWriteCloser, remoteAddr string) error {
c4208f86
JH
365 defer dontPanic(s.Logger)
366
fd740bc4
JH
367 if err := performHandshake(rwc); err != nil {
368 return fmt.Errorf("perform handshake: %w", err)
369 }
370
a2ef262a
JH
371 // Check if remoteAddr is present in the ban list
372 ipAddr := strings.Split(remoteAddr, ":")[0]
d9bc63a1 373 if isBanned, banUntil := s.BanList.IsBanned(ipAddr); isBanned {
a2ef262a
JH
374 // permaban
375 if banUntil == nil {
376 sendBanMessage(rwc, "You are permanently banned on this server")
377 s.Logger.Debug("Disconnecting permanently banned IP", "remoteAddr", ipAddr)
378 return nil
379 }
380
381 // temporary ban
382 if time.Now().Before(*banUntil) {
383 sendBanMessage(rwc, "You are temporarily banned on this server")
384 s.Logger.Debug("Disconnecting temporarily banned IP", "remoteAddr", ipAddr)
385 return nil
386 }
387 }
388
3178ae58
JH
389 // Create a new scanner for parsing incoming bytes into transaction tokens
390 scanner := bufio.NewScanner(rwc)
391 scanner.Split(transactionScanner)
392
393 scanner.Scan()
6988a057 394
f4cdaddc
JH
395 // Make a new []byte slice and copy the scanner bytes to it. This is critical to avoid a data race as the
396 // scanner re-uses the buffer for subsequent scans.
397 buf := make([]byte, len(scanner.Bytes()))
398 copy(buf, scanner.Bytes())
399
854a92fc 400 var clientLogin Transaction
f4cdaddc 401 if _, err := clientLogin.Write(buf); err != nil {
a2ef262a 402 return fmt.Errorf("error writing login transaction: %w", err)
46862572 403 }
e9c043c0
JH
404
405 c := s.NewClientConn(rwc, remoteAddr)
6988a057 406 defer c.Disconnect()
6988a057 407
d005ef04
JH
408 encodedPassword := clientLogin.GetField(FieldUserPassword).Data
409 c.Version = clientLogin.GetField(FieldVersion).Data
6988a057 410
d9bc63a1 411 login := clientLogin.GetField(FieldUserLogin).DecodeObfuscatedString()
6988a057
JH
412 if login == "" {
413 login = GuestAccount
414 }
415
fd740bc4 416 c.Logger = s.Logger.With("ip", ipAddr, "login", login)
0da28a1f 417
6988a057
JH
418 // If authentication fails, send error reply and close connection
419 if !c.Authenticate(login, encodedPassword) {
a2ef262a 420 t := c.NewErrReply(&clientLogin, "Incorrect login.")[0]
95159e55
JH
421
422 _, err := io.Copy(rwc, &t)
72dd37f1
JH
423 if err != nil {
424 return err
425 }
0da28a1f 426
adcd4879 427 c.Logger.Info("Incorrect login")
0da28a1f
JH
428
429 return nil
6988a057
JH
430 }
431
d005ef04
JH
432 if clientLogin.GetField(FieldUserIconID).Data != nil {
433 c.Icon = clientLogin.GetField(FieldUserIconID).Data
59097464
JH
434 }
435
d9bc63a1
JH
436 c.Account = c.Server.AccountManager.Get(login)
437 if c.Account == nil {
438 return nil
439 }
59097464 440
d005ef04 441 if clientLogin.GetField(FieldUserName).Data != nil {
d9bc63a1 442 if c.Authorize(AccessAnyName) {
d005ef04 443 c.UserName = clientLogin.GetField(FieldUserName).Data
ea5d8c51
JH
444 } else {
445 c.UserName = []byte(c.Account.Name)
446 }
6988a057
JH
447 }
448
d9bc63a1 449 if c.Authorize(AccessDisconUser) {
a2ef262a 450 c.Flags.Set(UserFlagAdmin, 1)
6988a057
JH
451 }
452
854a92fc 453 s.outbox <- c.NewReply(&clientLogin,
d005ef04
JH
454 NewField(FieldVersion, []byte{0x00, 0xbe}),
455 NewField(FieldCommunityBannerID, []byte{0, 0}),
456 NewField(FieldServerName, []byte(s.Config.Name)),
6988a057
JH
457 )
458
459 // Send user access privs so client UI knows how to behave
a2ef262a 460 c.Server.outbox <- NewTransaction(TranUserAccess, c.ID, NewField(FieldUserAccess, c.Account.Access[:]))
6988a057 461
d9bc63a1 462 // Accounts with AccessNoAgreement do not receive the server agreement on login. The behavior is different between
d005ef04
JH
463 // client versions. For 1.2.3 client, we do not send TranShowAgreement. For other client versions, we send
464 // TranShowAgreement but with the NoServerAgreement field set to 1.
d9bc63a1 465 if c.Authorize(AccessNoAgreement) {
a322be02
JH
466 // If client version is nil, then the client uses the 1.2.3 login behavior
467 if c.Version != nil {
a2ef262a 468 c.Server.outbox <- NewTransaction(TranShowAgreement, c.ID, NewField(FieldNoServerAgreement, []byte{1}))
a322be02 469 }
688c86d2 470 } else {
fd740bc4
JH
471 _, _ = c.Server.Agreement.Seek(0, 0)
472 data, _ := io.ReadAll(c.Server.Agreement)
473
474 c.Server.outbox <- NewTransaction(TranShowAgreement, c.ID, NewField(FieldData, data))
688c86d2 475 }
6988a057 476
2f8472fa
JH
477 // If the client has provided a username as part of the login, we can infer that it is using the 1.2.3 login
478 // flow and not the 1.5+ flow.
479 if len(c.UserName) != 0 {
480 // Add the client username to the logger. For 1.5+ clients, we don't have this information yet as it comes as
d005ef04 481 // part of TranAgreed
fd740bc4
JH
482 c.Logger = c.Logger.With("name", string(c.UserName))
483 c.Logger.Info("Login successful")
2f8472fa
JH
484
485 // Notify other clients on the server that the new user has logged in. For 1.5+ clients we don't have this
d005ef04 486 // information yet, so we do it in TranAgreed instead
d9bc63a1 487 for _, t := range c.NotifyOthers(
a2ef262a
JH
488 NewTransaction(
489 TranNotifyChangeUser, [2]byte{0, 0},
d005ef04 490 NewField(FieldUserName, c.UserName),
a2ef262a 491 NewField(FieldUserID, c.ID[:]),
d005ef04 492 NewField(FieldUserIconID, c.Icon),
a2ef262a 493 NewField(FieldUserFlags, c.Flags[:]),
2f8472fa
JH
494 ),
495 ) {
496 c.Server.outbox <- t
497 }
6988a057 498 }
bd1ce113 499
d9bc63a1
JH
500 c.Server.Stats.Increment(StatConnectionCounter, StatCurrentlyConnected)
501 defer c.Server.Stats.Decrement(StatCurrentlyConnected)
502
503 if len(s.ClientMgr.List()) > c.Server.Stats.Get(StatConnectionPeak) {
504 c.Server.Stats.Set(StatConnectionPeak, len(s.ClientMgr.List()))
00913df3 505 }
6988a057 506
3178ae58
JH
507 // Scan for new transactions and handle them as they come in.
508 for scanner.Scan() {
a2ef262a 509 // Copy the scanner bytes to a new slice to it to avoid a data race when the scanner re-uses the buffer.
fd740bc4
JH
510 tmpBuf := make([]byte, len(scanner.Bytes()))
511 copy(tmpBuf, scanner.Bytes())
6988a057 512
854a92fc 513 var t Transaction
fd740bc4 514 if _, err := t.Write(tmpBuf); err != nil {
854a92fc 515 return err
6988a057 516 }
854a92fc 517
a2ef262a 518 c.handleTransaction(t)
6988a057 519 }
3178ae58 520 return nil
6988a057
JH
521}
522
85767504 523// handleFileTransfer receives a client net.Conn from the file transfer server, performs the requested transfer type, then closes the connection
7cd900d6 524func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) error {
37a954c8 525 defer dontPanic(s.Logger)
0a92e50b 526
a2ef262a 527 // The first 16 bytes contain the file transfer.
df2735b2 528 var t transfer
a2ef262a
JH
529 if _, err := io.CopyN(&t, rwc, 16); err != nil {
530 return fmt.Errorf("error reading file transfer: %w", err)
6988a057
JH
531 }
532
d9bc63a1
JH
533 fileTransfer := s.FileTransferMgr.Get(t.ReferenceNumber)
534 if fileTransfer == nil {
535 return errors.New("invalid transaction ID")
536 }
537
0a92e50b 538 defer func() {
d9bc63a1 539 s.FileTransferMgr.Delete(t.ReferenceNumber)
df1ade54 540
94742e2f
JH
541 // Wait a few seconds before closing the connection: this is a workaround for problems
542 // observed with Windows clients where the client must initiate close of the TCP connection before
543 // the server does. This is gross and seems unnecessary. TODO: Revisit?
544 time.Sleep(3 * time.Second)
0a92e50b 545 }()
6988a057 546
7cd900d6
JH
547 rLogger := s.Logger.With(
548 "remoteAddr", ctx.Value(contextKeyReq).(requestCtx).remoteAddr,
df1ade54 549 "login", fileTransfer.ClientConn.Account.Login,
95159e55 550 "Name", string(fileTransfer.ClientConn.UserName),
7cd900d6
JH
551 )
552
dcd23d53 553 fullPath, err := ReadPath(fileTransfer.FileRoot, fileTransfer.FilePath, fileTransfer.FileName)
df1ade54
JH
554 if err != nil {
555 return err
556 }
557
6988a057 558 switch fileTransfer.Type {
d9bc63a1 559 case BannerDownload:
fd740bc4 560 if _, err := io.Copy(rwc, bytes.NewBuffer(s.Banner)); err != nil {
dcd23d53 561 return fmt.Errorf("banner download: %w", err)
9067f234 562 }
6988a057 563 case FileDownload:
d9bc63a1 564 s.Stats.Increment(StatDownloadCounter, StatDownloadsInProgress)
94742e2f 565 defer func() {
d9bc63a1 566 s.Stats.Decrement(StatDownloadsInProgress)
94742e2f 567 }()
23411fc2 568
a2ef262a 569 err = DownloadHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, true)
6988a057 570 if err != nil {
d9bc63a1 571 return fmt.Errorf("file download: %w", err)
7cd900d6
JH
572 }
573
6988a057 574 case FileUpload:
d9bc63a1
JH
575 s.Stats.Increment(StatUploadCounter, StatUploadsInProgress)
576 defer func() {
577 s.Stats.Decrement(StatUploadsInProgress)
578 }()
23411fc2 579
a2ef262a 580 err = UploadHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, s.Config.PreserveResourceForks)
7cd900d6 581 if err != nil {
dcd23d53 582 return fmt.Errorf("file upload: %w", err)
6988a057 583 }
85767504 584
6988a057 585 case FolderDownload:
d9bc63a1
JH
586 s.Stats.Increment(StatDownloadCounter, StatDownloadsInProgress)
587 defer func() {
588 s.Stats.Decrement(StatDownloadsInProgress)
589 }()
00913df3 590
a2ef262a 591 err = DownloadFolderHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, s.Config.PreserveResourceForks)
67db911d 592 if err != nil {
dcd23d53 593 return fmt.Errorf("folder download: %w", err)
67db911d
JH
594 }
595
6988a057 596 case FolderUpload:
d9bc63a1
JH
597 s.Stats.Increment(StatUploadCounter, StatUploadsInProgress)
598 defer func() {
599 s.Stats.Decrement(StatUploadsInProgress)
600 }()
601
a6216dd8 602 rLogger.Info(
6988a057 603 "Folder upload started",
df1ade54
JH
604 "dstPath", fullPath,
605 "TransferSize", binary.BigEndian.Uint32(fileTransfer.TransferSize),
6988a057
JH
606 "FolderItemCount", fileTransfer.FolderItemCount,
607 )
608
a2ef262a
JH
609 err = UploadFolderHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, s.Config.PreserveResourceForks)
610 if err != nil {
dcd23d53 611 return fmt.Errorf("folder upload: %w", err)
6988a057 612 }
6988a057 613 }
6988a057
JH
614 return nil
615}
b6e3be94
JH
616
617func (s *Server) SendAll(t TranType, fields ...Field) {
618 for _, c := range s.ClientMgr.List() {
619 s.outbox <- NewTransaction(t, c.ID, fields...)
620 }
621}
622
623func (s *Server) Shutdown(msg []byte) {
624 s.Logger.Info("Shutdown signal received")
625 s.SendAll(TranDisconnectMsg, NewField(FieldData, msg))
626
627 time.Sleep(3 * time.Second)
628
629 os.Exit(0)
630}