fix(multiplayer): expire client sessions proactively

This commit is contained in:
Josh Creek
2026-09-01 22:13:41 +01:00
parent 1e5825b096
commit cfc82bcea5
3 changed files with 40 additions and 0 deletions
+31
View File
@@ -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")