The network storage proxy client. Routes external API calls and file document requests through the game host via a proxy delegate.
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;
}
}
}