diff options
| author | historia <[not public]> | 2026-09-18 05:31:09 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-18 05:31:09 -0400 |
| commit | 153acb7f644d755c45533b7c5311e3ffa47e8973 (patch) | |
| tree | 7645e6f7b65bc3830b078e3b9336d2e6b199d584 /internal/game/core_login_account.go | |
| parent | 87d08f0151e643951769e323995f1c5ed89ee200 (diff) | |
| download | thehouseoficarus-153acb7f644d755c45533b7c5311e3ffa47e8973.tar.gz | |
fix: harden engine across casino, economy, concurrency, security, and content
Diffstat (limited to 'internal/game/core_login_account.go')
| -rw-r--r-- | internal/game/core_login_account.go | 82 |
1 files changed, 72 insertions, 10 deletions
diff --git a/internal/game/core_login_account.go b/internal/game/core_login_account.go index 921af82..a5106ea 100644 --- a/internal/game/core_login_account.go +++ b/internal/game/core_login_account.go @@ -16,6 +16,14 @@ func (g *Game) handleAccountName(sess *net.Session, input string) { sess.Write("Account name: ") return } + // Validate BEFORE any filesystem access: the name is joined into an + // account (and later character) path, so an invalid name must never + // reach FindAccount or anything downstream that reuses it. + if verr := player.ValidName(name); verr != nil { + sess.WriteLine(verr.Error()) + sess.Write("Account name: ") + return + } actualName, err := g.AccountStore.FindAccount(name) if err == nil { sess.Account = &player.Account{Name: actualName} @@ -24,11 +32,6 @@ func (g *Game) handleAccountName(sess *net.Session, input string) { sess.SetEcho(false) sess.Write("Password: ") } else { - if verr := player.ValidName(name); verr != nil { - sess.WriteLine(verr.Error()) - sess.Write("Account name: ") - return - } sess.Account = &player.Account{Name: name} sess.SetAuthName(name) sess.SetState(net.StateNewAccountPass) @@ -38,6 +41,22 @@ func (g *Game) handleAccountName(sess *net.Session, input string) { } } +// resetToAccountName returns a session that has no usable account left +// (purged, or republished out from under it) to the account-name prompt. A +// menu-state handler must call it instead of dereferencing sess.Account: +// a queued-mode Close only drops the transport after the writer drains, so +// an already-buffered input line can still be dispatched to the owner +// after the account is gone. +func (g *Game) resetToAccountName(sess *net.Session) { + sess.Account = nil + sess.PendingChar = "" + sess.PendingMenu = nil + sess.PendingPass = "" + sess.SetEcho(true) + sess.SetState(net.StateAccountName) + sess.Write("Account name: ") +} + func (g *Game) handlePassword(sess *net.Session, input string) { if input == "" { sess.Write("Password: ") @@ -171,6 +190,13 @@ func (g *Game) applyPasswordAuth(sess *net.Session, auth passwordAuth) { sess.SetState(net.StateAccountName) sess.Write("Account name: ") } + if sess.Account == nil { + // The account vanished while the password was being verified (a + // purge or republish from another session): there is nothing to + // authorize against, so start over from the account-name prompt. + g.resetToAccountName(sess) + return + } if auth.loadErr { reject("Error loading account.") return @@ -235,6 +261,10 @@ func (g *Game) handleNewPass(sess *net.Session, input string) { } func (g *Game) handleNewAccount(sess *net.Session, input string) { + if sess.Account == nil { + g.resetToAccountName(sess) + return + } input = strings.TrimSpace(input) if input == "" { sess.SetEcho(true) @@ -283,6 +313,13 @@ func (g *Game) handleNewAccountOffOwner(sess *net.Session, input string) { // applyNewAccount creates the account on the owner. The password was // already hashed by the caller (off the owner where possible). func (g *Game) applyNewAccount(sess *net.Session, hash string, hashErr error) { + if sess.Account == nil { + // The account vanished while the password was being hashed off the + // owner (a purge or republish from another session): there is nothing + // to create against, and sess.Account is dereferenced below. + g.resetToAccountName(sess) + return + } if hashErr != nil { sess.SetEcho(true) sess.WriteLine(fmt.Sprintf("Error creating account: %v", hashErr)) @@ -332,6 +369,10 @@ func (g *Game) applyNewAccount(sess *net.Session, hash string, hashErr error) { } func (g *Game) handleColorChoice(sess *net.Session, input string) { + if sess.Account == nil { + g.resetToAccountName(sess) + return + } input = strings.ToLower(strings.TrimSpace(input)) if input == "y" || input == "yes" { sess.Account.Options["color"] = "none" @@ -359,6 +400,13 @@ func menuColorMode(sess *net.Session) string { } func (g *Game) showMenu(sess *net.Session) { + if sess.Account == nil { + // The account is gone (purged or republished from another session) + // while this session was mid-flow: there is no menu to show for a + // deleted account. + g.resetToAccountName(sess) + return + } sess.SetState(net.StateMenu) mode := menuColorMode(sess) lines := []string{ @@ -387,6 +435,10 @@ func (g *Game) showMenu(sess *net.Session) { } func (g *Game) handleMenu(sess *net.Session, input string) { + if sess.Account == nil { + g.resetToAccountName(sess) + return + } switch strings.ToLower(strings.TrimSpace(input)) { case "": sess.Write("> ") @@ -483,6 +535,10 @@ func (g *Game) handleMenu(sess *net.Session, input string) { } func (g *Game) handleRenameAccount(sess *net.Session, input string) { + if sess.Account == nil { + g.resetToAccountName(sess) + return + } newName := strings.TrimSpace(input) if newName == "" { sess.Write("New account name: ") @@ -524,6 +580,10 @@ func (g *Game) handleRenameAccount(sess *net.Session, input string) { } func (g *Game) handlePurgeAccount(sess *net.Session, input string) { + if sess.Account == nil { + g.resetToAccountName(sess) + return + } input = strings.TrimSpace(input) expected := "PURGE " + sess.Account.Name if !strings.EqualFold(input, expected) { @@ -551,17 +611,19 @@ func (g *Game) handlePurgeAccount(sess *net.Session, input string) { } sess.WriteLine(fmt.Sprintf("Account %s has been purged. Thanks for playing.", sess.Account.Name)) - // Other authenticated sessions of this account are stale now; tell - // them and close them (they cannot safely keep using cached state). + // Every session authenticated to this account is stale now: the account + // file is gone, so none of them may keep a live protocol state (a menu + // handler dereferencing the cached account, or a buffered input line + // already inside the transport, would fault). Reset each to the + // account-name prompt before closing it. for _, other := range g.allSessionsOfAccount(sess.Account.Name) { if other == sess { continue } - other.Account = nil - other.PendingChar = "" - other.PendingMenu = nil other.WriteLine("\nYour account has been purged from another session.") + g.resetToAccountName(other) other.Close() } + g.resetToAccountName(sess) sess.Close() } |
