mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
fix(multiplayer): validate persisted matchmaking ids
This commit is contained in:
@@ -218,14 +218,14 @@ func restore_snapshot(saved: Dictionary) -> bool:
|
||||
return false
|
||||
var saved_phase := String(saved.get("phase", IDLE))
|
||||
var saved_ticket_id := String(saved.get("ticket_id", ""))
|
||||
if saved_ticket_id.is_empty() or not _is_ticket_state(saved_phase):
|
||||
if not _valid_opaque_id(saved_ticket_id) or not _is_ticket_state(saved_phase):
|
||||
return false
|
||||
var saved_playlist := String(saved.get("playlist", ""))
|
||||
if saved_playlist != "casual" and saved_playlist != "ranked":
|
||||
return false
|
||||
var saved_proposal_id := String(saved.get("proposal_id", ""))
|
||||
var saved_proposal_state := String(saved.get("proposal_state", ""))
|
||||
if saved_proposal_state not in ["", "OPEN", "ACCEPTED", "DECLINED", "EXPIRED", "CANCELLED"] or (not saved_proposal_state.is_empty() and saved_proposal_id.is_empty()):
|
||||
if saved_proposal_state not in ["", "OPEN", "ACCEPTED", "DECLINED", "EXPIRED", "CANCELLED"] or (not saved_proposal_state.is_empty() and not _valid_opaque_id(saved_proposal_id)) or (saved_proposal_state.is_empty() and not saved_proposal_id.is_empty() and not _valid_opaque_id(saved_proposal_id)):
|
||||
return false
|
||||
ticket_id = saved_ticket_id
|
||||
playlist = saved_playlist
|
||||
@@ -349,3 +349,10 @@ func _valid_epoch(value: Variant) -> bool:
|
||||
if value is float:
|
||||
return is_finite(float(value)) and float(value) >= 0.0 and float(value) == floor(float(value))
|
||||
return false
|
||||
|
||||
|
||||
func _valid_opaque_id(value: String) -> bool:
|
||||
if value.length() < 16 or value.length() > 128:
|
||||
return false
|
||||
var resource_pattern := RegEx.create_from_string("^[A-Za-z0-9_-]+$")
|
||||
return resource_pattern.search(value) != null
|
||||
|
||||
@@ -169,7 +169,7 @@ func test_expiry_is_distinct_from_generic_failure_and_remains_visible() -> void:
|
||||
|
||||
func test_restart_restore_requires_valid_identity_and_requests_authoritative_recovery() -> void:
|
||||
var state := MatchmakingState.new()
|
||||
assert_true(state.restore_snapshot({"phase": "QUEUED", "ticket_id": "ticket-1", "playlist": "casual", "revision": 2}), "valid active snapshot restores")
|
||||
assert_true(state.restore_snapshot({"phase": "QUEUED", "ticket_id": "ticket_1234567890", "playlist": "casual", "revision": 2}), "valid active snapshot restores")
|
||||
assert_true(state.needs_resync, "restored active state must recover from the server")
|
||||
assert_eq(state.revision, 2, "revision is retained for diagnostics")
|
||||
assert_true(not state.restore_snapshot({"phase": "QUEUED", "ticket_id": "", "playlist": "casual"}), "missing ticket identity is rejected")
|
||||
@@ -182,9 +182,11 @@ func test_restart_restore_requires_valid_identity_and_requests_authoritative_rec
|
||||
|
||||
func test_authoritative_enqueue_time_survives_wait_projection_and_restore() -> void:
|
||||
var state := MatchmakingState.new()
|
||||
assert_true(state.begin_queue("ticket-wait", "casual"), "queue setup succeeds")
|
||||
assert_true(state.apply_ticket_update({"ticket_id": "ticket-wait", "revision": 0, "state": "QUEUED", "playlist": "casual", "enqueued_at_unix": 1000}), "server enqueue timestamp applies")
|
||||
assert_true(state.begin_queue("ticket_wait_123456", "casual"), "queue setup succeeds")
|
||||
assert_true(state.apply_ticket_update({"ticket_id": "ticket_wait_123456", "revision": 0, "state": "QUEUED", "playlist": "casual", "enqueued_at_unix": 1000}), "server enqueue timestamp applies")
|
||||
assert_eq(state.waited_seconds(1065), 65, "wait uses server enqueue time")
|
||||
var restored := MatchmakingState.new()
|
||||
assert_true(restored.restore_snapshot(state.snapshot()), "snapshot restores")
|
||||
assert_eq(restored.waited_seconds(1065), 65, "authoritative wait survives restore")
|
||||
assert_true(not restored.restore_snapshot({"phase": "QUEUED", "ticket_id": "ticket-short", "playlist": "casual"}), "short snapshot ticket id is rejected")
|
||||
assert_true(not restored.restore_snapshot({"phase": "PROPOSED", "ticket_id": "ticket_wait_123456", "playlist": "casual", "proposal_id": "proposal/unsafe", "proposal_state": "OPEN"}), "unsafe snapshot proposal id is rejected")
|
||||
|
||||
Reference in New Issue
Block a user