11 "golang.org/x/text/encoding/charmap"
12 "golang.org/x/time/rate"
23 type contextKey string
25 var contextKeyReq = contextKey("req")
27 type requestCtx struct {
31 // Converts bytes from Mac Roman encoding to UTF-8
32 var txtDecoder = charmap.Macintosh.NewDecoder()
34 // Converts bytes from UTF-8 to Mac Roman encoding
35 var txtEncoder = charmap.Macintosh.NewEncoder()
41 rateLimiters map[string]*rate.Limiter
43 handlers map[TranType]HandlerFunc
52 FS FileStore // Storage backend to use for File storage
54 outbox chan Transaction
56 Agreement io.ReadSeeker
59 FileTransferMgr FileTransferMgr
61 ClientMgr ClientManager
62 AccountManager AccountManager
63 ThreadedNewsMgr ThreadedNewsMgr
66 MessageBoard io.ReadWriteSeeker
69 type Option = func(s *Server)
71 func WithConfig(config Config) func(s *Server) {
72 return func(s *Server) {
77 func WithLogger(logger *slog.Logger) func(s *Server) {
78 return func(s *Server) {
83 // WithPort optionally overrides the default TCP port.
84 func WithPort(port int) func(s *Server) {
85 return func(s *Server) {
90 // WithInterface optionally sets a specific interface to listen on.
91 func WithInterface(netInterface string) func(s *Server) {
92 return func(s *Server) {
93 s.NetInterface = netInterface
97 type ServerConfig struct {
100 func NewServer(options ...Option) (*Server, error) {
102 handlers: make(map[TranType]HandlerFunc),
103 outbox: make(chan Transaction),
104 rateLimiters: make(map[string]*rate.Limiter),
106 ChatMgr: NewMemChatManager(),
107 ClientMgr: NewMemClientMgr(),
108 FileTransferMgr: NewMemFileTransferMgr(),
112 for _, opt := range options {
116 // generate a new random passID for tracker registration
117 _, err := rand.Read(server.TrackerPassID[:])
125 func (s *Server) CurrentStats() map[string]interface{} {
126 return s.Stats.Values()
129 func (s *Server) ListenAndServe(ctx context.Context) error {
130 go s.registerWithTrackers(ctx)
131 go s.keepaliveHandler(ctx)
134 var wg sync.WaitGroup
138 ln, err := net.Listen("tcp", fmt.Sprintf("%s:%v", s.NetInterface, s.Port))
143 log.Fatal(s.Serve(ctx, ln))
148 ln, err := net.Listen("tcp", fmt.Sprintf("%s:%v", s.NetInterface, s.Port+1))
153 log.Fatal(s.ServeFileTransfers(ctx, ln))
161 func (s *Server) ServeFileTransfers(ctx context.Context, ln net.Listener) error {
163 conn, err := ln.Accept()
169 defer func() { _ = conn.Close() }()
171 err = s.handleFileTransfer(
172 context.WithValue(ctx, contextKeyReq, requestCtx{remoteAddr: conn.RemoteAddr().String()}),
177 s.Logger.Error("file transfer error", "err", err)
183 func (s *Server) sendTransaction(t Transaction) error {
184 client := s.ClientMgr.Get(t.ClientID)
190 _, err := io.Copy(client.Connection, &t)
192 return fmt.Errorf("failed to send transaction to client %v: %v", t.ClientID, err)
198 func (s *Server) processOutbox() {
202 if err := s.sendTransaction(t); err != nil {
203 s.Logger.Error("error sending transaction", "err", err)
209 // perIPRateLimit controls how frequently an IP address can connect before being throttled.
210 // 0.5 = 1 connection every 2 seconds
211 const perIPRateLimit = rate.Limit(0.5)
213 func (s *Server) Serve(ctx context.Context, ln net.Listener) error {
217 s.Logger.Info("Server shutting down")
220 conn, err := ln.Accept()
222 s.Logger.Error("Error accepting connection", "err", err)
227 ipAddr := strings.Split(conn.RemoteAddr().(*net.TCPAddr).String(), ":")[0]
229 connCtx := context.WithValue(ctx, contextKeyReq, requestCtx{
230 remoteAddr: conn.RemoteAddr().String(),
233 s.Logger.Info("Connection established", "ip", ipAddr)
236 // Check if we have an existing rate limit for the IP and create one if we do not.
237 rl, ok := s.rateLimiters[ipAddr]
239 rl = rate.NewLimiter(perIPRateLimit, 1)
240 s.rateLimiters[ipAddr] = rl
243 // Check if the rate limit is exceeded and close the connection if so.
245 s.Logger.Info("Rate limit exceeded", "RemoteAddr", conn.RemoteAddr())
250 if err := s.handleNewConnection(connCtx, conn, conn.RemoteAddr().String()); err != nil {
252 s.Logger.Info("Client disconnected", "RemoteAddr", conn.RemoteAddr())
254 s.Logger.Error("Error serving request", "RemoteAddr", conn.RemoteAddr(), "err", err)
262 // time in seconds between tracker re-registration
263 const trackerUpdateFrequency = 300
265 // registerWithTrackers runs every trackerUpdateFrequency seconds to update the server's tracker entry on all configured
267 func (s *Server) registerWithTrackers(ctx context.Context) {
268 if s.Config.EnableTrackerRegistration {
269 s.Logger.Info("Tracker registration enabled", "trackers", s.Config.Trackers)
273 if s.Config.EnableTrackerRegistration {
274 for _, t := range s.Config.Trackers {
275 tr := &TrackerRegistration{
276 UserCount: len(s.ClientMgr.List()),
277 PassID: s.TrackerPassID,
279 Description: s.Config.Description,
281 binary.BigEndian.PutUint16(tr.Port[:], uint16(s.Port))
283 // Check the tracker string for a password. This is janky but avoids a breaking change to the Config
285 splitAddr := strings.Split(":", t)
286 if len(splitAddr) == 3 {
287 tr.Password = splitAddr[2]
290 if err := register(&RealDialer{}, t, tr); err != nil {
291 s.Logger.Error(fmt.Sprintf("Unable to register with tracker %v", t), "error", err)
295 // Using time.Ticker with for/select would be more idiomatic, but it's super annoying that it doesn't tick on
296 // first pass. Revist, maybe.
297 // https://github.com/golang/go/issues/17601
298 time.Sleep(trackerUpdateFrequency * time.Second)
303 userIdleSeconds = 300 // time in seconds before an inactive user is marked idle
304 idleCheckInterval = 10 // time in seconds to check for idle users
307 // keepaliveHandler runs every idleCheckInterval seconds and increments a user's idle time by idleCheckInterval seconds.
308 // If the updated idle time exceeds userIdleSeconds and the user was not previously idle, we notify all connected clients
309 // that the user has gone idle. For most clients, this turns the user grey in the user list.
310 func (s *Server) keepaliveHandler(ctx context.Context) {
311 ticker := time.NewTicker(idleCheckInterval * time.Second)
319 for _, c := range s.ClientMgr.List() {
321 c.IdleTime += idleCheckInterval
324 if c.IdleTime > userIdleSeconds && !c.Flags.IsSet(UserFlagAway) {
325 c.Flags.Set(UserFlagAway, 1)
328 TranNotifyChangeUser,
329 NewField(FieldUserID, c.ID[:]),
330 NewField(FieldUserFlags, c.Flags[:]),
331 NewField(FieldUserName, c.UserName),
332 NewField(FieldUserIconID, c.Icon),
341 func (s *Server) NewClientConn(conn io.ReadWriteCloser, remoteAddr string) *ClientConn {
342 clientConn := &ClientConn{
343 Icon: []byte{0, 0}, // TODO: make array type
346 RemoteAddr: remoteAddr,
348 ClientFileTransferMgr: NewClientFileTransferMgr(),
351 s.ClientMgr.Add(clientConn)
356 func sendBanMessage(rwc io.Writer, message string) {
360 NewField(FieldData, []byte(message)),
361 NewField(FieldChatOptions, []byte{0, 0}),
363 _, _ = io.Copy(rwc, &t)
364 time.Sleep(1 * time.Second)
367 // handleNewConnection takes a new net.Conn and performs the initial login sequence
368 func (s *Server) handleNewConnection(ctx context.Context, rwc io.ReadWriteCloser, remoteAddr string) error {
369 defer dontPanic(s.Logger)
371 if err := performHandshake(rwc); err != nil {
372 return fmt.Errorf("perform handshake: %w", err)
375 // Check if remoteAddr is present in the ban list
376 ipAddr := strings.Split(remoteAddr, ":")[0]
377 if isBanned, banUntil := s.BanList.IsBanned(ipAddr); isBanned {
380 sendBanMessage(rwc, "You are permanently banned on this server")
381 s.Logger.Debug("Disconnecting permanently banned IP", "remoteAddr", ipAddr)
386 if time.Now().Before(*banUntil) {
387 sendBanMessage(rwc, "You are temporarily banned on this server")
388 s.Logger.Debug("Disconnecting temporarily banned IP", "remoteAddr", ipAddr)
393 // Create a new scanner for parsing incoming bytes into transaction tokens
394 scanner := bufio.NewScanner(rwc)
395 scanner.Split(transactionScanner)
399 // Make a new []byte slice and copy the scanner bytes to it. This is critical to avoid a data race as the
400 // scanner re-uses the buffer for subsequent scans.
401 buf := make([]byte, len(scanner.Bytes()))
402 copy(buf, scanner.Bytes())
404 var clientLogin Transaction
405 if _, err := clientLogin.Write(buf); err != nil {
406 return fmt.Errorf("error writing login transaction: %w", err)
409 c := s.NewClientConn(rwc, remoteAddr)
412 encodedPassword := clientLogin.GetField(FieldUserPassword).Data
413 c.Version = clientLogin.GetField(FieldVersion).Data
415 login := clientLogin.GetField(FieldUserLogin).DecodeObfuscatedString()
420 c.Logger = s.Logger.With("ip", ipAddr, "login", login)
422 // If authentication fails, send error reply and close connection
423 if !c.Authenticate(login, encodedPassword) {
424 t := c.NewErrReply(&clientLogin, "Incorrect login.")[0]
426 _, err := io.Copy(rwc, &t)
431 c.Logger.Info("Incorrect login")
436 if clientLogin.GetField(FieldUserIconID).Data != nil {
437 c.Icon = clientLogin.GetField(FieldUserIconID).Data
440 c.Account = c.Server.AccountManager.Get(login)
441 if c.Account == nil {
445 if clientLogin.GetField(FieldUserName).Data != nil {
446 if c.Authorize(AccessAnyName) {
447 c.UserName = clientLogin.GetField(FieldUserName).Data
449 c.UserName = []byte(c.Account.Name)
453 if c.Authorize(AccessDisconUser) {
454 c.Flags.Set(UserFlagAdmin, 1)
457 s.outbox <- c.NewReply(&clientLogin,
458 NewField(FieldVersion, []byte{0x00, 0xbe}),
459 NewField(FieldCommunityBannerID, []byte{0, 0}),
460 NewField(FieldServerName, []byte(s.Config.Name)),
463 // Send user access privs so client UI knows how to behave
464 c.Server.outbox <- NewTransaction(TranUserAccess, c.ID, NewField(FieldUserAccess, c.Account.Access[:]))
466 // Accounts with AccessNoAgreement do not receive the server agreement on login. The behavior is different between
467 // client versions. For 1.2.3 client, we do not send TranShowAgreement. For other client versions, we send
468 // TranShowAgreement but with the NoServerAgreement field set to 1.
469 if c.Authorize(AccessNoAgreement) {
470 // If client version is nil, then the client uses the 1.2.3 login behavior
471 if c.Version != nil {
472 c.Server.outbox <- NewTransaction(TranShowAgreement, c.ID, NewField(FieldNoServerAgreement, []byte{1}))
475 _, _ = c.Server.Agreement.Seek(0, 0)
476 data, _ := io.ReadAll(c.Server.Agreement)
478 c.Server.outbox <- NewTransaction(TranShowAgreement, c.ID, NewField(FieldData, data))
481 // If the client has provided a username as part of the login, we can infer that it is using the 1.2.3 login
482 // flow and not the 1.5+ flow.
483 if len(c.UserName) != 0 {
484 // Add the client username to the logger. For 1.5+ clients, we don't have this information yet as it comes as
485 // part of TranAgreed
486 c.Logger = c.Logger.With("name", string(c.UserName))
487 c.Logger.Info("Login successful")
489 // Notify other clients on the server that the new user has logged in. For 1.5+ clients we don't have this
490 // information yet, so we do it in TranAgreed instead
491 for _, t := range c.NotifyOthers(
493 TranNotifyChangeUser, [2]byte{0, 0},
494 NewField(FieldUserName, c.UserName),
495 NewField(FieldUserID, c.ID[:]),
496 NewField(FieldUserIconID, c.Icon),
497 NewField(FieldUserFlags, c.Flags[:]),
504 c.Server.Stats.Increment(StatConnectionCounter, StatCurrentlyConnected)
505 defer c.Server.Stats.Decrement(StatCurrentlyConnected)
507 if len(s.ClientMgr.List()) > c.Server.Stats.Get(StatConnectionPeak) {
508 c.Server.Stats.Set(StatConnectionPeak, len(s.ClientMgr.List()))
511 // Scan for new transactions and handle them as they come in.
513 // Copy the scanner bytes to a new slice to it to avoid a data race when the scanner re-uses the buffer.
514 tmpBuf := make([]byte, len(scanner.Bytes()))
515 copy(tmpBuf, scanner.Bytes())
518 if _, err := t.Write(tmpBuf); err != nil {
522 c.handleTransaction(t)
527 // handleFileTransfer receives a client net.Conn from the file transfer server, performs the requested transfer type, then closes the connection
528 func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) error {
529 defer dontPanic(s.Logger)
531 // The first 16 bytes contain the file transfer.
533 if _, err := io.CopyN(&t, rwc, 16); err != nil {
534 return fmt.Errorf("error reading file transfer: %w", err)
537 fileTransfer := s.FileTransferMgr.Get(t.ReferenceNumber)
538 if fileTransfer == nil {
539 return errors.New("invalid transaction ID")
543 s.FileTransferMgr.Delete(t.ReferenceNumber)
545 // Wait a few seconds before closing the connection: this is a workaround for problems
546 // observed with Windows clients where the client must initiate close of the TCP connection before
547 // the server does. This is gross and seems unnecessary. TODO: Revisit?
548 time.Sleep(3 * time.Second)
551 rLogger := s.Logger.With(
552 "remoteAddr", ctx.Value(contextKeyReq).(requestCtx).remoteAddr,
553 "login", fileTransfer.ClientConn.Account.Login,
554 "Name", string(fileTransfer.ClientConn.UserName),
557 fullPath, err := ReadPath(fileTransfer.FileRoot, fileTransfer.FilePath, fileTransfer.FileName)
562 switch fileTransfer.Type {
564 if _, err := io.Copy(rwc, bytes.NewBuffer(s.Banner)); err != nil {
565 return fmt.Errorf("banner download: %w", err)
568 s.Stats.Increment(StatDownloadCounter, StatDownloadsInProgress)
570 s.Stats.Decrement(StatDownloadsInProgress)
573 err = DownloadHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, true)
575 return fmt.Errorf("file download: %w", err)
579 s.Stats.Increment(StatUploadCounter, StatUploadsInProgress)
581 s.Stats.Decrement(StatUploadsInProgress)
584 err = UploadHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, s.Config.PreserveResourceForks)
586 return fmt.Errorf("file upload: %w", err)
590 s.Stats.Increment(StatDownloadCounter, StatDownloadsInProgress)
592 s.Stats.Decrement(StatDownloadsInProgress)
595 err = DownloadFolderHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, s.Config.PreserveResourceForks)
597 return fmt.Errorf("folder download: %w", err)
601 s.Stats.Increment(StatUploadCounter, StatUploadsInProgress)
603 s.Stats.Decrement(StatUploadsInProgress)
606 var transferSizeValue uint32
607 switch len(fileTransfer.TransferSize) {
609 transferSizeValue = uint32(binary.BigEndian.Uint16(fileTransfer.TransferSize))
611 transferSizeValue = binary.BigEndian.Uint32(fileTransfer.TransferSize)
613 rLogger.Warn("Unexpected TransferSize length: %d bytes", len(fileTransfer.TransferSize))
617 "Folder upload started",
619 "TransferSize", transferSizeValue,
620 "FolderItemCount", fileTransfer.FolderItemCount,
623 err = UploadFolderHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, s.Config.PreserveResourceForks)
625 return fmt.Errorf("folder upload: %w", err)
631 func (s *Server) SendAll(t TranType, fields ...Field) {
632 for _, c := range s.ClientMgr.List() {
633 s.outbox <- NewTransaction(t, c.ID, fields...)
637 func (s *Server) Shutdown(msg []byte) {
638 s.Logger.Info("Shutdown signal received")
639 s.SendAll(TranDisconnectMsg, NewField(FieldData, msg))
641 time.Sleep(3 * time.Second)