mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 08:23:45 +00:00
fix(multiplayer): verify complete durable assignment rosters
This commit is contained in:
@@ -76,8 +76,20 @@ FROM assignments
|
||||
WHERE match_id = $1 AND server_id = $2 AND expires_at > $3
|
||||
ORDER BY slot, player_id`
|
||||
|
||||
const AssignmentExpectedRosterSQL = `SELECT mp.player_id, i.steam_id, mp.slot, mp.team
|
||||
FROM match_participants mp
|
||||
JOIN identities i ON i.player_id = mp.player_id
|
||||
JOIN matches m ON m.match_id = mp.match_id
|
||||
JOIN allocations a ON a.allocation_id = m.allocation_id AND a.match_id = m.match_id AND a.server_id = m.server_id
|
||||
WHERE mp.match_id = $1 AND m.allocation_id = $2 AND m.server_id = $3
|
||||
AND m.region = $4 AND m.protocol_version = $5
|
||||
AND a.region = $4 AND a.build = $6 AND a.protocol_version = $5 AND a.transport = $7
|
||||
AND a.state = 'ALLOCATED' AND mp.participation_active
|
||||
ORDER BY mp.player_id
|
||||
FOR UPDATE OF mp`
|
||||
|
||||
func validateDurableAssignment(assignment DurableAssignment) error {
|
||||
if assignment.MatchID == "" || assignment.PlayerID == "" || assignment.AllocationID == "" || assignment.ServerID == "" || assignment.Slot < 0 || assignment.Slot > 5 || (assignment.Region != "EU" && assignment.Region != "NA") || assignment.ClientBuild == "" || assignment.ProtocolVersion < 1 || (assignment.Transport != "enet" && assignment.Transport != "steam_sdr") || assignment.Endpoint == "" || assignment.JoinAuthorisation == "" || len(assignment.ManifestDigest) == 0 || assignment.ExpiresAt.IsZero() || assignment.Revision < 0 {
|
||||
if assignment.MatchID == "" || assignment.PlayerID == "" || assignment.AllocationID == "" || assignment.ServerID == "" || assignment.Slot < 0 || assignment.Slot > 5 || (assignment.Region != "EU" && assignment.Region != "NA") || assignment.ClientBuild == "" || assignment.ProtocolVersion < 1 || (assignment.Transport != "enet" && assignment.Transport != "steam_sdr") || assignment.Endpoint == "" || assignment.JoinAuthorisation == "" || len(assignment.ManifestDigest) == 0 || assignment.ExpiresAt.IsZero() || assignment.Revision == 0 {
|
||||
return fmt.Errorf("invalid durable assignment")
|
||||
}
|
||||
return nil
|
||||
@@ -112,21 +124,42 @@ func SaveAssignments(ctx context.Context, db *sql.DB, assignments []DurableAssig
|
||||
if db == nil || len(assignments) == 0 {
|
||||
return fmt.Errorf("invalid assignment batch")
|
||||
}
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
if err := validateAssignmentBatch(assignments); err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
return RunSerializable(ctx, db, DefaultSerializableAttempts, func(ctx context.Context, tx *sql.Tx) error {
|
||||
return saveAssignmentsTx(ctx, tx, assignments)
|
||||
})
|
||||
}
|
||||
|
||||
func validateAssignmentBatch(assignments []DurableAssignment) error {
|
||||
if len(assignments) == 0 {
|
||||
return fmt.Errorf("invalid assignment batch")
|
||||
}
|
||||
first := assignments[0]
|
||||
seen := make(map[string]struct{}, len(assignments))
|
||||
seenSlots := make(map[int]struct{}, len(assignments))
|
||||
for _, assignment := range assignments {
|
||||
if err := validateDurableAssignment(assignment); err != nil {
|
||||
return err
|
||||
}
|
||||
key := assignment.MatchID + "\x00" + assignment.PlayerID
|
||||
if _, ok := seen[key]; ok {
|
||||
if assignment.MatchID != first.MatchID || assignment.AllocationID != first.AllocationID || assignment.ServerID != first.ServerID || assignment.Region != first.Region || assignment.ClientBuild != first.ClientBuild || assignment.ProtocolVersion != first.ProtocolVersion || assignment.Transport != first.Transport || assignment.Endpoint != first.Endpoint || string(assignment.ManifestDigest) != string(first.ManifestDigest) || assignment.Revision != first.Revision {
|
||||
return fmt.Errorf("mixed assignment batch")
|
||||
}
|
||||
if _, ok := seen[assignment.PlayerID]; ok {
|
||||
return fmt.Errorf("duplicate assignment in batch")
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
if _, ok := seenSlots[assignment.Slot]; ok {
|
||||
return fmt.Errorf("duplicate assignment slot in batch")
|
||||
}
|
||||
seen[assignment.PlayerID] = struct{}{}
|
||||
seenSlots[assignment.Slot] = struct{}{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func saveAssignmentsTx(ctx context.Context, tx *sql.Tx, assignments []DurableAssignment) error {
|
||||
for _, assignment := range assignments {
|
||||
result, err := tx.ExecContext(ctx, AssignmentUpsertSQL, assignment.MatchID, assignment.PlayerID, assignment.AllocationID, assignment.ServerID, assignment.Slot, assignment.Region, assignment.ClientBuild, assignment.ProtocolVersion, assignment.Transport, assignment.Endpoint, assignment.JoinAuthorisation, assignment.ManifestDigest, assignment.ExpiresAt, assignment.Revision)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -139,7 +172,7 @@ func SaveAssignments(ctx context.Context, db *sql.DB, assignments []DurableAssig
|
||||
return fmt.Errorf("assignment persistence conflict")
|
||||
}
|
||||
}
|
||||
return tx.Commit()
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveVerifiedAssignmentRoster converts the backend-verified signed roster to
|
||||
@@ -179,7 +212,59 @@ func SaveVerifiedAssignmentRoster(ctx context.Context, db *sql.DB, assignment do
|
||||
ManifestDigest: digest[:], ExpiresAt: auth.ExpiresAt, Revision: 1,
|
||||
})
|
||||
}
|
||||
return SaveAssignments(ctx, db, rows)
|
||||
if db == nil {
|
||||
return fmt.Errorf("invalid assignment database")
|
||||
}
|
||||
if err := validateAssignmentBatch(rows); err != nil {
|
||||
return err
|
||||
}
|
||||
return RunSerializable(ctx, db, DefaultSerializableAttempts, func(ctx context.Context, tx *sql.Tx) error {
|
||||
if err := validateExpectedAssignmentRoster(ctx, tx, assignment, roster); err != nil {
|
||||
return err
|
||||
}
|
||||
return saveAssignmentsTx(ctx, tx, rows)
|
||||
})
|
||||
}
|
||||
|
||||
func validateExpectedAssignmentRoster(ctx context.Context, tx *sql.Tx, assignment domain.Assignment, roster []domain.SignedJoinAuthorisation) error {
|
||||
rows, err := tx.QueryContext(ctx, AssignmentExpectedRosterSQL,
|
||||
assignment.Allocation.MatchID, assignment.Allocation.AllocationID, assignment.Allocation.ServerID,
|
||||
assignment.Allocation.Region, assignment.Allocation.Protocol, assignment.Allocation.Build, assignment.Allocation.Transport)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
type expectedPlayer struct {
|
||||
steamID string
|
||||
slot int
|
||||
team int
|
||||
}
|
||||
expected := make(map[string]expectedPlayer, len(roster))
|
||||
for rows.Next() {
|
||||
var playerID string
|
||||
var player expectedPlayer
|
||||
if err := rows.Scan(&playerID, &player.steamID, &player.slot, &player.team); err != nil {
|
||||
return err
|
||||
}
|
||||
expected[playerID] = player
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(expected) == 0 || len(expected) != len(roster) {
|
||||
return fmt.Errorf("signed assignment roster is incomplete")
|
||||
}
|
||||
for _, signed := range roster {
|
||||
auth := signed.Authorisation
|
||||
player, ok := expected[auth.PlayerID]
|
||||
if !ok || player.steamID != auth.SteamID || player.slot != auth.Slot || player.team != auth.Team {
|
||||
return fmt.Errorf("signed assignment roster does not match durable participants")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateSignedRosterEntry(assignment domain.Assignment, signed domain.SignedJoinAuthorisation, verify func([]byte, []byte) bool) error {
|
||||
|
||||
Reference in New Issue
Block a user