X-Git-Url: https://git.r.bdr.sh/rbdr/mobius/blobdiff_plain/00913df3521d967ac31ee22c23447b5aa711f2e6..c3c2f61b2fd90ab5c7fe6ec769644e9d66d40361:/hotline/server.go diff --git a/hotline/server.go b/hotline/server.go index 9cf93d0..9e5f498 100644 --- a/hotline/server.go +++ b/hotline/server.go @@ -2,7 +2,6 @@ package hotline import ( "bufio" - "bytes" "context" "encoding/binary" "errors" @@ -33,14 +32,6 @@ type requestCtx struct { name string } -const ( - userIdleSeconds = 300 // time in seconds before an inactive user is marked idle - idleCheckInterval = 10 // time in seconds to check for idle users - trackerUpdateFrequency = 300 // time in seconds between tracker re-registration -) - -var nostalgiaVersion = []byte{0, 0, 2, 0x2c} // version ID used by the Nostalgia client - type Server struct { Port int Accounts map[string]*Account @@ -158,18 +149,18 @@ func (s *Server) sendTransaction(t Transaction) error { s.mux.Lock() client := s.Clients[uint16(clientID)] + s.mux.Unlock() if client == nil { return fmt.Errorf("invalid client id %v", *t.clientID) } - s.mux.Unlock() - b, err := t.MarshalBinary() if err != nil { return err } - if _, err := client.Connection.Write(b); err != nil { + _, err = client.Connection.Write(b) + if err != nil { return err } @@ -386,7 +377,6 @@ func (s *Server) NewClientConn(conn io.ReadWriteCloser, remoteAddr string) *Clie Version: []byte{}, AutoReply: []byte{}, transfers: map[int]map[[4]byte]*FileTransfer{}, - Agreed: false, RemoteAddr: remoteAddr, } clientConn.transfers = map[int]map[[4]byte]*FileTransfer{ @@ -473,9 +463,6 @@ func (s *Server) connectedUsers() []Field { var connectedUsers []Field for _, c := range sortedClients(s.Clients) { - if !c.Agreed { - continue - } user := User{ ID: *c.ID, Icon: c.Icon, @@ -570,37 +557,65 @@ func (s *Server) handleNewConnection(ctx context.Context, rwc io.ReadWriteCloser scanner.Scan() + // Make a new []byte slice and copy the scanner bytes to it. This is critical to avoid a data race as the + // scanner re-uses the buffer for subsequent scans. + buf := make([]byte, len(scanner.Bytes())) + copy(buf, scanner.Bytes()) + var clientLogin Transaction - if _, err := clientLogin.Write(scanner.Bytes()); err != nil { + if _, err := clientLogin.Write(buf); err != nil { return err } - c := s.NewClientConn(rwc, remoteAddr) - // check if remoteAddr is present in the ban list if banUntil, ok := s.banList[strings.Split(remoteAddr, ":")[0]]; ok { // permaban if banUntil == nil { - s.outbox <- *NewTransaction( + t := NewTransaction( tranServerMsg, - c.ID, + &[]byte{0, 0}, NewField(fieldData, []byte("You are permanently banned on this server")), NewField(fieldChatOptions, []byte{0, 0}), ) + + b, err := t.MarshalBinary() + if err != nil { + return err + } + + _, err = rwc.Write(b) + if err != nil { + return err + } + time.Sleep(1 * time.Second) return nil - } else if time.Now().Before(*banUntil) { - s.outbox <- *NewTransaction( + } + + // temporary ban + if time.Now().Before(*banUntil) { + t := NewTransaction( tranServerMsg, - c.ID, + &[]byte{0, 0}, NewField(fieldData, []byte("You are temporarily banned on this server")), NewField(fieldChatOptions, []byte{0, 0}), ) + b, err := t.MarshalBinary() + if err != nil { + return err + } + + _, err = rwc.Write(b) + if err != nil { + return err + } + time.Sleep(1 * time.Second) return nil } - } + + c := s.NewClientConn(rwc, remoteAddr) defer c.Disconnect() encodedLogin := clientLogin.GetField(fieldUserLogin).Data @@ -672,12 +687,17 @@ func (s *Server) handleNewConnection(ctx context.Context, rwc io.ReadWriteCloser c.Server.outbox <- *NewTransaction(tranShowAgreement, c.ID, NewField(fieldData, s.Agreement)) } - // Used simplified hotline v1.2.3 login flow for clients that do not send login info in tranAgreed - if c.Version == nil || bytes.Equal(c.Version, nostalgiaVersion) { - c.Agreed = true + // If the client has provided a username as part of the login, we can infer that it is using the 1.2.3 login + // flow and not the 1.5+ flow. + if len(c.UserName) != 0 { + // Add the client username to the logger. For 1.5+ clients, we don't have this information yet as it comes as + // part of tranAgreed c.logger = c.logger.With("name", string(c.UserName)) - c.logger.Infow("Login successful", "clientVersion", fmt.Sprintf("%v", func() int { i, _ := byteToInt(c.Version); return i }())) + c.logger.Infow("Login successful", "clientVersion", "Not sent (probably 1.2.3)") + + // Notify other clients on the server that the new user has logged in. For 1.5+ clients we don't have this + // information yet, so we do it in tranAgreed instead for _, t := range c.notifyOthers( *NewTransaction( tranNotifyChangeUser, nil, @@ -754,6 +774,10 @@ func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) erro delete(s.fileTransfers, t.ReferenceNumber) s.mux.Unlock() + // Wait a few seconds before closing the connection: this is a workaround for problems + // observed with Windows clients where the client must initiate close of the TCP connection before + // the server does. This is gross and seems unnecessary. TODO: Revisit? + time.Sleep(3 * time.Second) }() s.mux.Lock() @@ -788,7 +812,9 @@ func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) erro case FileDownload: s.Stats.DownloadCounter += 1 s.Stats.DownloadsInProgress += 1 - defer func() { s.Stats.DownloadsInProgress -= 1 }() + defer func() { + s.Stats.DownloadsInProgress -= 1 + }() var dataOffset int64 if fileTransfer.fileResumeData != nil {