fix(multiplayer): validate allocated roster shape

This commit is contained in:
Josh Creek
2026-09-01 16:18:31 +01:00
parent 69a8402f11
commit 80d6ee8cf5
4 changed files with 29 additions and 5 deletions
+13 -4
View File
@@ -122,14 +122,23 @@ func configure_join_authorisations(tokens: Array, context: Dictionary, signing_k
func assigned_player_slots() -> Array:
var result: Array = []
var seen_identities := {}
var seen_slots := {}
for token in _allowed_join_authorisations.keys():
var claims := _join_claims(String(token))
if claims.is_empty():
continue
return []
var identity := str(claims.get("PlayerID", ""))
var team := int(claims.get("Team", -1))
var slot := int(claims.get("Slot", -1))
if identity.is_empty() or team < 0 or team >= TEAM_COUNT or slot < 0 or slot > 5 or slot / 3 != team or seen_identities.has(identity) or seen_slots.has(slot):
return []
seen_identities[identity] = true
seen_slots[slot] = true
result.append({
"player_identity": str(claims.get("PlayerID", "")),
"team": int(claims.get("Team", -1)),
"slot": int(claims.get("Slot", -1)),
"player_identity": identity,
"team": team,
"slot": slot,
})
result.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return int(a["slot"]) < int(b["slot"]))
return result
+1 -1
View File
@@ -78,7 +78,7 @@ func _ready() -> void:
"server_id": String(config.get_value("server-id")),
"protocol": str(NetCodec.PROTOCOL_VERSION),
"protocol_version": NetCodec.PROTOCOL_VERSION,
}, signing_key):
}, signing_key) or MatchNet.assigned_player_slots().size() != roster_tokens.size():
printerr("cosmic-clash-server: refusing to start with invalid join-authorisations-file")
get_tree().quit(1)
return
+13
View File
@@ -89,6 +89,19 @@ func test_allocated_join_authorisation_rejects_inconsistent_team_and_slot() -> v
assert_true(not match_net._valid_join_authorisation(token), "a slot assigned to team 1 cannot claim team 0")
func test_assigned_roster_rejects_duplicate_identity_or_slot_shape() -> void:
var claims := {
"MatchID": "match-1", "ServerID": "server-1", "PlayerID": "player-1",
"SteamID": "steam-1", "Slot": 0, "Team": 0, "Protocol": "1",
"Generation": 1, "ExpiresAt": "2099-08-31T12:00:00Z",
}
var first := Marshalls.raw_to_base64(JSON.stringify({"Authorisation": claims, "Signature": "one"}).to_utf8_buffer())
var duplicate := Marshalls.raw_to_base64(JSON.stringify({"Authorisation": claims, "Signature": "two"}).to_utf8_buffer())
var match_net := MatchNet.new()
assert_true(match_net.configure_join_authorisations([first, duplicate], {"match_id": "match-1", "server_id": "server-1", "protocol": "1", "protocol_version": 1}), "duplicate fixture configures for structural inspection")
assert_eq(match_net.assigned_player_slots().size(), 0, "duplicate identity/slot roster fails closed")
func test_allocated_join_authorisation_verifies_canonical_hmac() -> void:
# This envelope is generated from server/domain.JoinAuthorisationBytes with
# HMAC-SHA256(test-key), proving the Godot verifier agrees with the Go
+2
View File
@@ -1404,3 +1404,5 @@ Allocated supervisor launch arguments now have a direct regression guard: author
Read-only authenticated queue, proposal, assignment, legacy profile, and ranked-profile routes now emit lifecycle-safe observability events for successful, rejected, and not-found reads. An API regression exercises all five real HTTP routes and verifies the event set; event fields remain free of credentials. This closes the local read-route portion of task 8.44; metrics/traces export, dashboards, and alert routing remain operational work.
Allocated join admission now retains and applies the signed assignments authoritative team and global slot: peer order can no longer rebalance a valid allocation, and inconsistent team/slot claims are rejected before roster admission. The server exposes the verified assignment list for allocation-aware startup and uses the per-team spawn index derived from the assigned slot. Go verification is clean; Godot execution remains blocked by the documented macOS pre-test crash.
Allocated boot now also validates the complete signed roster shape before opening the gameplay endpoint: malformed claims, duplicate player identities, duplicate slots, and team/global-slot mismatches fail closed rather than leaving a partially usable server. The Godot `--check-only` attempt still reaches the known macOS renderer/ZSTD crash before script parsing, so this startup guard remains statically reviewed and covered by the existing signed-claim tests pending a working Godot runtime.