Proxy/NetworkStorageProxyClient.cs

The network storage proxy client. Routes external API calls and file document requests through the game host via a proxy delegate.

Networking
using System;
using System.Text.Json;
using System.Threading.Tasks;

namespace Sandbox;

public static partial class NetworkStorage
{
	// ── Proxy Methods (non-host clients route through host) ──

	/// <summary>
	/// Route an endpoint call through the game host via the registered RequestProxy delegate.
	/// </summary>
	private static async Task<JsonElement?> CallEndpointViaProxy( string slug, object input, bool allowSecurityRetry = true, bool allowClockRetry = true )
	{
		var steamId = Game.SteamId.ToString();
		EndpointSecurityRequest securityRequest;
		string inputJson;
		try
		{
			securityRequest = await BuildEndpointSecurityRequest( slug, input ?? new { }, allowAuthSession: false );
			if ( securityRequest is null )
				return null;
			inputJson = JsonSerializer.Serialize( securityRequest.Body );
		}
		catch ( Exception ex )
		{
			if ( NetworkStorageLogConfig.LogErrors )
				Log.Warning( $"[NetworkStorage] {slug} PROXY request build failed — {ex.Message}" );
			RecordEndpointError( slug, ex is TimeoutException ? "PROXY_TIMEOUT" : "PROXY_FAILED", ex.Message );
			return null;
		}
		string proxySlug = slug;

		// Get the client's auth token as consent proof (included in HMAC signature)
		string clientToken = null;
		try
		{
			clientToken = await GetAuthTokenWithRetry( $"{slug} proxy consent ({steamId})" );
		}
		catch ( Exception ex )
		{
			if ( NetworkStorageLogConfig.LogErrors )
				Log.Warning( $"[NetworkStorage] {slug} PROXY: failed to get client token — {ex.Message}" );
		}

		if ( NetworkStorageLogConfig.LogProxy )
		{
			NetLog.Request( slug, $"PROXY → host (steamId={steamId})" );
			Log.Info( $"[NetworkStorage] {slug} PROXY request via host for steamId={steamId} route={(string.IsNullOrEmpty( proxySlug ) ? "obfuscated" : proxySlug)} input={inputJson ?? "null"}" );
		}

		try
		{
			var result = await RequestProxy( steamId, clientToken ?? "", proxySlug, inputJson );
			if ( result == null )
			{
				if ( NetworkStorageLogConfig.LogErrors )
				{
					Log.Warning( $"[NetworkStorage] {slug} PROXY returned null — host may have rejected the request" );
					NetLog.Error( slug, "Proxy returned null" );
				}
				RecordEndpointError( slug, "PROXY_FAILED", "Proxy returned null" );
				return null;
			}

			var reply = NetworkStorageProxyEnvelope.Read( result, NetworkStorageProxyEnvelope.EndpointKind );
			if ( !reply.Ok )
			{
				RecordEndpointError( slug, reply.ErrorCode, reply.ErrorMessage );
				return null;
			}

			result = UnsealEndpointResponse( securityRequest.ResponseContext, slug, reply.Body );
			if ( result is null )
				return null;
			if ( NetworkStorageLogConfig.LogProxy )
				Log.Info( $"[NetworkStorage] {slug} PROXY → {TruncateJson( result, 200 )}" );
			var parsed = ParseResponse( slug, result );
			if ( parsed.HasValue && NetworkStorageLogConfig.LogProxy )
				NetLog.Response( slug, $"PROXY OK — {TruncateJson( parsed.Value )}" );
			if ( !parsed.HasValue && allowSecurityRetry && IsRetrySafeSecurityRefusal( result ) )
			{
				await EnsureRuntimeSecurityConfigAsync( "proxy-security-retry", forceRefresh: true );
				return await CallEndpointViaProxy( slug, input, allowSecurityRetry: false, allowClockRetry: allowClockRetry );
			}
			if ( !parsed.HasValue && allowClockRetry && LastEndpointErrorWasStaleRequest( slug ) )
			{
				Log.Info( $"[NetworkStorage] {slug} request id was outside the server replay window, retrying with server time." );
				return await CallEndpointViaProxy( slug, input, allowSecurityRetry, allowClockRetry: false );
			}
			return parsed;
		}
		catch ( Exception ex )
		{
			if ( NetworkStorageLogConfig.LogErrors )
			{
				Log.Warning( $"[NetworkStorage] {slug} PROXY FAILED — {ex.Message}" );
				NetLog.Error( slug, $"Proxy error: {ex.Message}" );
			}
			RecordEndpointError( slug, ex is TimeoutException ? "PROXY_TIMEOUT" : "PROXY_FAILED", ex.Message );
			return null;
		}
	}

	/// <summary>
	/// Route a document read through the game host via the registered DocumentProxy delegate.
	/// </summary>
	private static async Task<JsonElement?> GetDocumentViaProxy( string collectionId, string documentId )
	{
		var steamId = Game.SteamId.ToString();
		var docId = documentId ?? steamId;

		// Get the client's auth token as consent proof (included in HMAC signature)
		string clientToken = null;
		try
		{
			clientToken = await GetAuthTokenWithRetry( $"storage proxy consent ({collectionId}/{docId})" );
		}
		catch ( Exception ex )
		{
			if ( NetworkStorageLogConfig.LogErrors )
				Log.Warning( $"[NetworkStorage] storage PROXY: failed to get client token — {ex.Message}" );
		}

		if ( NetworkStorageLogConfig.LogProxy )
		{
			NetLog.Request( "storage", $"PROXY → host GET {collectionId}/{docId}" );
			Log.Info( $"[NetworkStorage] storage PROXY request via host for {collectionId}/{docId}" );
		}

		try
		{
			var result = await DocumentProxy( steamId, clientToken ?? "", collectionId, docId );
			if ( result == null )
			{
				if ( NetworkStorageLogConfig.LogErrors )
				{
					Log.Warning( $"[NetworkStorage] storage PROXY returned null for {collectionId}/{docId}" );
					NetLog.Error( "storage", "Proxy returned null" );
				}
				RecordEndpointError( "storage", "PROXY_FAILED", "Proxy returned null" );
				return null;
			}

			var reply = NetworkStorageProxyEnvelope.Read( result, NetworkStorageProxyEnvelope.DocumentKind );
			if ( !reply.Ok )
			{
				RecordEndpointError( "storage", reply.ErrorCode, reply.ErrorMessage );
				return null;
			}

			if ( NetworkStorageLogConfig.LogProxy )
				Log.Info( $"[NetworkStorage] storage PROXY → {TruncateJson( reply.Body, 200 )}" );
			var document = JsonSerializer.Deserialize<JsonElement>( reply.Body );
			if ( NetworkStorageLogConfig.LogProxy )
				NetLog.Response( "storage", $"PROXY OK ({reply.Body.Length} bytes)" );
			return document;
		}
		catch ( Exception ex )
		{
			if ( NetworkStorageLogConfig.LogErrors )
			{
				Log.Warning( $"[NetworkStorage] storage PROXY FAILED — {ex.Message}" );
				NetLog.Error( "storage", $"Proxy error: {ex.Message}" );
			}
			RecordEndpointError( "storage", ex is TimeoutException ? "PROXY_TIMEOUT" : "PROXY_FAILED", ex.Message );
			return null;
		}
	}

}