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