]> git.r.bdr.sh - rbdr/mobius/blame - hotline/server.go
Extensive refactor and clean up
[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"
7cd900d6 12 "gopkg.in/yaml.v3"
6988a057 13 "io"
a6216dd8
JH
14 "log"
15 "log/slog"
6988a057
JH
16 "net"
17 "os"
6988a057 18 "path/filepath"
6988a057
JH
19 "strings"
20 "sync"
21 "time"
6988a057
JH
22)
23
7cd900d6
JH
24type contextKey string
25
26var contextKeyReq = contextKey("req")
27
28type requestCtx struct {
29 remoteAddr string
7cd900d6
JH
30}
31
2e1aec0f
JH
32// Converts bytes from Mac Roman encoding to UTF-8
33var txtDecoder = charmap.Macintosh.NewDecoder()
34
35// Converts bytes from UTF-8 to Mac Roman encoding
36var txtEncoder = charmap.Macintosh.NewEncoder()
37
6988a057 38type Server struct {
a2ef262a
JH
39 NetInterface string
40 Port int
a2ef262a 41
d9bc63a1 42 Config Config
c1c44744 43 ConfigDir string
a6216dd8 44 Logger *slog.Logger
c1c44744 45
40414f92 46 TrackerPassID [4]byte
00913df3 47
d9bc63a1 48 Stats Counter
6988a057 49
7cd900d6 50 FS FileStore // Storage backend to use for File storage
6988a057
JH
51
52 outbox chan Transaction
53
d9bc63a1
JH
54 // TODO
55 Agreement []byte
56 banner []byte
57 // END TODO
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
d9bc63a1
JH
69// NewServer constructs a new Server from a config dir
70func 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 }
00913df3 120
d9bc63a1
JH
121 // Start Client Keepalive go routine
122 go server.keepaliveHandler()
00913df3 123
d9bc63a1 124 return &server, nil
00913df3
JH
125}
126
d9bc63a1
JH
127func (s *Server) CurrentStats() map[string]interface{} {
128 return s.Stats.Values()
6988a057
JH
129}
130
a2ef262a 131func (s *Server) ListenAndServe(ctx context.Context) error {
6988a057
JH
132 var wg sync.WaitGroup
133
134 wg.Add(1)
8168522a 135 go func() {
2d0f2abe 136 ln, err := net.Listen("tcp", fmt.Sprintf("%s:%v", s.NetInterface, s.Port))
8168522a 137 if err != nil {
a6216dd8 138 log.Fatal(err)
8168522a
JH
139 }
140
a6216dd8 141 log.Fatal(s.Serve(ctx, ln))
8168522a 142 }()
6988a057
JH
143
144 wg.Add(1)
8168522a 145 go func() {
2d0f2abe 146 ln, err := net.Listen("tcp", fmt.Sprintf("%s:%v", s.NetInterface, s.Port+1))
8168522a 147 if err != nil {
a6216dd8 148 log.Fatal(err)
8168522a
JH
149 }
150
a6216dd8 151 log.Fatal(s.ServeFileTransfers(ctx, ln))
8168522a 152 }()
6988a057
JH
153
154 wg.Wait()
155
156 return nil
157}
158
7cd900d6 159func (s *Server) ServeFileTransfers(ctx context.Context, ln net.Listener) error {
6988a057
JH
160 for {
161 conn, err := ln.Accept()
162 if err != nil {
163 return err
164 }
165
166 go func() {
7cd900d6
JH
167 defer func() { _ = conn.Close() }()
168
169 err = s.handleFileTransfer(
a2ef262a 170 context.WithValue(ctx, contextKeyReq, requestCtx{remoteAddr: conn.RemoteAddr().String()}),
7cd900d6
JH
171 conn,
172 )
173
174 if err != nil {
a6216dd8 175 s.Logger.Error("file transfer error", "reason", err)
6988a057
JH
176 }
177 }()
178 }
179}
180
181func (s *Server) sendTransaction(t Transaction) error {
d9bc63a1 182 client := s.ClientMgr.Get(t.clientID)
a2ef262a 183
d9bc63a1 184 if client == nil {
a2ef262a 185 return nil
6988a057 186 }
6988a057 187
a2ef262a 188 _, err := io.Copy(client.Connection, &t)
75e4191b 189 if err != nil {
a2ef262a 190 return fmt.Errorf("failed to send transaction to client %v: %v", t.clientID, err)
6988a057 191 }
3178ae58 192
6988a057
JH
193 return nil
194}
195
67db911d
JH
196func (s *Server) processOutbox() {
197 for {
198 t := <-s.outbox
199 go func() {
200 if err := s.sendTransaction(t); err != nil {
a6216dd8 201 s.Logger.Error("error sending transaction", "err", err)
67db911d
JH
202 }
203 }()
204 }
205}
206
7cd900d6 207func (s *Server) Serve(ctx context.Context, ln net.Listener) error {
67db911d
JH
208 go s.processOutbox()
209
6988a057
JH
210 for {
211 conn, err := ln.Accept()
212 if err != nil {
a6216dd8 213 s.Logger.Error("error accepting connection", "err", err)
6988a057 214 }
67db911d
JH
215 connCtx := context.WithValue(ctx, contextKeyReq, requestCtx{
216 remoteAddr: conn.RemoteAddr().String(),
217 })
6988a057
JH
218
219 go func() {
a6216dd8 220 s.Logger.Info("Connection established", "RemoteAddr", conn.RemoteAddr())
0da28a1f 221
46862572 222 defer conn.Close()
67db911d 223 if err := s.handleNewConnection(connCtx, conn, conn.RemoteAddr().String()); err != nil {
6988a057 224 if err == io.EOF {
a6216dd8 225 s.Logger.Info("Client disconnected", "RemoteAddr", conn.RemoteAddr())
6988a057 226 } else {
a6216dd8 227 s.Logger.Error("error serving request", "RemoteAddr", conn.RemoteAddr(), "err", err)
6988a057
JH
228 }
229 }
230 }()
231 }
232}
233
234const (
c7e932c0 235 agreementFile = "Agreement.txt"
6988a057
JH
236)
237
d9bc63a1
JH
238func (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)
6988a057 250 }
d9bc63a1 251 }
6988a057 252
d9bc63a1
JH
253 time.Sleep(trackerUpdateFrequency * time.Second)
254 }
6988a057
JH
255}
256
d9bc63a1 257// keepaliveHandler
6988a057
JH
258func (s *Server) keepaliveHandler() {
259 for {
260 time.Sleep(idleCheckInterval * time.Second)
6988a057 261
d9bc63a1
JH
262 for _, c := range s.ClientMgr.List() {
263 c.mu.Lock()
264
61c272e1 265 c.IdleTime += idleCheckInterval
6988a057 266
d9bc63a1
JH
267 // Check if the user
268 if c.IdleTime > userIdleSeconds && !c.Flags.IsSet(UserFlagAway) {
a2ef262a 269 c.Flags.Set(UserFlagAway, 1)
d9bc63a1
JH
270
271 c.SendAll(
d005ef04 272 TranNotifyChangeUser,
a2ef262a
JH
273 NewField(FieldUserID, c.ID[:]),
274 NewField(FieldUserFlags, c.Flags[:]),
d005ef04
JH
275 NewField(FieldUserName, c.UserName),
276 NewField(FieldUserIconID, c.Icon),
6988a057
JH
277 )
278 }
d9bc63a1 279 c.mu.Unlock()
6988a057 280 }
46862572 281 }
6988a057
JH
282}
283
67db911d 284func (s *Server) NewClientConn(conn io.ReadWriteCloser, remoteAddr string) *ClientConn {
6988a057 285 clientConn := &ClientConn{
a2ef262a 286 Icon: []byte{0, 0}, // TODO: make array type
6988a057
JH
287 Connection: conn,
288 Server: s,
d4c152a4 289 RemoteAddr: remoteAddr,
d2810ae9 290
d9bc63a1 291 ClientFileTransferMgr: NewClientFileTransferMgr(),
180d6544
JH
292 }
293
d9bc63a1 294 s.ClientMgr.Add(clientConn)
6988a057 295
d9bc63a1 296 return clientConn
6988a057
JH
297}
298
a2ef262a
JH
299// loadFromYAMLFile loads data from a YAML file into the provided data structure.
300func loadFromYAMLFile(path string, data interface{}) error {
46862572
JH
301 fh, err := os.Open(path)
302 if err != nil {
303 return err
304 }
a2ef262a 305 defer fh.Close()
46862572 306
6988a057 307 decoder := yaml.NewDecoder(fh)
a2ef262a 308 return decoder.Decode(data)
6988a057
JH
309}
310
a2ef262a
JH
311func 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
6988a057 322// handleNewConnection takes a new net.Conn and performs the initial login sequence
3178ae58 323func (s *Server) handleNewConnection(ctx context.Context, rwc io.ReadWriteCloser, remoteAddr string) error {
c4208f86
JH
324 defer dontPanic(s.Logger)
325
a2ef262a
JH
326 // Check if remoteAddr is present in the ban list
327 ipAddr := strings.Split(remoteAddr, ":")[0]
d9bc63a1 328 if isBanned, banUntil := s.BanList.IsBanned(ipAddr); isBanned {
a2ef262a
JH
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)
6988a057
JH
346 }
347
3178ae58
JH
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()
6988a057 353
f4cdaddc
JH
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
854a92fc 359 var clientLogin Transaction
f4cdaddc 360 if _, err := clientLogin.Write(buf); err != nil {
a2ef262a 361 return fmt.Errorf("error writing login transaction: %w", err)
46862572 362 }
e9c043c0
JH
363
364 c := s.NewClientConn(rwc, remoteAddr)
6988a057 365 defer c.Disconnect()
6988a057 366
d005ef04
JH
367 encodedPassword := clientLogin.GetField(FieldUserPassword).Data
368 c.Version = clientLogin.GetField(FieldVersion).Data
6988a057 369
d9bc63a1 370 login := clientLogin.GetField(FieldUserLogin).DecodeObfuscatedString()
6988a057
JH
371 if login == "" {
372 login = GuestAccount
373 }
374
d9bc63a1 375 c.logger = s.Logger.With("ip", ipAddr, "login", login)
0da28a1f 376
6988a057
JH
377 // If authentication fails, send error reply and close connection
378 if !c.Authenticate(login, encodedPassword) {
a2ef262a 379 t := c.NewErrReply(&clientLogin, "Incorrect login.")[0]
95159e55
JH
380
381 _, err := io.Copy(rwc, &t)
72dd37f1
JH
382 if err != nil {
383 return err
384 }
0da28a1f 385
a6216dd8 386 c.logger.Info("Login failed", "clientVersion", fmt.Sprintf("%x", c.Version))
0da28a1f
JH
387
388 return nil
6988a057
JH
389 }
390
d005ef04
JH
391 if clientLogin.GetField(FieldUserIconID).Data != nil {
392 c.Icon = clientLogin.GetField(FieldUserIconID).Data
59097464
JH
393 }
394
d9bc63a1
JH
395 c.Account = c.Server.AccountManager.Get(login)
396 if c.Account == nil {
397 return nil
398 }
59097464 399
d005ef04 400 if clientLogin.GetField(FieldUserName).Data != nil {
d9bc63a1 401 if c.Authorize(AccessAnyName) {
d005ef04 402 c.UserName = clientLogin.GetField(FieldUserName).Data
ea5d8c51
JH
403 } else {
404 c.UserName = []byte(c.Account.Name)
405 }
6988a057
JH
406 }
407
d9bc63a1 408 if c.Authorize(AccessDisconUser) {
a2ef262a 409 c.Flags.Set(UserFlagAdmin, 1)
6988a057
JH
410 }
411
854a92fc 412 s.outbox <- c.NewReply(&clientLogin,
d005ef04
JH
413 NewField(FieldVersion, []byte{0x00, 0xbe}),
414 NewField(FieldCommunityBannerID, []byte{0, 0}),
415 NewField(FieldServerName, []byte(s.Config.Name)),
6988a057
JH
416 )
417
418 // Send user access privs so client UI knows how to behave
a2ef262a 419 c.Server.outbox <- NewTransaction(TranUserAccess, c.ID, NewField(FieldUserAccess, c.Account.Access[:]))
6988a057 420
d9bc63a1 421 // Accounts with AccessNoAgreement do not receive the server agreement on login. The behavior is different between
d005ef04
JH
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.
d9bc63a1 424 if c.Authorize(AccessNoAgreement) {
a322be02
JH
425 // If client version is nil, then the client uses the 1.2.3 login behavior
426 if c.Version != nil {
a2ef262a 427 c.Server.outbox <- NewTransaction(TranShowAgreement, c.ID, NewField(FieldNoServerAgreement, []byte{1}))
a322be02 428 }
688c86d2 429 } else {
a2ef262a 430 c.Server.outbox <- NewTransaction(TranShowAgreement, c.ID, NewField(FieldData, s.Agreement))
688c86d2 431 }
6988a057 432
2f8472fa
JH
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
d005ef04 437 // part of TranAgreed
95159e55 438 c.logger = c.logger.With("Name", string(c.UserName))
a6216dd8 439 c.logger.Info("Login successful", "clientVersion", "Not sent (probably 1.2.3)")
2f8472fa
JH
440
441 // Notify other clients on the server that the new user has logged in. For 1.5+ clients we don't have this
d005ef04 442 // information yet, so we do it in TranAgreed instead
d9bc63a1 443 for _, t := range c.NotifyOthers(
a2ef262a
JH
444 NewTransaction(
445 TranNotifyChangeUser, [2]byte{0, 0},
d005ef04 446 NewField(FieldUserName, c.UserName),
a2ef262a 447 NewField(FieldUserID, c.ID[:]),
d005ef04 448 NewField(FieldUserIconID, c.Icon),
a2ef262a 449 NewField(FieldUserFlags, c.Flags[:]),
2f8472fa
JH
450 ),
451 ) {
452 c.Server.outbox <- t
453 }
6988a057 454 }
bd1ce113 455
d9bc63a1
JH
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()))
00913df3 461 }
6988a057 462
3178ae58
JH
463 // Scan for new transactions and handle them as they come in.
464 for scanner.Scan() {
a2ef262a 465 // Copy the scanner bytes to a new slice to it to avoid a data race when the scanner re-uses the buffer.
3178ae58
JH
466 buf := make([]byte, len(scanner.Bytes()))
467 copy(buf, scanner.Bytes())
6988a057 468
854a92fc
JH
469 var t Transaction
470 if _, err := t.Write(buf); err != nil {
471 return err
6988a057 472 }
854a92fc 473
a2ef262a 474 c.handleTransaction(t)
6988a057 475 }
3178ae58 476 return nil
6988a057
JH
477}
478
85767504 479// handleFileTransfer receives a client net.Conn from the file transfer server, performs the requested transfer type, then closes the connection
7cd900d6 480func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) error {
37a954c8 481 defer dontPanic(s.Logger)
0a92e50b 482
a2ef262a 483 // The first 16 bytes contain the file transfer.
df2735b2 484 var t transfer
a2ef262a
JH
485 if _, err := io.CopyN(&t, rwc, 16); err != nil {
486 return fmt.Errorf("error reading file transfer: %w", err)
6988a057
JH
487 }
488
d9bc63a1
JH
489 fileTransfer := s.FileTransferMgr.Get(t.ReferenceNumber)
490 if fileTransfer == nil {
491 return errors.New("invalid transaction ID")
492 }
493
0a92e50b 494 defer func() {
d9bc63a1 495 s.FileTransferMgr.Delete(t.ReferenceNumber)
df1ade54 496
94742e2f
JH
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)
0a92e50b 501 }()
6988a057 502
7cd900d6
JH
503 rLogger := s.Logger.With(
504 "remoteAddr", ctx.Value(contextKeyReq).(requestCtx).remoteAddr,
df1ade54 505 "login", fileTransfer.ClientConn.Account.Login,
95159e55 506 "Name", string(fileTransfer.ClientConn.UserName),
7cd900d6
JH
507 )
508
df1ade54
JH
509 fullPath, err := readPath(s.Config.FileRoot, fileTransfer.FilePath, fileTransfer.FileName)
510 if err != nil {
511 return err
512 }
513
6988a057 514 switch fileTransfer.Type {
d9bc63a1 515 case BannerDownload:
5cc444c8
JH
516 if _, err := io.Copy(rwc, bytes.NewBuffer(s.banner)); err != nil {
517 return fmt.Errorf("error sending banner: %w", err)
9067f234 518 }
6988a057 519 case FileDownload:
d9bc63a1 520 s.Stats.Increment(StatDownloadCounter, StatDownloadsInProgress)
94742e2f 521 defer func() {
d9bc63a1 522 s.Stats.Decrement(StatDownloadsInProgress)
94742e2f 523 }()
23411fc2 524
a2ef262a 525 err = DownloadHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, true)
6988a057 526 if err != nil {
d9bc63a1 527 return fmt.Errorf("file download: %w", err)
7cd900d6
JH
528 }
529
6988a057 530 case FileUpload:
d9bc63a1
JH
531 s.Stats.Increment(StatUploadCounter, StatUploadsInProgress)
532 defer func() {
533 s.Stats.Decrement(StatUploadsInProgress)
534 }()
23411fc2 535
a2ef262a 536 err = UploadHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, s.Config.PreserveResourceForks)
7cd900d6 537 if err != nil {
a2ef262a 538 return fmt.Errorf("file upload error: %w", err)
6988a057 539 }
85767504 540
6988a057 541 case FolderDownload:
d9bc63a1
JH
542 s.Stats.Increment(StatDownloadCounter, StatDownloadsInProgress)
543 defer func() {
544 s.Stats.Decrement(StatDownloadsInProgress)
545 }()
00913df3 546
a2ef262a 547 err = DownloadFolderHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, s.Config.PreserveResourceForks)
67db911d 548 if err != nil {
a2ef262a 549 return fmt.Errorf("file upload error: %w", err)
67db911d
JH
550 }
551
6988a057 552 case FolderUpload:
d9bc63a1
JH
553 s.Stats.Increment(StatUploadCounter, StatUploadsInProgress)
554 defer func() {
555 s.Stats.Decrement(StatUploadsInProgress)
556 }()
557
a6216dd8 558 rLogger.Info(
6988a057 559 "Folder upload started",
df1ade54
JH
560 "dstPath", fullPath,
561 "TransferSize", binary.BigEndian.Uint32(fileTransfer.TransferSize),
6988a057
JH
562 "FolderItemCount", fileTransfer.FolderItemCount,
563 )
564
a2ef262a
JH
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)
6988a057 568 }
6988a057 569 }
6988a057
JH
570 return nil
571}