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