mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
fix(multiplayer): validate client resource paths
This commit is contained in:
@@ -129,7 +129,7 @@ static func websocket_url(url: String) -> String:
|
||||
|
||||
|
||||
func queue_create(ticket_id: String, playlist: String, client_build: String, protocol_version: int) -> Error:
|
||||
if ticket_id.is_empty() or (playlist != "casual" and playlist != "ranked") or client_build.is_empty() or protocol_version < 1:
|
||||
if not is_valid_resource_id(ticket_id) or (playlist != "casual" and playlist != "ranked") or client_build.is_empty() or protocol_version < 1:
|
||||
return ERR_INVALID_PARAMETER
|
||||
if not state.begin_queue(ticket_id, playlist):
|
||||
return ERR_INVALID_PARAMETER
|
||||
@@ -176,13 +176,13 @@ func can_retry_last_mutation() -> bool:
|
||||
|
||||
|
||||
func recover_queue(ticket_id: String) -> Error:
|
||||
if ticket_id.is_empty():
|
||||
if not is_valid_resource_id(ticket_id):
|
||||
return ERR_INVALID_PARAMETER
|
||||
return _start_request("queue_recover", HTTPClient.METHOD_GET, "/v1/queue/" + ticket_id, {}, "")
|
||||
|
||||
|
||||
func recover_proposal(proposal_id: String) -> Error:
|
||||
if proposal_id.is_empty():
|
||||
if not is_valid_resource_id(proposal_id):
|
||||
return ERR_INVALID_PARAMETER
|
||||
return _start_request("proposal_recover", HTTPClient.METHOD_GET, "/v1/proposals/" + proposal_id, {}, "")
|
||||
|
||||
@@ -200,7 +200,7 @@ func fetch_ranked_profile() -> Error:
|
||||
|
||||
|
||||
func fetch_assignment(match_id: String) -> Error:
|
||||
if match_id.is_empty() or player_id.is_empty():
|
||||
if not is_valid_resource_id(match_id) or player_id.is_empty():
|
||||
return ERR_INVALID_PARAMETER
|
||||
return _start_request("assignment", HTTPClient.METHOD_GET, "/v1/assignments/" + match_id, {}, "")
|
||||
|
||||
@@ -247,19 +247,19 @@ static func _split_assignment_endpoint(value: String) -> Dictionary:
|
||||
|
||||
|
||||
func heartbeat(ticket_id: String, expected_revision: int) -> Error:
|
||||
if ticket_id.is_empty() or expected_revision < 0:
|
||||
if not is_valid_resource_id(ticket_id) or expected_revision < 0:
|
||||
return ERR_INVALID_PARAMETER
|
||||
return _start_request("queue_heartbeat", HTTPClient.METHOD_POST, "/v1/queue/%s/heartbeat" % ticket_id, {}, _idempotency_key("heartbeat"), expected_revision)
|
||||
|
||||
|
||||
func cancel_queue(ticket_id: String, expected_revision: int) -> Error:
|
||||
if ticket_id.is_empty() or expected_revision < 0 or not state.can_cancel():
|
||||
if not is_valid_resource_id(ticket_id) or expected_revision < 0 or not state.can_cancel():
|
||||
return ERR_INVALID_PARAMETER
|
||||
return _start_request("queue_cancel", HTTPClient.METHOD_POST, "/v1/queue/%s/cancel" % ticket_id, {}, _idempotency_key("cancel"), expected_revision)
|
||||
|
||||
|
||||
func respond_to_proposal(proposal_id: String, accept: bool, expected_revision: int) -> Error:
|
||||
if proposal_id.is_empty() or expected_revision < 0:
|
||||
if not is_valid_resource_id(proposal_id) or expected_revision < 0:
|
||||
return ERR_INVALID_PARAMETER
|
||||
var action := "accept" if accept else "decline"
|
||||
return _start_request("proposal_" + action, HTTPClient.METHOD_POST, "/v1/proposals/%s/%s" % [proposal_id, action], {}, _idempotency_key("proposal"), expected_revision)
|
||||
|
||||
@@ -123,6 +123,12 @@ func test_retryable_mutation_policy_only_retries_safe_failures() -> void:
|
||||
assert_true(not ControlPlaneClient.is_retryable_mutation_response(409), "revision/idempotency conflict is not blindly replayed")
|
||||
|
||||
|
||||
func test_rest_resource_identifiers_use_the_opaque_contract_shape() -> void:
|
||||
assert_true(ControlPlaneClient.is_valid_resource_id("ticket_1234567890"), "contract-sized resource id is accepted")
|
||||
assert_true(not ControlPlaneClient.is_valid_resource_id("ticket-1"), "short resource id is rejected")
|
||||
assert_true(not ControlPlaneClient.is_valid_resource_id("ticket_1234567890/path"), "path separator is rejected")
|
||||
|
||||
|
||||
func test_queue_revision_conflicts_schedule_authoritative_recovery() -> void:
|
||||
assert_true(ControlPlaneClient.should_recover_queue_after_conflict("queue_heartbeat", 409, "ticket-1"), "stale heartbeat recovers the queue ticket")
|
||||
assert_true(ControlPlaneClient.should_recover_queue_after_conflict("queue_cancel", 409, "ticket-1"), "stale cancellation recovers the queue ticket")
|
||||
|
||||
@@ -1597,6 +1597,8 @@ Assignment projection now rejects fractional `slot` and `protocol_version` value
|
||||
|
||||
Allocated `ServerConfig` startup now enforces the opaque match/server ID contract, rejecting short or unsafe allocation flags before process launch.
|
||||
|
||||
Authenticated client REST methods now enforce opaque ticket, proposal, and match IDs before constructing request paths, preventing malformed identifiers from crossing the URL boundary.
|
||||
|
||||
Signed MatchNet claims now also require exact JSON string/integer types for every identity, protocol, expiry, slot, team, and generation field; string-number coercion is rejected before canonical signature verification.
|
||||
|
||||
Presentation progress: a shared `Game/themes/cosmic_clash_theme.tres` now gives the menu, lobby, matchmaking, and settings surfaces consistent button, input, option, and label styling. The custom-font portion of `TODO.md` remains open until a distributable font asset is selected.
|
||||
|
||||
Reference in New Issue
Block a user