mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
fix(multiplayer): enforce opaque event resource ids
This commit is contained in:
@@ -300,6 +300,13 @@ static func is_valid_rfc3339_timestamp(value: String) -> bool:
|
||||
return timestamp_pattern.search(value) != null
|
||||
|
||||
|
||||
static func is_valid_resource_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
|
||||
|
||||
|
||||
static func is_retryable_mutation_response(response_code: int) -> bool:
|
||||
return response_code == 0 or response_code == HTTPClient.RESPONSE_REQUEST_TIMEOUT or response_code == HTTPClient.RESPONSE_TOO_MANY_REQUESTS or response_code >= 500
|
||||
|
||||
@@ -474,7 +481,7 @@ static func _valid_websocket_event(event: Dictionary) -> bool:
|
||||
return false
|
||||
if not event.has("revision") or not _valid_revision(event["revision"]):
|
||||
return false
|
||||
if not event.has("resource_id") or not event["resource_id"] is String or String(event["resource_id"]).is_empty():
|
||||
if not event.has("resource_id") or not event["resource_id"] is String or not is_valid_resource_id(String(event["resource_id"])):
|
||||
return false
|
||||
if not event.has("occurred_at") or not event["occurred_at"] is String or not is_valid_rfc3339_timestamp(String(event["occurred_at"])):
|
||||
return false
|
||||
|
||||
@@ -54,7 +54,7 @@ func test_session_expiry_is_checked_at_the_boundary_and_fails_closed() -> void:
|
||||
|
||||
|
||||
func test_websocket_event_validation_requires_contract_specific_fields() -> void:
|
||||
var envelope := {"event": "state_changed", "revision": 1, "resource_id": "ticket-1", "occurred_at": "2026-08-31T12:00:00Z", "state": "QUEUED"}
|
||||
var envelope := {"event": "state_changed", "revision": 1, "resource_id": "ticket_123456789", "occurred_at": "2026-08-31T12:00:00Z", "state": "QUEUED"}
|
||||
assert_true(ControlPlaneClient._valid_websocket_event(envelope), "valid state event is accepted")
|
||||
var accepted := envelope.duplicate()
|
||||
accepted["state"] = "ACCEPTED"
|
||||
@@ -66,7 +66,7 @@ func test_websocket_event_validation_requires_contract_specific_fields() -> void
|
||||
var bad_state := envelope.duplicate()
|
||||
bad_state["state"] = "SECRET"
|
||||
assert_true(not ControlPlaneClient._valid_websocket_event(bad_state), "unknown state event is rejected")
|
||||
var assignment := {"event": "assignment_changed", "revision": 0, "resource_id": "match-1", "occurred_at": "2026-08-31T12:00:00Z", "match_id": "match-1", "server_id": "server-1"}
|
||||
var assignment := {"event": "assignment_changed", "revision": 0, "resource_id": "match_1234567890", "occurred_at": "2026-08-31T12:00:00Z", "match_id": "match-1", "server_id": "server-1"}
|
||||
assert_true(ControlPlaneClient._valid_websocket_event(assignment), "complete assignment event is accepted")
|
||||
assignment.erase("server_id")
|
||||
assert_true(not ControlPlaneClient._valid_websocket_event(assignment), "incomplete assignment event is rejected")
|
||||
@@ -79,6 +79,12 @@ func test_websocket_event_validation_requires_contract_specific_fields() -> void
|
||||
var malformed_time := envelope.duplicate()
|
||||
malformed_time["occurred_at"] = "yesterday"
|
||||
assert_true(not ControlPlaneClient._valid_websocket_event(malformed_time), "malformed event timestamp is rejected")
|
||||
var short_resource := envelope.duplicate()
|
||||
short_resource["resource_id"] = "short"
|
||||
assert_true(not ControlPlaneClient._valid_websocket_event(short_resource), "short resource identifier is rejected")
|
||||
var unsafe_resource := envelope.duplicate()
|
||||
unsafe_resource["resource_id"] = "ticket_123456789/secret"
|
||||
assert_true(not ControlPlaneClient._valid_websocket_event(unsafe_resource), "resource identifier with separators is rejected")
|
||||
|
||||
|
||||
func test_websocket_reconnect_defers_recovery_while_http_mutation_is_in_flight() -> void:
|
||||
|
||||
@@ -1562,3 +1562,5 @@ Client sessions now fail closed at the expiry boundary and proactively clear cre
|
||||
Queue heartbeat and cancellation revision conflicts now schedule the same authoritative ticket recovery as proposal conflicts, preventing stale client actions from leaving the visible queue state unresolved. Adversarial operation/status/identity coverage is included.
|
||||
|
||||
WebSocket event envelopes now require RFC3339 timestamps rather than merely non-empty text, matching the versioned contract; session-expiry format checks use the same boundary validator. Malformed-format adversarial coverage is included.
|
||||
|
||||
WebSocket event resource identifiers now enforce the contract’s opaque 16–128 character allowlist, preventing path/separator text or undersized identifiers from entering the client projection.
|
||||
|
||||
Reference in New Issue
Block a user