mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-13 15:12:09 +00:00
feat: add ban policy to ticket verification
This commit is contained in:
+1
-1
@@ -1133,7 +1133,7 @@ New `--role=client-reconnect` grades the returning player: not a spectator, owns
|
|||||||
| 7.1 `[D:1.2]` | **IN PROGRESS.** GodotSteam integration and custom export templates — **client *and* headless server** | Pinned build inputs and the reproducible validation command are documented; awaiting the custom binaries/SDK access |
|
| 7.1 `[D:1.2]` | **IN PROGRESS.** GodotSteam integration and custom export templates — **client *and* headless server** | Pinned build inputs and the reproducible validation command are documented; awaiting the custom binaries/SDK access |
|
||||||
| 7.2 `[D:7.1]` | **IN PROGRESS.** `NetTransport` boundary extracted with ENet and feature-gated `steam_transport.gd` (`SteamMultiplayerPeer`, SDR); advertising waits for `ISteamGameServer` work | `NetworkManager.host/join(..., transport)` selects explicitly; stock builds reject Steam without ENet fallback |
|
| 7.2 `[D:7.1]` | **IN PROGRESS.** `NetTransport` boundary extracted with ENet and feature-gated `steam_transport.gd` (`SteamMultiplayerPeer`, SDR); advertising waits for `ISteamGameServer` work | `NetworkManager.host/join(..., transport)` selects explicitly; stock builds reject Steam without ENet fallback |
|
||||||
| 7.3 `[D:7.2]` `[P]` | `server_browser.tscn` via `ISteamMatchmakingServers` | Internet, LAN, favourites and history lists all populate |
|
| 7.3 `[D:7.2]` `[P]` | `server_browser.tscn` via `ISteamMatchmakingServers` | Internet, LAN, favourites and history lists all populate |
|
||||||
| 7.4 `[D:7.2]` `[P]` | Auth tickets in `hello` → `BeginAuthSession`; Steam identity in the roster; persistent ban list | Ownership, VAC and ban state verified server-side |
|
| 7.4 `[D:7.2]` `[P]` | **IN PROGRESS.** `TicketVerifier` now supports a synchronized backend ban decision before single-use ticket consumption; auth tickets in `hello` → `BeginAuthSession`, Steam identity in the roster and persistent ban list remain | `server/domain/auth.go` and adversarial tests reject banned identities without consuming their ticket and allow a later verification after unban; GodotSteam auth integration, server-side VAC state and durable ban storage remain |
|
||||||
| 7.5 `[D:7.2]` `[P]` | Feature-gate every Steam call behind `OS.has_feature("steam") and ClassDB.class_exists("SteamMultiplayerPeer")`; verify the ENet path end to end | Non-Steam build is fully functional |
|
| 7.5 `[D:7.2]` `[P]` | Feature-gate every Steam call behind `OS.has_feature("steam") and ClassDB.class_exists("SteamMultiplayerPeer")`; verify the ENet path end to end | Non-Steam build is fully functional |
|
||||||
| 7.6 `[D:7.4]` | **IN PROGRESS.** Pure Go `AuthCoordinator` models pending/accepted/rejected/cancelled backend auth sessions around the single-use verifier, including maintenance expiry sweeping; `store.PostgresSessions` persists only token digests, the authenticated API can inject that durable session backend, and `POST /v1/session/steam` issues sessions only from an injected verified-identity provider | `server/domain/auth.go`, `server/store/session_sql.go`, `server/api/service.go` and adversarial tests keep identity unavailable while pending, reject cancellation/expiry/wrong-attempt/replay, expire abandoned attempts at the boundary, release identity only after verifier success, reject invalid session inputs/extra identity fields, prove API delegation, and issue opaque sessions; real Steam BeginAuthSession/EndAuthSession adapter and live PostgreSQL/session integration remain |
|
| 7.6 `[D:7.4]` | **IN PROGRESS.** Pure Go `AuthCoordinator` models pending/accepted/rejected/cancelled backend auth sessions around the single-use verifier, including maintenance expiry sweeping; `store.PostgresSessions` persists only token digests, the authenticated API can inject that durable session backend, and `POST /v1/session/steam` issues sessions only from an injected verified-identity provider | `server/domain/auth.go`, `server/store/session_sql.go`, `server/api/service.go` and adversarial tests keep identity unavailable while pending, reject cancellation/expiry/wrong-attempt/replay, expire abandoned attempts at the boundary, release identity only after verifier success, reject invalid session inputs/extra identity fields, prove API delegation, and issue opaque sessions; real Steam BeginAuthSession/EndAuthSession adapter and live PostgreSQL/session integration remain |
|
||||||
| 7.7 `[D:7.1]` `[P]` | Obtain the production App ID, publisher key, SDR coordinator SDK/signing approval, certificates and hosted-data-centre support from Valve | Production prerequisites and rotation owners are recorded; no Spacewar credential or development certificate can reach a release build |
|
| 7.7 `[D:7.1]` `[P]` | Obtain the production App ID, publisher key, SDR coordinator SDK/signing approval, certificates and hosted-data-centre support from Valve | Production prerequisites and rotation owners are recorded; no Spacewar credential or development certificate can reach a release build |
|
||||||
|
|||||||
+17
-2
@@ -26,6 +26,7 @@ type TicketVerifier struct {
|
|||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
expectedApp uint64
|
expectedApp uint64
|
||||||
consumed map[string]time.Time
|
consumed map[string]time.Time
|
||||||
|
banned map[string]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthAttemptState string
|
type AuthAttemptState string
|
||||||
@@ -159,7 +160,21 @@ func NewTicketVerifier(expectedApp uint64) (*TicketVerifier, error) {
|
|||||||
if expectedApp == 0 {
|
if expectedApp == 0 {
|
||||||
return nil, ErrTicketRejected
|
return nil, ErrTicketRejected
|
||||||
}
|
}
|
||||||
return &TicketVerifier{expectedApp: expectedApp, consumed: make(map[string]time.Time)}, nil
|
return &TicketVerifier{expectedApp: expectedApp, consumed: make(map[string]time.Time), banned: make(map[string]bool)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *TicketVerifier) SetBanned(playerID string, banned bool) error {
|
||||||
|
if v == nil || playerID == "" {
|
||||||
|
return ErrTicketRejected
|
||||||
|
}
|
||||||
|
v.mu.Lock()
|
||||||
|
defer v.mu.Unlock()
|
||||||
|
if banned {
|
||||||
|
v.banned[playerID] = true
|
||||||
|
} else {
|
||||||
|
delete(v.banned, playerID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify consumes a backend-validated ticket exactly once. In production the
|
// Verify consumes a backend-validated ticket exactly once. In production the
|
||||||
@@ -175,7 +190,7 @@ func (v *TicketVerifier) Verify(ticket SteamTicket, resolve func(string) (string
|
|||||||
return VerifiedIdentity{}, ErrTicketRejected
|
return VerifiedIdentity{}, ErrTicketRejected
|
||||||
}
|
}
|
||||||
playerID, ok := resolve(ticket.SteamID)
|
playerID, ok := resolve(ticket.SteamID)
|
||||||
if !ok || playerID == "" {
|
if !ok || playerID == "" || v.banned[playerID] {
|
||||||
return VerifiedIdentity{}, ErrTicketRejected
|
return VerifiedIdentity{}, ErrTicketRejected
|
||||||
}
|
}
|
||||||
v.consumed[ticket.TicketID] = now
|
v.consumed[ticket.TicketID] = now
|
||||||
|
|||||||
@@ -38,6 +38,25 @@ func TestTicketVerifierBindsAppIdentityExpiryAndSingleUse(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTicketVerifierRejectsBannedIdentityBeforeConsumption(t *testing.T) {
|
||||||
|
now := time.Unix(1000, 0)
|
||||||
|
verifier, _ := NewTicketVerifier(480)
|
||||||
|
if err := verifier.SetBanned("player-1", true); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ticket := SteamTicket{TicketID: "ticket-1", SteamID: "steam-1", AppID: 480, ExpiresAt: now.Add(time.Minute)}
|
||||||
|
resolve := func(string) (string, bool) { return "player-1", true }
|
||||||
|
if _, err := verifier.Verify(ticket, resolve, now); !errors.Is(err, ErrTicketRejected) {
|
||||||
|
t.Fatalf("banned ticket accepted: %v", err)
|
||||||
|
}
|
||||||
|
if err := verifier.SetBanned("player-1", false); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := verifier.Verify(ticket, resolve, now); err != nil {
|
||||||
|
t.Fatalf("unbanned ticket remained consumed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSessionIsOpaqueShortLivedAndRevocable(t *testing.T) {
|
func TestSessionIsOpaqueShortLivedAndRevocable(t *testing.T) {
|
||||||
now := time.Unix(1000, 0)
|
now := time.Unix(1000, 0)
|
||||||
store := NewSessionStore()
|
store := NewSessionStore()
|
||||||
|
|||||||
Reference in New Issue
Block a user