]> git.r.bdr.sh - rbdr/mobius/blame - hotline/server.go
Delete cmd/mobius-hotline-server/mobius/config/Files/hello.txt
[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 {
6e4935b3 177 s.Logger.Error("file transfer error", "err", 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) {
dd88b856
JH
268 if s.Config.EnableTrackerRegistration {
269 s.Logger.Info("Tracker registration enabled", "trackers", s.Config.Trackers)
270 }
117c2040 271
d9bc63a1 272 for {
6eaf9391
JH
273 if s.Config.EnableTrackerRegistration {
274 for _, t := range s.Config.Trackers {
fd740bc4
JH
275 tr := &TrackerRegistration{
276 UserCount: len(s.ClientMgr.List()),
277 PassID: s.TrackerPassID,
278 Name: s.Config.Name,
279 Description: s.Config.Description,
280 }
281 binary.BigEndian.PutUint16(tr.Port[:], uint16(s.Port))
282
6eaf9391
JH
283 // Check the tracker string for a password. This is janky but avoids a breaking change to the Config
284 // Trackers field.
285 splitAddr := strings.Split(":", t)
286 if len(splitAddr) == 3 {
287 tr.Password = splitAddr[2]
288 }
289
290 if err := register(&RealDialer{}, t, tr); err != nil {
291 s.Logger.Error(fmt.Sprintf("Unable to register with tracker %v", t), "error", err)
fd740bc4 292 }
6988a057 293 }
d9bc63a1 294 }
6eaf9391
JH
295 // Using time.Ticker with for/select would be more idiomatic, but it's super annoying that it doesn't tick on
296 // first pass. Revist, maybe.
297 // https://github.com/golang/go/issues/17601
298 time.Sleep(trackerUpdateFrequency * time.Second)
d9bc63a1 299 }
fd740bc4 300}
d9bc63a1 301
fd740bc4
JH
302const (
303 userIdleSeconds = 300 // time in seconds before an inactive user is marked idle
304 idleCheckInterval = 10 // time in seconds to check for idle users
305)
6988a057 306
fd740bc4
JH
307// keepaliveHandler runs every idleCheckInterval seconds and increments a user's idle time by idleCheckInterval seconds.
308// If the updated idle time exceeds userIdleSeconds and the user was not previously idle, we notify all connected clients
309// that the user has gone idle. For most clients, this turns the user grey in the user list.
310func (s *Server) keepaliveHandler(ctx context.Context) {
311 ticker := time.NewTicker(idleCheckInterval * time.Second)
312 defer ticker.Stop()
d9bc63a1 313
fd740bc4
JH
314 for {
315 select {
316 case <-ctx.Done():
317 return
318 case <-ticker.C:
319 for _, c := range s.ClientMgr.List() {
320 c.mu.Lock()
321 c.IdleTime += idleCheckInterval
322
323 // Check if the user
324 if c.IdleTime > userIdleSeconds && !c.Flags.IsSet(UserFlagAway) {
325 c.Flags.Set(UserFlagAway, 1)
326
327 c.SendAll(
328 TranNotifyChangeUser,
329 NewField(FieldUserID, c.ID[:]),
330 NewField(FieldUserFlags, c.Flags[:]),
331 NewField(FieldUserName, c.UserName),
332 NewField(FieldUserIconID, c.Icon),
333 )
334 }
335 c.mu.Unlock()
6988a057
JH
336 }
337 }
46862572 338 }
6988a057
JH
339}
340
67db911d 341func (s *Server) NewClientConn(conn io.ReadWriteCloser, remoteAddr string) *ClientConn {
6988a057 342 clientConn := &ClientConn{
a2ef262a 343 Icon: []byte{0, 0}, // TODO: make array type
6988a057
JH
344 Connection: conn,
345 Server: s,
d4c152a4 346 RemoteAddr: remoteAddr,
d2810ae9 347
d9bc63a1 348 ClientFileTransferMgr: NewClientFileTransferMgr(),
180d6544
JH
349 }
350
d9bc63a1 351 s.ClientMgr.Add(clientConn)
6988a057 352
d9bc63a1 353 return clientConn
6988a057
JH
354}
355
a2ef262a
JH
356func sendBanMessage(rwc io.Writer, message string) {
357 t := NewTransaction(
358 TranServerMsg,
359 [2]byte{0, 0},
360 NewField(FieldData, []byte(message)),
361 NewField(FieldChatOptions, []byte{0, 0}),
362 )
363 _, _ = io.Copy(rwc, &t)
364 time.Sleep(1 * time.Second)
365}
366
6988a057 367// handleNewConnection takes a new net.Conn and performs the initial login sequence
3178ae58 368func (s *Server) handleNewConnection(ctx context.Context, rwc io.ReadWriteCloser, remoteAddr string) error {
c4208f86
JH
369 defer dontPanic(s.Logger)
370
fd740bc4
JH
371 if err := performHandshake(rwc); err != nil {
372 return fmt.Errorf("perform handshake: %w", err)
373 }
374
a2ef262a
JH
375 // Check if remoteAddr is present in the ban list
376 ipAddr := strings.Split(remoteAddr, ":")[0]
d9bc63a1 377 if isBanned, banUntil := s.BanList.IsBanned(ipAddr); isBanned {
a2ef262a
JH
378 // permaban
379 if banUntil == nil {
380 sendBanMessage(rwc, "You are permanently banned on this server")
381 s.Logger.Debug("Disconnecting permanently banned IP", "remoteAddr", ipAddr)
382 return nil
383 }
384
385 // temporary ban
386 if time.Now().Before(*banUntil) {
387 sendBanMessage(rwc, "You are temporarily banned on this server")
388 s.Logger.Debug("Disconnecting temporarily banned IP", "remoteAddr", ipAddr)
389 return nil
390 }
391 }
392
3178ae58
JH
393 // Create a new scanner for parsing incoming bytes into transaction tokens
394 scanner := bufio.NewScanner(rwc)
395 scanner.Split(transactionScanner)
396
397 scanner.Scan()
6988a057 398
f4cdaddc
JH
399 // Make a new []byte slice and copy the scanner bytes to it. This is critical to avoid a data race as the
400 // scanner re-uses the buffer for subsequent scans.
401 buf := make([]byte, len(scanner.Bytes()))
402 copy(buf, scanner.Bytes())
403
854a92fc 404 var clientLogin Transaction
f4cdaddc 405 if _, err := clientLogin.Write(buf); err != nil {
a2ef262a 406 return fmt.Errorf("error writing login transaction: %w", err)
46862572 407 }
e9c043c0
JH
408
409 c := s.NewClientConn(rwc, remoteAddr)
6988a057 410 defer c.Disconnect()
6988a057 411
d005ef04
JH
412 encodedPassword := clientLogin.GetField(FieldUserPassword).Data
413 c.Version = clientLogin.GetField(FieldVersion).Data
6988a057 414
d9bc63a1 415 login := clientLogin.GetField(FieldUserLogin).DecodeObfuscatedString()
6988a057
JH
416 if login == "" {
417 login = GuestAccount
418 }
419
fd740bc4 420 c.Logger = s.Logger.With("ip", ipAddr, "login", login)
0da28a1f 421
6988a057
JH
422 // If authentication fails, send error reply and close connection
423 if !c.Authenticate(login, encodedPassword) {
a2ef262a 424 t := c.NewErrReply(&clientLogin, "Incorrect login.")[0]
95159e55
JH
425
426 _, err := io.Copy(rwc, &t)
72dd37f1
JH
427 if err != nil {
428 return err
429 }
0da28a1f 430
adcd4879 431 c.Logger.Info("Incorrect login")
0da28a1f
JH
432
433 return nil
6988a057
JH
434 }
435
d005ef04
JH
436 if clientLogin.GetField(FieldUserIconID).Data != nil {
437 c.Icon = clientLogin.GetField(FieldUserIconID).Data
59097464
JH
438 }
439
d9bc63a1
JH
440 c.Account = c.Server.AccountManager.Get(login)
441 if c.Account == nil {
442 return nil
443 }
59097464 444
d005ef04 445 if clientLogin.GetField(FieldUserName).Data != nil {
d9bc63a1 446 if c.Authorize(AccessAnyName) {
d005ef04 447 c.UserName = clientLogin.GetField(FieldUserName).Data
ea5d8c51
JH
448 } else {
449 c.UserName = []byte(c.Account.Name)
450 }
6988a057
JH
451 }
452
d9bc63a1 453 if c.Authorize(AccessDisconUser) {
a2ef262a 454 c.Flags.Set(UserFlagAdmin, 1)
6988a057
JH
455 }
456
854a92fc 457 s.outbox <- c.NewReply(&clientLogin,
d005ef04
JH
458 NewField(FieldVersion, []byte{0x00, 0xbe}),
459 NewField(FieldCommunityBannerID, []byte{0, 0}),
460 NewField(FieldServerName, []byte(s.Config.Name)),
6988a057
JH
461 )
462
463 // Send user access privs so client UI knows how to behave
a2ef262a 464 c.Server.outbox <- NewTransaction(TranUserAccess, c.ID, NewField(FieldUserAccess, c.Account.Access[:]))
6988a057 465
d9bc63a1 466 // Accounts with AccessNoAgreement do not receive the server agreement on login. The behavior is different between
d005ef04
JH
467 // client versions. For 1.2.3 client, we do not send TranShowAgreement. For other client versions, we send
468 // TranShowAgreement but with the NoServerAgreement field set to 1.
d9bc63a1 469 if c.Authorize(AccessNoAgreement) {
a322be02
JH
470 // If client version is nil, then the client uses the 1.2.3 login behavior
471 if c.Version != nil {
a2ef262a 472 c.Server.outbox <- NewTransaction(TranShowAgreement, c.ID, NewField(FieldNoServerAgreement, []byte{1}))
a322be02 473 }
688c86d2 474 } else {
fd740bc4
JH
475 _, _ = c.Server.Agreement.Seek(0, 0)
476 data, _ := io.ReadAll(c.Server.Agreement)
477
478 c.Server.outbox <- NewTransaction(TranShowAgreement, c.ID, NewField(FieldData, data))
688c86d2 479 }
6988a057 480
2f8472fa
JH
481 // If the client has provided a username as part of the login, we can infer that it is using the 1.2.3 login
482 // flow and not the 1.5+ flow.
483 if len(c.UserName) != 0 {
484 // Add the client username to the logger. For 1.5+ clients, we don't have this information yet as it comes as
d005ef04 485 // part of TranAgreed
fd740bc4
JH
486 c.Logger = c.Logger.With("name", string(c.UserName))
487 c.Logger.Info("Login successful")
2f8472fa
JH
488
489 // Notify other clients on the server that the new user has logged in. For 1.5+ clients we don't have this
d005ef04 490 // information yet, so we do it in TranAgreed instead
d9bc63a1 491 for _, t := range c.NotifyOthers(
a2ef262a
JH
492 NewTransaction(
493 TranNotifyChangeUser, [2]byte{0, 0},
d005ef04 494 NewField(FieldUserName, c.UserName),
a2ef262a 495 NewField(FieldUserID, c.ID[:]),
d005ef04 496 NewField(FieldUserIconID, c.Icon),
a2ef262a 497 NewField(FieldUserFlags, c.Flags[:]),
2f8472fa
JH
498 ),
499 ) {
500 c.Server.outbox <- t
501 }
6988a057 502 }
bd1ce113 503
d9bc63a1
JH
504 c.Server.Stats.Increment(StatConnectionCounter, StatCurrentlyConnected)
505 defer c.Server.Stats.Decrement(StatCurrentlyConnected)
506
507 if len(s.ClientMgr.List()) > c.Server.Stats.Get(StatConnectionPeak) {
508 c.Server.Stats.Set(StatConnectionPeak, len(s.ClientMgr.List()))
00913df3 509 }
6988a057 510
3178ae58
JH
511 // Scan for new transactions and handle them as they come in.
512 for scanner.Scan() {
a2ef262a 513 // Copy the scanner bytes to a new slice to it to avoid a data race when the scanner re-uses the buffer.
fd740bc4
JH
514 tmpBuf := make([]byte, len(scanner.Bytes()))
515 copy(tmpBuf, scanner.Bytes())
6988a057 516
854a92fc 517 var t Transaction
fd740bc4 518 if _, err := t.Write(tmpBuf); err != nil {
854a92fc 519 return err
6988a057 520 }
854a92fc 521
a2ef262a 522 c.handleTransaction(t)
6988a057 523 }
3178ae58 524 return nil
6988a057
JH
525}
526
85767504 527// handleFileTransfer receives a client net.Conn from the file transfer server, performs the requested transfer type, then closes the connection
7cd900d6 528func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) error {
37a954c8 529 defer dontPanic(s.Logger)
0a92e50b 530
a2ef262a 531 // The first 16 bytes contain the file transfer.
df2735b2 532 var t transfer
a2ef262a
JH
533 if _, err := io.CopyN(&t, rwc, 16); err != nil {
534 return fmt.Errorf("error reading file transfer: %w", err)
6988a057
JH
535 }
536
d9bc63a1
JH
537 fileTransfer := s.FileTransferMgr.Get(t.ReferenceNumber)
538 if fileTransfer == nil {
539 return errors.New("invalid transaction ID")
540 }
541
0a92e50b 542 defer func() {
d9bc63a1 543 s.FileTransferMgr.Delete(t.ReferenceNumber)
df1ade54 544
94742e2f
JH
545 // Wait a few seconds before closing the connection: this is a workaround for problems
546 // observed with Windows clients where the client must initiate close of the TCP connection before
547 // the server does. This is gross and seems unnecessary. TODO: Revisit?
548 time.Sleep(3 * time.Second)
0a92e50b 549 }()
6988a057 550
7cd900d6
JH
551 rLogger := s.Logger.With(
552 "remoteAddr", ctx.Value(contextKeyReq).(requestCtx).remoteAddr,
df1ade54 553 "login", fileTransfer.ClientConn.Account.Login,
95159e55 554 "Name", string(fileTransfer.ClientConn.UserName),
7cd900d6
JH
555 )
556
dcd23d53 557 fullPath, err := ReadPath(fileTransfer.FileRoot, fileTransfer.FilePath, fileTransfer.FileName)
df1ade54
JH
558 if err != nil {
559 return err
560 }
561
6988a057 562 switch fileTransfer.Type {
d9bc63a1 563 case BannerDownload:
fd740bc4 564 if _, err := io.Copy(rwc, bytes.NewBuffer(s.Banner)); err != nil {
dcd23d53 565 return fmt.Errorf("banner download: %w", err)
9067f234 566 }
6988a057 567 case FileDownload:
d9bc63a1 568 s.Stats.Increment(StatDownloadCounter, StatDownloadsInProgress)
94742e2f 569 defer func() {
d9bc63a1 570 s.Stats.Decrement(StatDownloadsInProgress)
94742e2f 571 }()
23411fc2 572
a2ef262a 573 err = DownloadHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, true)
6988a057 574 if err != nil {
d9bc63a1 575 return fmt.Errorf("file download: %w", err)
7cd900d6
JH
576 }
577
6988a057 578 case FileUpload:
d9bc63a1
JH
579 s.Stats.Increment(StatUploadCounter, StatUploadsInProgress)
580 defer func() {
581 s.Stats.Decrement(StatUploadsInProgress)
582 }()
23411fc2 583
a2ef262a 584 err = UploadHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, s.Config.PreserveResourceForks)
7cd900d6 585 if err != nil {
dcd23d53 586 return fmt.Errorf("file upload: %w", err)
6988a057 587 }
85767504 588
6988a057 589 case FolderDownload:
d9bc63a1
JH
590 s.Stats.Increment(StatDownloadCounter, StatDownloadsInProgress)
591 defer func() {
592 s.Stats.Decrement(StatDownloadsInProgress)
593 }()
00913df3 594
a2ef262a 595 err = DownloadFolderHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, s.Config.PreserveResourceForks)
67db911d 596 if err != nil {
dcd23d53 597 return fmt.Errorf("folder download: %w", err)
67db911d
JH
598 }
599
6988a057 600 case FolderUpload:
d9bc63a1
JH
601 s.Stats.Increment(StatUploadCounter, StatUploadsInProgress)
602 defer func() {
603 s.Stats.Decrement(StatUploadsInProgress)
604 }()
605
a6216dd8 606 rLogger.Info(
6988a057 607 "Folder upload started",
df1ade54
JH
608 "dstPath", fullPath,
609 "TransferSize", binary.BigEndian.Uint32(fileTransfer.TransferSize),
6988a057
JH
610 "FolderItemCount", fileTransfer.FolderItemCount,
611 )
612
a2ef262a
JH
613 err = UploadFolderHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, s.Config.PreserveResourceForks)
614 if err != nil {
dcd23d53 615 return fmt.Errorf("folder upload: %w", err)
6988a057 616 }
6988a057 617 }
6988a057
JH
618 return nil
619}
b6e3be94
JH
620
621func (s *Server) SendAll(t TranType, fields ...Field) {
622 for _, c := range s.ClientMgr.List() {
623 s.outbox <- NewTransaction(t, c.ID, fields...)
624 }
625}
626
627func (s *Server) Shutdown(msg []byte) {
628 s.Logger.Info("Shutdown signal received")
629 s.SendAll(TranDisconnectMsg, NewField(FieldData, msg))
630
631 time.Sleep(3 * time.Second)
632
633 os.Exit(0)
634}