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