11 "golang.org/x/text/encoding/charmap"
22 type contextKey string
24 var contextKeyReq = contextKey("req")
26 type requestCtx struct {
30 // Converts bytes from Mac Roman encoding to UTF-8
31 var txtDecoder = charmap.Macintosh.NewDecoder()
33 // Converts bytes from UTF-8 to Mac Roman encoding
34 var txtEncoder = charmap.Macintosh.NewEncoder()
40 handlers map[TranType]HandlerFunc
49 FS FileStore // Storage backend to use for File storage
51 outbox chan Transaction
53 Agreement io.ReadSeeker
56 FileTransferMgr FileTransferMgr
58 ClientMgr ClientManager
59 AccountManager AccountManager
60 ThreadedNewsMgr ThreadedNewsMgr
63 MessageBoard io.ReadWriteSeeker
66 type Option = func(s *Server)
68 func WithConfig(config Config) func(s *Server) {
69 return func(s *Server) {
74 func WithLogger(logger *slog.Logger) func(s *Server) {
75 return func(s *Server) {
80 // WithPort optionally overrides the default TCP port.
81 func WithPort(port int) func(s *Server) {
82 return func(s *Server) {
87 // WithInterface optionally sets a specific interface to listen on.
88 func WithInterface(netInterface string) func(s *Server) {
89 return func(s *Server) {
90 s.NetInterface = netInterface
94 type ServerConfig struct {
97 func NewServer(options ...Option) (*Server, error) {
99 handlers: make(map[TranType]HandlerFunc),
100 outbox: make(chan Transaction),
102 ChatMgr: NewMemChatManager(),
103 ClientMgr: NewMemClientMgr(),
104 FileTransferMgr: NewMemFileTransferMgr(),
108 for _, opt := range options {
112 // generate a new random passID for tracker registration
113 _, err := rand.Read(server.TrackerPassID[:])
121 func (s *Server) CurrentStats() map[string]interface{} {
122 return s.Stats.Values()
125 func (s *Server) ListenAndServe(ctx context.Context) error {
126 go s.registerWithTrackers(ctx)
127 go s.keepaliveHandler(ctx)
130 var wg sync.WaitGroup
134 ln, err := net.Listen("tcp", fmt.Sprintf("%s:%v", s.NetInterface, s.Port))
139 log.Fatal(s.Serve(ctx, ln))
144 ln, err := net.Listen("tcp", fmt.Sprintf("%s:%v", s.NetInterface, s.Port+1))
149 log.Fatal(s.ServeFileTransfers(ctx, ln))
157 func (s *Server) ServeFileTransfers(ctx context.Context, ln net.Listener) error {
159 conn, err := ln.Accept()
165 defer func() { _ = conn.Close() }()
167 err = s.handleFileTransfer(
168 context.WithValue(ctx, contextKeyReq, requestCtx{remoteAddr: conn.RemoteAddr().String()}),
173 s.Logger.Error("file transfer error", "reason", err)
179 func (s *Server) sendTransaction(t Transaction) error {
180 client := s.ClientMgr.Get(t.ClientID)
186 _, err := io.Copy(client.Connection, &t)
188 return fmt.Errorf("failed to send transaction to client %v: %v", t.ClientID, err)
194 func (s *Server) processOutbox() {
198 if err := s.sendTransaction(t); err != nil {
199 s.Logger.Error("error sending transaction", "err", err)
205 func (s *Server) Serve(ctx context.Context, ln net.Listener) error {
209 s.Logger.Info("Server shutting down")
212 conn, err := ln.Accept()
214 s.Logger.Error("Error accepting connection", "err", err)
219 connCtx := context.WithValue(ctx, contextKeyReq, requestCtx{
220 remoteAddr: conn.RemoteAddr().String(),
223 s.Logger.Info("Connection established", "addr", conn.RemoteAddr())
226 if err := s.handleNewConnection(connCtx, conn, conn.RemoteAddr().String()); err != nil {
228 s.Logger.Info("Client disconnected", "RemoteAddr", conn.RemoteAddr())
230 s.Logger.Error("Error serving request", "RemoteAddr", conn.RemoteAddr(), "err", err)
238 // time in seconds between tracker re-registration
239 const trackerUpdateFrequency = 300
241 // registerWithTrackers runs every trackerUpdateFrequency seconds to update the server's tracker entry on all configured
243 func (s *Server) registerWithTrackers(ctx context.Context) {
244 ticker := time.NewTicker(trackerUpdateFrequency * time.Second)
252 if s.Config.EnableTrackerRegistration {
253 tr := &TrackerRegistration{
254 UserCount: len(s.ClientMgr.List()),
255 PassID: s.TrackerPassID,
257 Description: s.Config.Description,
259 binary.BigEndian.PutUint16(tr.Port[:], uint16(s.Port))
261 for _, t := range s.Config.Trackers {
262 if err := register(&RealDialer{}, t, tr); err != nil {
263 s.Logger.Error(fmt.Sprintf("Unable to register with tracker %v", t), "error", err)
273 userIdleSeconds = 300 // time in seconds before an inactive user is marked idle
274 idleCheckInterval = 10 // time in seconds to check for idle users
277 // keepaliveHandler runs every idleCheckInterval seconds and increments a user's idle time by idleCheckInterval seconds.
278 // If the updated idle time exceeds userIdleSeconds and the user was not previously idle, we notify all connected clients
279 // that the user has gone idle. For most clients, this turns the user grey in the user list.
280 func (s *Server) keepaliveHandler(ctx context.Context) {
281 ticker := time.NewTicker(idleCheckInterval * time.Second)
289 for _, c := range s.ClientMgr.List() {
291 c.IdleTime += idleCheckInterval
294 if c.IdleTime > userIdleSeconds && !c.Flags.IsSet(UserFlagAway) {
295 c.Flags.Set(UserFlagAway, 1)
298 TranNotifyChangeUser,
299 NewField(FieldUserID, c.ID[:]),
300 NewField(FieldUserFlags, c.Flags[:]),
301 NewField(FieldUserName, c.UserName),
302 NewField(FieldUserIconID, c.Icon),
311 func (s *Server) NewClientConn(conn io.ReadWriteCloser, remoteAddr string) *ClientConn {
312 clientConn := &ClientConn{
313 Icon: []byte{0, 0}, // TODO: make array type
316 RemoteAddr: remoteAddr,
318 ClientFileTransferMgr: NewClientFileTransferMgr(),
321 s.ClientMgr.Add(clientConn)
326 func sendBanMessage(rwc io.Writer, message string) {
330 NewField(FieldData, []byte(message)),
331 NewField(FieldChatOptions, []byte{0, 0}),
333 _, _ = io.Copy(rwc, &t)
334 time.Sleep(1 * time.Second)
337 // handleNewConnection takes a new net.Conn and performs the initial login sequence
338 func (s *Server) handleNewConnection(ctx context.Context, rwc io.ReadWriteCloser, remoteAddr string) error {
339 defer dontPanic(s.Logger)
341 if err := performHandshake(rwc); err != nil {
342 return fmt.Errorf("perform handshake: %w", err)
345 // Check if remoteAddr is present in the ban list
346 ipAddr := strings.Split(remoteAddr, ":")[0]
347 if isBanned, banUntil := s.BanList.IsBanned(ipAddr); isBanned {
350 sendBanMessage(rwc, "You are permanently banned on this server")
351 s.Logger.Debug("Disconnecting permanently banned IP", "remoteAddr", ipAddr)
356 if time.Now().Before(*banUntil) {
357 sendBanMessage(rwc, "You are temporarily banned on this server")
358 s.Logger.Debug("Disconnecting temporarily banned IP", "remoteAddr", ipAddr)
363 // Create a new scanner for parsing incoming bytes into transaction tokens
364 scanner := bufio.NewScanner(rwc)
365 scanner.Split(transactionScanner)
369 // Make a new []byte slice and copy the scanner bytes to it. This is critical to avoid a data race as the
370 // scanner re-uses the buffer for subsequent scans.
371 buf := make([]byte, len(scanner.Bytes()))
372 copy(buf, scanner.Bytes())
374 var clientLogin Transaction
375 if _, err := clientLogin.Write(buf); err != nil {
376 return fmt.Errorf("error writing login transaction: %w", err)
379 c := s.NewClientConn(rwc, remoteAddr)
382 encodedPassword := clientLogin.GetField(FieldUserPassword).Data
383 c.Version = clientLogin.GetField(FieldVersion).Data
385 login := clientLogin.GetField(FieldUserLogin).DecodeObfuscatedString()
390 c.Logger = s.Logger.With("ip", ipAddr, "login", login)
392 // If authentication fails, send error reply and close connection
393 if !c.Authenticate(login, encodedPassword) {
394 t := c.NewErrReply(&clientLogin, "Incorrect login.")[0]
396 _, err := io.Copy(rwc, &t)
401 c.Logger.Info("Login failed", "clientVersion", fmt.Sprintf("%x", c.Version))
406 if clientLogin.GetField(FieldUserIconID).Data != nil {
407 c.Icon = clientLogin.GetField(FieldUserIconID).Data
410 c.Account = c.Server.AccountManager.Get(login)
411 if c.Account == nil {
415 if clientLogin.GetField(FieldUserName).Data != nil {
416 if c.Authorize(AccessAnyName) {
417 c.UserName = clientLogin.GetField(FieldUserName).Data
419 c.UserName = []byte(c.Account.Name)
423 if c.Authorize(AccessDisconUser) {
424 c.Flags.Set(UserFlagAdmin, 1)
427 s.outbox <- c.NewReply(&clientLogin,
428 NewField(FieldVersion, []byte{0x00, 0xbe}),
429 NewField(FieldCommunityBannerID, []byte{0, 0}),
430 NewField(FieldServerName, []byte(s.Config.Name)),
433 // Send user access privs so client UI knows how to behave
434 c.Server.outbox <- NewTransaction(TranUserAccess, c.ID, NewField(FieldUserAccess, c.Account.Access[:]))
436 // Accounts with AccessNoAgreement do not receive the server agreement on login. The behavior is different between
437 // client versions. For 1.2.3 client, we do not send TranShowAgreement. For other client versions, we send
438 // TranShowAgreement but with the NoServerAgreement field set to 1.
439 if c.Authorize(AccessNoAgreement) {
440 // If client version is nil, then the client uses the 1.2.3 login behavior
441 if c.Version != nil {
442 c.Server.outbox <- NewTransaction(TranShowAgreement, c.ID, NewField(FieldNoServerAgreement, []byte{1}))
445 _, _ = c.Server.Agreement.Seek(0, 0)
446 data, _ := io.ReadAll(c.Server.Agreement)
448 c.Server.outbox <- NewTransaction(TranShowAgreement, c.ID, NewField(FieldData, data))
451 // If the client has provided a username as part of the login, we can infer that it is using the 1.2.3 login
452 // flow and not the 1.5+ flow.
453 if len(c.UserName) != 0 {
454 // Add the client username to the logger. For 1.5+ clients, we don't have this information yet as it comes as
455 // part of TranAgreed
456 c.Logger = c.Logger.With("name", string(c.UserName))
457 c.Logger.Info("Login successful")
459 // Notify other clients on the server that the new user has logged in. For 1.5+ clients we don't have this
460 // information yet, so we do it in TranAgreed instead
461 for _, t := range c.NotifyOthers(
463 TranNotifyChangeUser, [2]byte{0, 0},
464 NewField(FieldUserName, c.UserName),
465 NewField(FieldUserID, c.ID[:]),
466 NewField(FieldUserIconID, c.Icon),
467 NewField(FieldUserFlags, c.Flags[:]),
474 c.Server.Stats.Increment(StatConnectionCounter, StatCurrentlyConnected)
475 defer c.Server.Stats.Decrement(StatCurrentlyConnected)
477 if len(s.ClientMgr.List()) > c.Server.Stats.Get(StatConnectionPeak) {
478 c.Server.Stats.Set(StatConnectionPeak, len(s.ClientMgr.List()))
481 // Scan for new transactions and handle them as they come in.
483 // Copy the scanner bytes to a new slice to it to avoid a data race when the scanner re-uses the buffer.
484 tmpBuf := make([]byte, len(scanner.Bytes()))
485 copy(tmpBuf, scanner.Bytes())
488 if _, err := t.Write(tmpBuf); err != nil {
492 c.handleTransaction(t)
497 // handleFileTransfer receives a client net.Conn from the file transfer server, performs the requested transfer type, then closes the connection
498 func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) error {
499 defer dontPanic(s.Logger)
501 // The first 16 bytes contain the file transfer.
503 if _, err := io.CopyN(&t, rwc, 16); err != nil {
504 return fmt.Errorf("error reading file transfer: %w", err)
507 fileTransfer := s.FileTransferMgr.Get(t.ReferenceNumber)
508 if fileTransfer == nil {
509 return errors.New("invalid transaction ID")
513 s.FileTransferMgr.Delete(t.ReferenceNumber)
515 // Wait a few seconds before closing the connection: this is a workaround for problems
516 // observed with Windows clients where the client must initiate close of the TCP connection before
517 // the server does. This is gross and seems unnecessary. TODO: Revisit?
518 time.Sleep(3 * time.Second)
521 rLogger := s.Logger.With(
522 "remoteAddr", ctx.Value(contextKeyReq).(requestCtx).remoteAddr,
523 "login", fileTransfer.ClientConn.Account.Login,
524 "Name", string(fileTransfer.ClientConn.UserName),
527 fullPath, err := ReadPath(s.Config.FileRoot, fileTransfer.FilePath, fileTransfer.FileName)
532 switch fileTransfer.Type {
534 if _, err := io.Copy(rwc, bytes.NewBuffer(s.Banner)); err != nil {
535 return fmt.Errorf("error sending Banner: %w", err)
538 s.Stats.Increment(StatDownloadCounter, StatDownloadsInProgress)
540 s.Stats.Decrement(StatDownloadsInProgress)
543 err = DownloadHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, true)
545 return fmt.Errorf("file download: %w", err)
549 s.Stats.Increment(StatUploadCounter, StatUploadsInProgress)
551 s.Stats.Decrement(StatUploadsInProgress)
554 err = UploadHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, s.Config.PreserveResourceForks)
556 return fmt.Errorf("file upload error: %w", err)
560 s.Stats.Increment(StatDownloadCounter, StatDownloadsInProgress)
562 s.Stats.Decrement(StatDownloadsInProgress)
565 err = DownloadFolderHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, s.Config.PreserveResourceForks)
567 return fmt.Errorf("file upload error: %w", err)
571 s.Stats.Increment(StatUploadCounter, StatUploadsInProgress)
573 s.Stats.Decrement(StatUploadsInProgress)
577 "Folder upload started",
579 "TransferSize", binary.BigEndian.Uint32(fileTransfer.TransferSize),
580 "FolderItemCount", fileTransfer.FolderItemCount,
583 err = UploadFolderHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, s.Config.PreserveResourceForks)
585 return fmt.Errorf("file upload error: %w", err)
591 func (s *Server) SendAll(t TranType, fields ...Field) {
592 for _, c := range s.ClientMgr.List() {
593 s.outbox <- NewTransaction(t, c.ID, fields...)
597 func (s *Server) Shutdown(msg []byte) {
598 s.Logger.Info("Shutdown signal received")
599 s.SendAll(TranDisconnectMsg, NewField(FieldData, msg))
601 time.Sleep(3 * time.Second)