11 "golang.org/x/text/encoding/charmap"
21 type contextKey string
23 var contextKeyReq = contextKey("req")
25 type requestCtx struct {
29 // Converts bytes from Mac Roman encoding to UTF-8
30 var txtDecoder = charmap.Macintosh.NewDecoder()
32 // Converts bytes from UTF-8 to Mac Roman encoding
33 var txtEncoder = charmap.Macintosh.NewEncoder()
39 handlers map[TranType]HandlerFunc
48 FS FileStore // Storage backend to use for File storage
50 outbox chan Transaction
52 Agreement io.ReadSeeker
55 FileTransferMgr FileTransferMgr
57 ClientMgr ClientManager
58 AccountManager AccountManager
59 ThreadedNewsMgr ThreadedNewsMgr
62 MessageBoard io.ReadWriteSeeker
65 type Option = func(s *Server)
67 func WithConfig(config Config) func(s *Server) {
68 return func(s *Server) {
73 func WithLogger(logger *slog.Logger) func(s *Server) {
74 return func(s *Server) {
79 // WithPort optionally overrides the default TCP port.
80 func WithPort(port int) func(s *Server) {
81 return func(s *Server) {
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
93 type ServerConfig struct {
96 func NewServer(options ...Option) (*Server, error) {
98 handlers: make(map[TranType]HandlerFunc),
99 outbox: make(chan Transaction),
101 ChatMgr: NewMemChatManager(),
102 ClientMgr: NewMemClientMgr(),
103 FileTransferMgr: NewMemFileTransferMgr(),
107 for _, opt := range options {
111 // generate a new random passID for tracker registration
112 _, err := rand.Read(server.TrackerPassID[:])
120 func (s *Server) CurrentStats() map[string]interface{} {
121 return s.Stats.Values()
124 func (s *Server) ListenAndServe(ctx context.Context) error {
125 go s.registerWithTrackers(ctx)
126 go s.keepaliveHandler(ctx)
129 var wg sync.WaitGroup
133 ln, err := net.Listen("tcp", fmt.Sprintf("%s:%v", s.NetInterface, s.Port))
138 log.Fatal(s.Serve(ctx, ln))
143 ln, err := net.Listen("tcp", fmt.Sprintf("%s:%v", s.NetInterface, s.Port+1))
148 log.Fatal(s.ServeFileTransfers(ctx, ln))
156 func (s *Server) ServeFileTransfers(ctx context.Context, ln net.Listener) error {
158 conn, err := ln.Accept()
164 defer func() { _ = conn.Close() }()
166 err = s.handleFileTransfer(
167 context.WithValue(ctx, contextKeyReq, requestCtx{remoteAddr: conn.RemoteAddr().String()}),
172 s.Logger.Error("file transfer error", "reason", err)
178 func (s *Server) sendTransaction(t Transaction) error {
179 client := s.ClientMgr.Get(t.ClientID)
185 _, err := io.Copy(client.Connection, &t)
187 return fmt.Errorf("failed to send transaction to client %v: %v", t.ClientID, err)
193 func (s *Server) processOutbox() {
197 if err := s.sendTransaction(t); err != nil {
198 s.Logger.Error("error sending transaction", "err", err)
204 func (s *Server) Serve(ctx context.Context, ln net.Listener) error {
208 s.Logger.Info("Server shutting down")
211 conn, err := ln.Accept()
213 s.Logger.Error("Error accepting connection", "err", err)
218 connCtx := context.WithValue(ctx, contextKeyReq, requestCtx{
219 remoteAddr: conn.RemoteAddr().String(),
222 s.Logger.Info("Connection established", "addr", conn.RemoteAddr())
225 if err := s.handleNewConnection(connCtx, conn, conn.RemoteAddr().String()); err != nil {
227 s.Logger.Info("Client disconnected", "RemoteAddr", conn.RemoteAddr())
229 s.Logger.Error("Error serving request", "RemoteAddr", conn.RemoteAddr(), "err", err)
237 // time in seconds between tracker re-registration
238 const trackerUpdateFrequency = 300
240 // registerWithTrackers runs every trackerUpdateFrequency seconds to update the server's tracker entry on all configured
242 func (s *Server) registerWithTrackers(ctx context.Context) {
243 ticker := time.NewTicker(trackerUpdateFrequency * time.Second)
251 if s.Config.EnableTrackerRegistration {
252 tr := &TrackerRegistration{
253 UserCount: len(s.ClientMgr.List()),
254 PassID: s.TrackerPassID,
256 Description: s.Config.Description,
258 binary.BigEndian.PutUint16(tr.Port[:], uint16(s.Port))
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)
272 userIdleSeconds = 300 // time in seconds before an inactive user is marked idle
273 idleCheckInterval = 10 // time in seconds to check for idle users
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)
288 for _, c := range s.ClientMgr.List() {
290 c.IdleTime += idleCheckInterval
293 if c.IdleTime > userIdleSeconds && !c.Flags.IsSet(UserFlagAway) {
294 c.Flags.Set(UserFlagAway, 1)
297 TranNotifyChangeUser,
298 NewField(FieldUserID, c.ID[:]),
299 NewField(FieldUserFlags, c.Flags[:]),
300 NewField(FieldUserName, c.UserName),
301 NewField(FieldUserIconID, c.Icon),
310 func (s *Server) NewClientConn(conn io.ReadWriteCloser, remoteAddr string) *ClientConn {
311 clientConn := &ClientConn{
312 Icon: []byte{0, 0}, // TODO: make array type
315 RemoteAddr: remoteAddr,
317 ClientFileTransferMgr: NewClientFileTransferMgr(),
320 s.ClientMgr.Add(clientConn)
325 func sendBanMessage(rwc io.Writer, message string) {
329 NewField(FieldData, []byte(message)),
330 NewField(FieldChatOptions, []byte{0, 0}),
332 _, _ = io.Copy(rwc, &t)
333 time.Sleep(1 * time.Second)
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)
340 if err := performHandshake(rwc); err != nil {
341 return fmt.Errorf("perform handshake: %w", err)
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 {
349 sendBanMessage(rwc, "You are permanently banned on this server")
350 s.Logger.Debug("Disconnecting permanently banned IP", "remoteAddr", ipAddr)
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)
362 // Create a new scanner for parsing incoming bytes into transaction tokens
363 scanner := bufio.NewScanner(rwc)
364 scanner.Split(transactionScanner)
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())
373 var clientLogin Transaction
374 if _, err := clientLogin.Write(buf); err != nil {
375 return fmt.Errorf("error writing login transaction: %w", err)
378 c := s.NewClientConn(rwc, remoteAddr)
381 encodedPassword := clientLogin.GetField(FieldUserPassword).Data
382 c.Version = clientLogin.GetField(FieldVersion).Data
384 login := clientLogin.GetField(FieldUserLogin).DecodeObfuscatedString()
389 c.Logger = s.Logger.With("ip", ipAddr, "login", login)
391 // If authentication fails, send error reply and close connection
392 if !c.Authenticate(login, encodedPassword) {
393 t := c.NewErrReply(&clientLogin, "Incorrect login.")[0]
395 _, err := io.Copy(rwc, &t)
400 c.Logger.Info("Login failed", "clientVersion", fmt.Sprintf("%x", c.Version))
405 if clientLogin.GetField(FieldUserIconID).Data != nil {
406 c.Icon = clientLogin.GetField(FieldUserIconID).Data
409 c.Account = c.Server.AccountManager.Get(login)
410 if c.Account == nil {
414 if clientLogin.GetField(FieldUserName).Data != nil {
415 if c.Authorize(AccessAnyName) {
416 c.UserName = clientLogin.GetField(FieldUserName).Data
418 c.UserName = []byte(c.Account.Name)
422 if c.Authorize(AccessDisconUser) {
423 c.Flags.Set(UserFlagAdmin, 1)
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)),
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[:]))
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}))
444 _, _ = c.Server.Agreement.Seek(0, 0)
445 data, _ := io.ReadAll(c.Server.Agreement)
447 c.Server.outbox <- NewTransaction(TranShowAgreement, c.ID, NewField(FieldData, data))
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")
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(
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[:]),
473 c.Server.Stats.Increment(StatConnectionCounter, StatCurrentlyConnected)
474 defer c.Server.Stats.Decrement(StatCurrentlyConnected)
476 if len(s.ClientMgr.List()) > c.Server.Stats.Get(StatConnectionPeak) {
477 c.Server.Stats.Set(StatConnectionPeak, len(s.ClientMgr.List()))
480 // Scan for new transactions and handle them as they come in.
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())
487 if _, err := t.Write(tmpBuf); err != nil {
491 c.handleTransaction(t)
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)
500 // The first 16 bytes contain the file transfer.
502 if _, err := io.CopyN(&t, rwc, 16); err != nil {
503 return fmt.Errorf("error reading file transfer: %w", err)
506 fileTransfer := s.FileTransferMgr.Get(t.ReferenceNumber)
507 if fileTransfer == nil {
508 return errors.New("invalid transaction ID")
512 s.FileTransferMgr.Delete(t.ReferenceNumber)
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)
520 rLogger := s.Logger.With(
521 "remoteAddr", ctx.Value(contextKeyReq).(requestCtx).remoteAddr,
522 "login", fileTransfer.ClientConn.Account.Login,
523 "Name", string(fileTransfer.ClientConn.UserName),
526 fullPath, err := ReadPath(s.Config.FileRoot, fileTransfer.FilePath, fileTransfer.FileName)
531 switch fileTransfer.Type {
533 if _, err := io.Copy(rwc, bytes.NewBuffer(s.Banner)); err != nil {
534 return fmt.Errorf("error sending Banner: %w", err)
537 s.Stats.Increment(StatDownloadCounter, StatDownloadsInProgress)
539 s.Stats.Decrement(StatDownloadsInProgress)
542 err = DownloadHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, true)
544 return fmt.Errorf("file download: %w", err)
548 s.Stats.Increment(StatUploadCounter, StatUploadsInProgress)
550 s.Stats.Decrement(StatUploadsInProgress)
553 err = UploadHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, s.Config.PreserveResourceForks)
555 return fmt.Errorf("file upload error: %w", err)
559 s.Stats.Increment(StatDownloadCounter, StatDownloadsInProgress)
561 s.Stats.Decrement(StatDownloadsInProgress)
564 err = DownloadFolderHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, s.Config.PreserveResourceForks)
566 return fmt.Errorf("file upload error: %w", err)
570 s.Stats.Increment(StatUploadCounter, StatUploadsInProgress)
572 s.Stats.Decrement(StatUploadsInProgress)
576 "Folder upload started",
578 "TransferSize", binary.BigEndian.Uint32(fileTransfer.TransferSize),
579 "FolderItemCount", fileTransfer.FolderItemCount,
582 err = UploadFolderHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, s.Config.PreserveResourceForks)
584 return fmt.Errorf("file upload error: %w", err)