diff options
| author | Jeff Halter <868228+jhalter@users.noreply.github.com> | 2026-07-09 18:28:58 -0700 |
|---|---|---|
| committer | Jeff Halter <868228+jhalter@users.noreply.github.com> | 2026-07-09 18:28:58 -0700 |
| commit | fff87ed2d3791d6853529457cc37689e8869b270 (patch) | |
| tree | 2a32d47fd192ddccb9029ca6cd13605325daaa1b /hotline/server.go | |
| parent | 631b7f1f99c2ff6295152210640a2f9b2c4f5a88 (diff) | |
Publish ClientConn only after login and guard Account with the state mutex
NewClientConn added the connection to the ClientManager before Account
and Version were assigned, so any goroutine iterating ClientMgr.List()
during the login handshake could dereference a nil Account (e.g.
HandleSetUser reading c.Account.Login) and panic. Account was also read
and written across goroutines with no synchronization: HandleSetUser
wrote c.Account.Access on another client's connection while that
client's own transaction loop read it in Authorize.
The connection is now added to the manager in handleNewConnection only
once Account, Version, UserName, and Flags are initialized, so a
published client is always fully formed. Failed logins never publish
the connection at all; Disconnect's manager delete is a no-op for them.
Account joins the mutex-guarded session state with accessors in the
established style: SetAccount/GetAccount, SetAccountAccess for the one
post-login mutation, AccessBytes for building transaction fields, and
Authorize now takes the read lock. All cross-goroutine call sites go
through the accessors.
HandleSetUser previously recomputed the admin flag from the client's
stale access level and only converged on the following edit; the access
update now happens before the recompute.
Diffstat (limited to 'hotline/server.go')
| -rw-r--r-- | hotline/server.go | 18 |
1 files changed, 11 insertions, 7 deletions
diff --git a/hotline/server.go b/hotline/server.go index 14cee04..700cf19 100644 --- a/hotline/server.go +++ b/hotline/server.go @@ -512,8 +512,6 @@ func (s *Server) NewClientConn(conn io.ReadWriteCloser, remoteAddr string) *Clie ClientFileTransferMgr: NewClientFileTransferMgr(), } - s.ClientMgr.Add(clientConn) - return clientConn } @@ -627,16 +625,17 @@ func (s *Server) handleNewConnection(ctx context.Context, rwc io.ReadWriteCloser c.SetIcon(clientLogin.GetField(FieldUserIconID).Data) } - c.Account = c.Server.AccountManager.Get(login) - if c.Account == nil { + account := c.Server.AccountManager.Get(login) + if account == nil { return nil } + c.SetAccount(account) if clientLogin.GetField(FieldUserName).Data != nil { if c.Authorize(AccessAnyName) { c.SetUserName(clientLogin.GetField(FieldUserName).Data) } else { - c.SetUserName([]byte(c.Account.Name)) + c.SetUserName([]byte(account.Name)) } } @@ -644,6 +643,11 @@ func (s *Server) handleNewConnection(ctx context.Context, rwc io.ReadWriteCloser c.SetFlag(UserFlagAdmin, 1) } + // Publish the client to the manager only now that its session state (Account, Version, + // UserName, Flags) is fully initialized. Other goroutines iterate ClientMgr.List() and + // dereference Account, so a client must never be visible before login completes. + s.ClientMgr.Add(c) + c.Send(c.NewReply(&clientLogin, NewField(FieldVersion, []byte{0x00, 0xbe}), NewField(FieldCommunityBannerID, []byte{0, 0}), @@ -651,7 +655,7 @@ func (s *Server) handleNewConnection(ctx context.Context, rwc io.ReadWriteCloser )) // Send user access privs so client UI knows how to behave - c.Send(NewTransaction(TranUserAccess, c.ID, NewField(FieldUserAccess, c.Account.Access[:]))) + c.Send(NewTransaction(TranUserAccess, c.ID, NewField(FieldUserAccess, c.AccessBytes()))) // Accounts with AccessNoAgreement do not receive the server agreement on login. The behavior is different between // client versions. For 1.2.3 client, we do not send TranShowAgreement. For other client versions, we send @@ -746,7 +750,7 @@ func (s *Server) handleFileTransfer(ctx context.Context, rwc io.ReadWriter) erro } rLogger := s.Logger.With( "remoteAddr", remoteAddr, - "login", fileTransfer.ClientConn.Account.Login, + "login", fileTransfer.ClientConn.GetAccount().Login, "Name", string(fileTransfer.ClientConn.GetUserName()), ) |