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) {
245 if s.Config.EnableTrackerRegistration {
246 for _, t := range s.Config.Trackers {
247 tr := &TrackerRegistration{
248 UserCount: len(s.ClientMgr.List()),
249 PassID: s.TrackerPassID,
251 Description: s.Config.Description,
253 binary.BigEndian.PutUint16(tr.Port[:], uint16(s.Port))
255 // Check the tracker string for a password. This is janky but avoids a breaking change to the Config
257 splitAddr := strings.Split(":", t)
258 if len(splitAddr) == 3 {
259 tr.Password = splitAddr[2]
262 if err := register(&RealDialer{}, t, tr); err != nil {
263 s.Logger.Error(fmt.Sprintf("Unable to register with tracker %v", t), "error", err)
267 // Using time.Ticker with for/select would be more idiomatic, but it's super annoying that it doesn't tick on
268 // first pass. Revist, maybe.
269 // https://github.com/golang/go/issues/17601
270 time.Sleep(trackerUpdateFrequency * time.Second)
275 userIdleSeconds = 300 // time in seconds before an inactive user is marked idle
276 idleCheckInterval = 10 // time in seconds to check for idle users
279 // keepaliveHandler runs every idleCheckInterval seconds and increments a user's idle time by idleCheckInterval seconds.
280 // If the updated idle time exceeds userIdleSeconds and the user was not previously idle, we notify all connected clients
281 // that the user has gone idle. For most clients, this turns the user grey in the user list.
282 func (s *Server) keepaliveHandler(ctx context.Context) {
283 ticker := time.NewTicker(idleCheckInterval * time.Second)
291 for _, c := range s.ClientMgr.List() {
293 c.IdleTime += idleCheckInterval
296 if c.IdleTime > userIdleSeconds && !c.Flags.IsSet(UserFlagAway) {
297 c.Flags.Set(UserFlagAway, 1)
300 TranNotifyChangeUser,
301 NewField(FieldUserID, c.ID[:]),
302 NewField(FieldUserFlags, c.Flags[:]),
303 NewField(FieldUserName, c.UserName),
304 NewField(FieldUserIconID, c.Icon),
313 func (s *Server) NewClientConn(conn io.ReadWriteCloser, remoteAddr string) *ClientConn {
314 clientConn := &ClientConn{
315 Icon: []byte{0, 0}, // TODO: make array type
318 RemoteAddr: remoteAddr,
320 ClientFileTransferMgr: NewClientFileTransferMgr(),
323 s.ClientMgr.Add(clientConn)
328 func sendBanMessage(rwc io.Writer, message string) {
332 NewField(FieldData, []byte(message)),
333 NewField(FieldChatOptions, []byte{0, 0}),
335 _, _ = io.Copy(rwc, &t)
336 time.Sleep(1 * time.Second)
339 // handleNewConnection takes a new net.Conn and performs the initial login sequence
340 func (s *Server) handleNewConnection(ctx context.Context, rwc io.ReadWriteCloser, remoteAddr string) error {
341 defer dontPanic(s.Logger)
343 if err := performHandshake(rwc); err != nil {
344 return fmt.Errorf("perform handshake: %w", err)
347 // Check if remoteAddr is present in the ban list
348 ipAddr := strings.Split(remoteAddr, ":")[0]
349 if isBanned, banUntil := s.BanList.IsBanned(ipAddr); isBanned {
352 sendBanMessage(rwc, "You are permanently banned on this server")
353 s.Logger.Debug("Disconnecting permanently banned IP", "remoteAddr", ipAddr)
358 if time.Now().Before(*banUntil) {
359 sendBanMessage(rwc, "You are temporarily banned on this server")
360 s.Logger.Debug("Disconnecting temporarily banned IP", "remoteAddr", ipAddr)
365 // Create a new scanner for parsing incoming bytes into transaction tokens
366 scanner := bufio.NewScanner(rwc)
367 scanner.Split(transactionScanner)
371 // Make a new []byte slice and copy the scanner bytes to it. This is critical to avoid a data race as the
372 // scanner re-uses the buffer for subsequent scans.
373 buf := make([]byte, len(scanner.Bytes()))
374 copy(buf, scanner.Bytes())
376 var clientLogin Transaction
377 if _, err := clientLogin.Write(buf); err != nil {
378 return fmt.Errorf("error writing login transaction: %w", err)
381 c := s.NewClientConn(rwc, remoteAddr)
384 encodedPassword := clientLogin.GetField(FieldUserPassword).Data
385 c.Version = clientLogin.GetField(FieldVersion).Data
387 login := clientLogin.GetField(FieldUserLogin).DecodeObfuscatedString()
392 c.Logger = s.Logger.With("ip", ipAddr, "login", login)
394 // If authentication fails, send error reply and close connection
395 if !c.Authenticate(login, encodedPassword) {
396 t := c.NewErrReply(&clientLogin, "Incorrect login.")[0]
398 _, err := io.Copy(rwc, &t)
403 c.Logger.Info("Login failed", "clientVersion", fmt.Sprintf("%x", c.Version))
408 if clientLogin.GetField(FieldUserIconID).Data != nil {
409 c.Icon = clientLogin.GetField(FieldUserIconID).Data
412 c.Account = c.Server.AccountManager.Get(login)
413 if c.Account == nil {
417 if clientLogin.GetField(FieldUserName).Data != nil {
418 if c.Authorize(AccessAnyName) {
419 c.UserName = clientLogin.GetField(FieldUserName).Data
421 c.UserName = []byte(c.Account.Name)
425 if c.Authorize(AccessDisconUser) {
426 c.Flags.Set(UserFlagAdmin, 1)
429 s.outbox <- c.NewReply(&clientLogin,
430 NewField(FieldVersion, []byte{0x00, 0xbe}),
431 NewField(FieldCommunityBannerID, []byte{0, 0}),
432 NewField(FieldServerName, []byte(s.Config.Name)),
435 // Send user access privs so client UI knows how to behave
436 c.Server.outbox <- NewTransaction(TranUserAccess, c.ID, NewField(FieldUserAccess, c.Account.Access[:]))
438 // Accounts with AccessNoAgreement do not receive the server agreement on login. The behavior is different between
439 // client versions. For 1.2.3 client, we do not send TranShowAgreement. For other client versions, we send
440 // TranShowAgreement but with the NoServerAgreement field set to 1.
441 if c.Authorize(AccessNoAgreement) {
442 // If client version is nil, then the client uses the 1.2.3 login behavior
443 if c.Version != nil {
444 c.Server.outbox <- NewTransaction(TranShowAgreement, c.ID, NewField(FieldNoServerAgreement, []byte{1}))
447 _, _ = c.Server.Agreement.Seek(0, 0)
448 data, _ := io.ReadAll(c.Server.Agreement)
450 c.Server.outbox <- NewTransaction(TranShowAgreement, c.ID, NewField(FieldData, data))
453 // If the client has provided a username as part of the login, we can infer that it is using the 1.2.3 login
454 // flow and not the 1.5+ flow.
455 if len(c.UserName) != 0 {
456 // Add the client username to the logger. For 1.5+ clients, we don't have this information yet as it comes as
457 // part of TranAgreed
458 c.Logger = c.Logger.With("name", string(c.UserName))
459 c.Logger.Info("Login successful")
461 // Notify other clients on the server that the new user has logged in. For 1.5+ clients we don't have this
462 // information yet, so we do it in TranAgreed instead
463 for _, t := range c.NotifyOthers(
465 TranNotifyChangeUser, [2]byte{0, 0},
466 NewField(FieldUserName, c.UserName),
467 NewField(FieldUserID, c.ID[:]),
468 NewField(FieldUserIconID, c.Icon),
469 NewField(FieldUserFlags, c.Flags[:]),
476 c.Server.Stats.Increment(StatConnectionCounter, StatCurrentlyConnected)
477 defer c.Server.Stats.Decrement(StatCurrentlyConnected)
479 if len(s.ClientMgr.List()) > c.Server.Stats.Get(StatConnectionPeak) {
480 c.Server.Stats.Set(StatConnectionPeak, len(s.ClientMgr.List()))
483 // Scan for new transactions and handle them as they come in.
485 // Copy the scanner bytes to a new slice to it to avoid a data race when the scanner re-uses the buffer.
486 tmpBuf := make([]byte, len(scanner.Bytes()))
487 copy(tmpBuf, scanner.Bytes())
490 if _, err := t.Write(tmpBuf); err != nil {
494 c.handleTransaction(t)
499 // handleFileTransfer receives a client net.Conn from the file transfer server, performs the requested transfer type, then closes the connection
500 func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) error {
501 defer dontPanic(s.Logger)
503 // The first 16 bytes contain the file transfer.
505 if _, err := io.CopyN(&t, rwc, 16); err != nil {
506 return fmt.Errorf("error reading file transfer: %w", err)
509 fileTransfer := s.FileTransferMgr.Get(t.ReferenceNumber)
510 if fileTransfer == nil {
511 return errors.New("invalid transaction ID")
515 s.FileTransferMgr.Delete(t.ReferenceNumber)
517 // Wait a few seconds before closing the connection: this is a workaround for problems
518 // observed with Windows clients where the client must initiate close of the TCP connection before
519 // the server does. This is gross and seems unnecessary. TODO: Revisit?
520 time.Sleep(3 * time.Second)
523 rLogger := s.Logger.With(
524 "remoteAddr", ctx.Value(contextKeyReq).(requestCtx).remoteAddr,
525 "login", fileTransfer.ClientConn.Account.Login,
526 "Name", string(fileTransfer.ClientConn.UserName),
529 fullPath, err := ReadPath(s.Config.FileRoot, fileTransfer.FilePath, fileTransfer.FileName)
534 switch fileTransfer.Type {
536 if _, err := io.Copy(rwc, bytes.NewBuffer(s.Banner)); err != nil {
537 return fmt.Errorf("error sending Banner: %w", err)
540 s.Stats.Increment(StatDownloadCounter, StatDownloadsInProgress)
542 s.Stats.Decrement(StatDownloadsInProgress)
545 err = DownloadHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, true)
547 return fmt.Errorf("file download: %w", err)
551 s.Stats.Increment(StatUploadCounter, StatUploadsInProgress)
553 s.Stats.Decrement(StatUploadsInProgress)
556 err = UploadHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, s.Config.PreserveResourceForks)
558 return fmt.Errorf("file upload error: %w", err)
562 s.Stats.Increment(StatDownloadCounter, StatDownloadsInProgress)
564 s.Stats.Decrement(StatDownloadsInProgress)
567 err = DownloadFolderHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, s.Config.PreserveResourceForks)
569 return fmt.Errorf("file upload error: %w", err)
573 s.Stats.Increment(StatUploadCounter, StatUploadsInProgress)
575 s.Stats.Decrement(StatUploadsInProgress)
579 "Folder upload started",
581 "TransferSize", binary.BigEndian.Uint32(fileTransfer.TransferSize),
582 "FolderItemCount", fileTransfer.FolderItemCount,
585 err = UploadFolderHandler(rwc, fullPath, fileTransfer, s.FS, rLogger, s.Config.PreserveResourceForks)
587 return fmt.Errorf("file upload error: %w", err)
593 func (s *Server) SendAll(t TranType, fields ...Field) {
594 for _, c := range s.ClientMgr.List() {
595 s.outbox <- NewTransaction(t, c.ID, fields...)
599 func (s *Server) Shutdown(msg []byte) {
600 s.Logger.Info("Shutdown signal received")
601 s.SendAll(TranDisconnectMsg, NewField(FieldData, msg))
603 time.Sleep(3 * time.Second)