mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 08:23:45 +00:00
fix(multiplayer): expire client sessions proactively
This commit is contained in:
@@ -52,6 +52,8 @@ func _ready() -> void:
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if not auth_expired and is_session_expired(session_expires_at):
|
||||
_expire_session()
|
||||
if _websocket == null:
|
||||
return
|
||||
_websocket.poll()
|
||||
@@ -269,6 +271,21 @@ static func is_valid_access_token(token: String) -> bool:
|
||||
return separator > 0 and separator < token.length() - 1 and token.length() <= 4096 and not token.contains("\r") and not token.contains("\n")
|
||||
|
||||
|
||||
static func is_session_expired(expires_at: String, now_unix: int = -1) -> bool:
|
||||
if expires_at.is_empty():
|
||||
return false
|
||||
var timestamp_pattern := RegEx.create_from_string("^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:\\d{2})$")
|
||||
if timestamp_pattern.search(expires_at) == null:
|
||||
return true
|
||||
var expiry_unix := Time.get_unix_time_from_datetime_string(expires_at)
|
||||
if expiry_unix < 0:
|
||||
return true
|
||||
var current_unix := now_unix
|
||||
if current_unix < 0:
|
||||
current_unix = int(Time.get_unix_time_from_system())
|
||||
return expiry_unix <= current_unix
|
||||
|
||||
|
||||
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
|
||||
|
||||
@@ -287,6 +304,9 @@ func _start_request(operation: String, method: HTTPClient.Method, path: String,
|
||||
return ERR_BUSY if not _operation.is_empty() else ERR_UNAUTHORIZED
|
||||
if operation != "steam_session" and access_token.is_empty():
|
||||
return ERR_UNAUTHORIZED
|
||||
if operation != "steam_session" and is_session_expired(session_expires_at):
|
||||
_expire_session()
|
||||
return ERR_UNAUTHORIZED
|
||||
var headers := PackedStringArray(["Accept: application/json"])
|
||||
if operation != "steam_session":
|
||||
headers.append("Authorization: Bearer " + access_token)
|
||||
@@ -306,6 +326,17 @@ func _start_request(operation: String, method: HTTPClient.Method, path: String,
|
||||
return OK
|
||||
|
||||
|
||||
func _expire_session() -> void:
|
||||
if auth_expired:
|
||||
return
|
||||
access_token = ""
|
||||
auth_expired = true
|
||||
disconnect_event_stream()
|
||||
state.fail("Session expired; sign in again")
|
||||
ranked_profile.set_error("Session expired; sign in again")
|
||||
session_expired.emit()
|
||||
|
||||
|
||||
func _on_request_completed(result: HTTPRequest.Result, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
|
||||
var operation := _operation
|
||||
_operation = ""
|
||||
|
||||
@@ -43,6 +43,13 @@ func test_ticket_normalization_derives_authoritative_enqueue_time() -> void:
|
||||
assert_eq(int(normalized["enqueued_at_unix"]), 1000, "RFC3339 enqueue time is converted to epoch")
|
||||
|
||||
|
||||
func test_session_expiry_is_checked_at_the_boundary_and_fails_closed() -> void:
|
||||
assert_true(not ControlPlaneClient.is_session_expired("", 1000), "legacy sessions without an expiry remain compatible")
|
||||
assert_true(not ControlPlaneClient.is_session_expired("1970-01-01T00:16:40Z", 999), "session remains valid before expiry")
|
||||
assert_true(ControlPlaneClient.is_session_expired("1970-01-01T00:16:40Z", 1000), "session expires at the exact boundary")
|
||||
assert_true(ControlPlaneClient.is_session_expired("not-a-timestamp", 1000), "malformed non-empty expiry fails closed")
|
||||
|
||||
|
||||
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"}
|
||||
assert_true(ControlPlaneClient._valid_websocket_event(envelope), "valid state event is accepted")
|
||||
|
||||
Reference in New Issue
Block a user