]> git.r.bdr.sh - rbdr/mobius/blame_incremental - hotline/server.go
Add option for account-specific file root
[rbdr/mobius] / hotline / server.go
... / ...
CommitLineData
1package hotline
2
3import (
4 "bufio"
5 "bytes"
6 "context"
7 "crypto/rand"
8 "encoding/binary"
9 "errors"
10 "fmt"
11 "golang.org/x/text/encoding/charmap"
12 "io"
13 "log"
14 "log/slog"
15 "net"
16 "os"
17 "strings"
18 "sync"
19 "time"
20)
21
22type contextKey string
23
24var contextKeyReq = contextKey("req")
25
26type requestCtx struct {
27 remoteAddr string
28}
29
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
36type Server struct {
37 NetInterface string
38 Port int
39
40 handlers map[TranType]HandlerFunc
41
42 Config Config
43 Logger *slog.Logger
44
45 TrackerPassID [4]byte
46
47 Stats Counter
48
49 FS FileStore // Storage backend to use for File storage
50
51 outbox chan Transaction
52
53 Agreement io.ReadSeeker
54 Banner []byte
55
56 FileTransferMgr FileTransferMgr
57 ChatMgr ChatManager
58 ClientMgr ClientManager
59 AccountManager AccountManager
60 ThreadedNewsMgr ThreadedNewsMgr
61 BanList BanMgr
62
63 MessageBoard io.ReadWriteSeeker
64}
65
66type Option = func(s *Server)
67
68func WithConfig(config Config) func(s *Server) {
69 return func(s *Server) {
70 s.Config = config
71 }
72}
73
74func WithLogger(logger *slog.Logger) func(s *Server) {
75 return func(s *Server) {
76 s.Logger = logger
77 }
78}
79
80// WithPort optionally overrides the default TCP port.
81func WithPort(port int) func(s *Server) {
82 return func(s *Server) {
83 s.Port = port
84 }
85}
86
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
91 }
92}
93
94type ServerConfig struct {
95}
96
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 }
107
108 for _, opt := range options {
109 opt(&server)
110 }
111
112 // generate a new random passID for tracker registration
113 _, err := rand.Read(server.TrackerPassID[:])
114 if err != nil {
115 return nil, err
116 }
117
118 return &server, nil
119}
120
121func (s *Server) CurrentStats() map[string]interface{} {
122 return s.Stats.Values()
123}
124
125func (s *Server) ListenAndServe(ctx context.Context) error {
126 go s.registerWithTrackers(ctx)
127 go s.keepaliveHandler(ctx)
128 go s.processOutbox()
129
130 var wg sync.WaitGroup
131
132 wg.Add(1)
133 go func() {
134 ln, err := net.Listen("tcp", fmt.Sprintf("%s:%v", s.NetInterface, s.Port))
135 if err != nil {
136 log.Fatal(err)
137 }
138
139 log.Fatal(s.Serve(ctx, ln))
140 }()
141
142 wg.Add(1)
143 go func() {
144 ln, err := net.Listen("tcp", fmt.Sprintf("%s:%v", s.NetInterface, s.Port+1))
145 if err != nil {
146 log.Fatal(err)
147 }
148
149 log.Fatal(s.ServeFileTransfers(ctx, ln))
150 }()
151
152 wg.Wait()
153
154 return nil
155}
156
157func (s *Server) ServeFileTransfers(ctx context.Context, ln net.Listener) error {
158 for {
159 conn, err := ln.Accept()
160 if err != nil {
161 return err
162 }
163
164 go func() {
165 defer func() { _ = conn.Close() }()
166
167 err = s.handleFileTransfer(
168 context.WithValue(ctx, contextKeyReq, requestCtx{remoteAddr: conn.RemoteAddr().String()}),
169 conn,
170 )
171
172 if err != nil {
173 s.Logger.Error("file transfer error", "reason", err)
174 }
175 }()
176 }
177}
178
179func (s *Server) sendTransaction(t Transaction) error {
180 client := s.ClientMgr.Get(t.ClientID)
181
182 if client == nil {
183 return nil
184 }
185
186 _, err := io.Copy(client.Connection, &t)
187 if err != nil {
188 return fmt.Errorf("failed to send transaction to client %v: %v", t.ClientID, err)
189 }
190
191 return nil
192}
193
194func (s *Server) processOutbox() {
195 for {
196 t := <-s.outbox
197 go func() {
198 if err := s.sendTransaction(t); err != nil {
199 s.Logger.Error("error sending transaction", "err", err)
200 }
201 }()
202 }
203}
204
205func (s *Server) Serve(ctx context.Context, ln net.Listener) error {
206 for {
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 }
217
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 }
232 }
233 }()
234 }
235 }
236}
237
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) {
244 for {
245 if s.Config.EnableTrackerRegistration {
246 for _, t := range s.Config.Trackers {
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
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)
264 }
265 }
266 }
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)
271 }
272}
273
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)
278
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()
285
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()
308 }
309 }
310 }
311}
312
313func (s *Server) NewClientConn(conn io.ReadWriteCloser, remoteAddr string) *ClientConn {
314 clientConn := &ClientConn{
315 Icon: []byte{0, 0}, // TODO: make array type
316 Connection: conn,
317 Server: s,
318 RemoteAddr: remoteAddr,
319
320 ClientFileTransferMgr: NewClientFileTransferMgr(),
321 }
322
323 s.ClientMgr.Add(clientConn)
324
325 return clientConn
326}
327
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
339// handleNewConnection takes a new net.Conn and performs the initial login sequence
340func (s *Server) handleNewConnection(ctx context.Context, rwc io.ReadWriteCloser, remoteAddr string) error {
341 defer dontPanic(s.Logger)
342
343 if err := performHandshake(rwc); err != nil {
344 return fmt.Errorf("perform handshake: %w", err)
345 }
346
347 // Check if remoteAddr is present in the ban list
348 ipAddr := strings.Split(remoteAddr, ":")[0]
349 if isBanned, banUntil := s.BanList.IsBanned(ipAddr); isBanned {
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
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()
370
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
376 var clientLogin Transaction
377 if _, err := clientLogin.Write(buf); err != nil {
378 return fmt.Errorf("error writing login transaction: %w", err)
379 }
380
381 c := s.NewClientConn(rwc, remoteAddr)
382 defer c.Disconnect()
383
384 encodedPassword := clientLogin.GetField(FieldUserPassword).Data
385 c.Version = clientLogin.GetField(FieldVersion).Data
386
387 login := clientLogin.GetField(FieldUserLogin).DecodeObfuscatedString()
388 if login == "" {
389 login = GuestAccount
390 }
391
392 c.Logger = s.Logger.With("ip", ipAddr, "login", login)
393
394 // If authentication fails, send error reply and close connection
395 if !c.Authenticate(login, encodedPassword) {
396 t := c.NewErrReply(&clientLogin, "Incorrect login.")[0]
397
398 _, err := io.Copy(rwc, &t)
399 if err != nil {
400 return err
401 }
402
403 c.Logger.Info("Login failed", "clientVersion", fmt.Sprintf("%x", c.Version))
404
405 return nil
406 }
407
408 if clientLogin.GetField(FieldUserIconID).Data != nil {
409 c.Icon = clientLogin.GetField(FieldUserIconID).Data
410 }
411
412 c.Account = c.Server.AccountManager.Get(login)
413 if c.Account == nil {
414 return nil
415 }
416
417 if clientLogin.GetField(FieldUserName).Data != nil {
418 if c.Authorize(AccessAnyName) {
419 c.UserName = clientLogin.GetField(FieldUserName).Data
420 } else {
421 c.UserName = []byte(c.Account.Name)
422 }
423 }
424
425 if c.Authorize(AccessDisconUser) {
426 c.Flags.Set(UserFlagAdmin, 1)
427 }
428
429 s.outbox <- c.NewReply(&clientLogin,
430 NewField(FieldVersion, []byte{0x00, 0xbe}),
431 NewField(FieldCommunityBannerID, []byte{0, 0}),
432 NewField(FieldServerName, []byte(s.Config.Name)),
433 )
434
435 // Send user access privs so client UI knows how to behave
436 c.Server.outbox <- NewTransaction(TranUserAccess, c.ID, NewField(FieldUserAccess, c.Account.Access[:]))
437
438 // Accounts with AccessNoAgreement do not receive the server agreement on login. The behavior is different between
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.
441 if c.Authorize(AccessNoAgreement) {
442 // If client version is nil, then the client uses the 1.2.3 login behavior
443 if c.Version != nil {
444 c.Server.outbox <- NewTransaction(TranShowAgreement, c.ID, NewField(FieldNoServerAgreement, []byte{1}))
445 }
446 } else {
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))
451 }
452
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
457 // part of TranAgreed
458 c.Logger = c.Logger.With("name", string(c.UserName))
459 c.Logger.Info("Login successful")
460
461 // Notify other clients on the server that the new user has logged in. For 1.5+ clients we don't have this
462 // information yet, so we do it in TranAgreed instead
463 for _, t := range c.NotifyOthers(
464 NewTransaction(
465 TranNotifyChangeUser, [2]byte{0, 0},
466 NewField(FieldUserName, c.UserName),
467 NewField(FieldUserID, c.ID[:]),
468 NewField(FieldUserIconID, c.Icon),
469 NewField(FieldUserFlags, c.Flags[:]),
470 ),
471 ) {
472 c.Server.outbox <- t
473 }
474 }
475
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()))
481 }
482
483 // Scan for new transactions and handle them as they come in.
484 for scanner.Scan() {
485 // Copy the scanner bytes to a new slice to it to avoid a data race when the scanner re-uses the buffer.
486 tmpBuf := make([]byte, len(scanner.Bytes()))
487 copy(tmpBuf, scanner.Bytes())
488
489 var t Transaction
490 if _, err := t.Write(tmpBuf); err != nil {
491 return err
492 }
493
494 c.handleTransaction(t)
495 }
496 return nil
497}
498
499// handleFileTransfer receives a client net.Conn from the file transfer server, performs the requested transfer type, then closes the connection
500func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) error {
501 defer dontPanic(s.Logger)
502
503 // The first 16 bytes contain the file transfer.
504 var t transfer
505 if _, err := io.CopyN(&t, rwc, 16); err != nil {
506 return fmt.Errorf("error reading file transfer: %w", err)
507 }
508
509 fileTransfer := s.FileTransferMgr.Get(t.ReferenceNumber)
510 if fileTransfer == nil {
511 return errors.New("invalid transaction ID")
512 }
513
514 defer func() {
515 s.FileTransferMgr.Delete(t.ReferenceNumber)
516
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)
521 }()
522
523 rLogger := s.Logger.With(
524 "remoteAddr", ctx.Value(contextKeyReq).(requestCtx).remoteAddr,
525 "login", fileTransfer.ClientConn.Account.Login,
526 "Name", string(fileTransfer.ClientConn.UserName),
527 )
528
529 fullPath, err := ReadPath(fileTransfer.FileRoot, fileTransfer.FilePath, fileTransfer.FileName)
530 if err != nil {
531 return err
532 }
533
534 switch fileTransfer.Type {
535 case BannerDownload:
536 if _, err := io.Copy(rwc, bytes.NewBuffer(s.Banner)); err != nil {
537 return fmt.Errorf("banner download: %w", err)
538 }
539 case FileDownload:
540 s.Stats.Increment(StatDownloadCounter, StatDownloadsInProgress)
541 defer func() {
542 s.Stats.Decrement(StatDownloadsInProgress)
543 }()
544
545 err = DownloadHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, true)
546 if err != nil {
547 return fmt.Errorf("file download: %w", err)
548 }
549
550 case FileUpload:
551 s.Stats.Increment(StatUploadCounter, StatUploadsInProgress)
552 defer func() {
553 s.Stats.Decrement(StatUploadsInProgress)
554 }()
555
556 err = UploadHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, s.Config.PreserveResourceForks)
557 if err != nil {
558 return fmt.Errorf("file upload: %w", err)
559 }
560
561 case FolderDownload:
562 s.Stats.Increment(StatDownloadCounter, StatDownloadsInProgress)
563 defer func() {
564 s.Stats.Decrement(StatDownloadsInProgress)
565 }()
566
567 err = DownloadFolderHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, s.Config.PreserveResourceForks)
568 if err != nil {
569 return fmt.Errorf("folder download: %w", err)
570 }
571
572 case FolderUpload:
573 s.Stats.Increment(StatUploadCounter, StatUploadsInProgress)
574 defer func() {
575 s.Stats.Decrement(StatUploadsInProgress)
576 }()
577
578 rLogger.Info(
579 "Folder upload started",
580 "dstPath", fullPath,
581 "TransferSize", binary.BigEndian.Uint32(fileTransfer.TransferSize),
582 "FolderItemCount", fileTransfer.FolderItemCount,
583 )
584
585 err = UploadFolderHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, s.Config.PreserveResourceForks)
586 if err != nil {
587 return fmt.Errorf("folder upload: %w", err)
588 }
589 }
590 return nil
591}
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}