mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
fix(multiplayer): validate ranked season ids
This commit is contained in:
@@ -37,7 +37,11 @@ func apply(payload: Dictionary) -> bool:
|
||||
ranked_games = next_games
|
||||
tier = next_tier
|
||||
provisional = bool(payload["provisional"])
|
||||
season_id = String(payload.get("season_id", ""))
|
||||
season_id = ""
|
||||
if payload.has("season_id"):
|
||||
if not payload["season_id"] is String or not is_valid_opaque_id(String(payload["season_id"])):
|
||||
return _reject("Profile response contains invalid season identifier")
|
||||
season_id = String(payload["season_id"])
|
||||
season_ends_at_unix = 0
|
||||
if payload.has("season_ends_at"):
|
||||
if not payload["season_ends_at"] is String or not is_valid_season_timestamp(String(payload["season_ends_at"])):
|
||||
@@ -58,6 +62,13 @@ static func is_valid_season_timestamp(value: String) -> bool:
|
||||
return timestamp_pattern.search(value) != null
|
||||
|
||||
|
||||
static func is_valid_opaque_id(value: String) -> bool:
|
||||
if value.length() < 16 or value.length() > 128:
|
||||
return false
|
||||
var id_pattern := RegEx.create_from_string("^[A-Za-z0-9_-]+$")
|
||||
return id_pattern.search(value) != null
|
||||
|
||||
|
||||
func set_error(reason: String) -> void:
|
||||
available = false
|
||||
error_message = reason
|
||||
|
||||
@@ -150,7 +150,7 @@ func test_assignment_endpoint_split_never_accepts_url_or_bad_port() -> void:
|
||||
|
||||
func test_ranked_profile_is_backend_display_data_and_rejects_unsafe_values() -> void:
|
||||
var profile := RankedProfileState.new()
|
||||
assert_true(profile.apply({"rating": 1500.0, "rd": 200.0, "volatility": 0.06, "ranked_games": 3, "tier": "GOLD", "provisional": true, "season_id": "s1"}), "valid profile applies")
|
||||
assert_true(profile.apply({"rating": 1500.0, "rd": 200.0, "volatility": 0.06, "ranked_games": 3, "tier": "GOLD", "provisional": true, "season_id": "season_1234567890"}), "valid profile applies")
|
||||
assert_eq(profile.display_text(), "Provisional · 3 ranked games", "provisional status overrides tier presentation")
|
||||
assert_true(not profile.apply({"rating": -1.0, "rd": 200.0, "volatility": 0.06, "ranked_games": 3, "tier": "GOLD", "provisional": false}), "negative rating is rejected")
|
||||
assert_true(not profile.available, "unsafe response is not displayed")
|
||||
@@ -160,8 +160,10 @@ func test_ranked_profile_is_backend_display_data_and_rejects_unsafe_values() ->
|
||||
|
||||
func test_ranked_profile_projects_and_bounds_season_countdown() -> void:
|
||||
var profile := RankedProfileState.new()
|
||||
assert_true(profile.apply({"rating": 1500.0, "rd": 200.0, "volatility": 0.06, "ranked_games": 10, "tier": "GOLD", "provisional": false, "season_id": "s1", "season_ends_at": "1970-01-03T00:00:00Z"}), "season end applies")
|
||||
assert_true(profile.apply({"rating": 1500.0, "rd": 200.0, "volatility": 0.06, "ranked_games": 10, "tier": "GOLD", "provisional": false, "season_id": "season_1234567890", "season_ends_at": "1970-01-03T00:00:00Z"}), "season end applies")
|
||||
assert_true(profile.display_text(1000).contains("Season ends in 2d"), "countdown rounds up remaining season time")
|
||||
assert_true(profile.display_text(300000).contains("Season ends in 0d"), "expired season countdown is clamped")
|
||||
assert_true(not profile.apply({"rating": 1500.0, "rd": 200.0, "volatility": 0.06, "ranked_games": 10, "tier": "GOLD", "provisional": false, "season_ends_at": "not-a-timestamp"}), "malformed season expiry is rejected")
|
||||
assert_true(not profile.apply({"rating": 1500.0, "rd": 200.0, "volatility": 0.06, "ranked_games": 10, "tier": "GOLD", "provisional": false, "season_ends_at": 123}), "non-string season expiry is rejected")
|
||||
assert_true(not profile.apply({"rating": 1500.0, "rd": 200.0, "volatility": 0.06, "ranked_games": 10, "tier": "GOLD", "provisional": false, "season_id": "short"}), "short season identifier is rejected")
|
||||
assert_true(not profile.apply({"rating": 1500.0, "rd": 200.0, "volatility": 0.06, "ranked_games": 10, "tier": "GOLD", "provisional": false, "season_id": 123}), "non-string season identifier is rejected")
|
||||
|
||||
@@ -1579,6 +1579,8 @@ MatchNet join-authorisation admission now applies the same expiry format guard b
|
||||
|
||||
Ranked profile season metadata now validates optional expiry type and RFC3339 format before deriving the UI countdown, rejecting malformed server projections instead of silently displaying a profile without season context.
|
||||
|
||||
Ranked profile `season_id` now enforces the OpenAPI opaque-ID shape and exact string type, preventing undersized or coerced identifiers from entering the client projection.
|
||||
|
||||
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