Code/Auth/NetworkStorageDedicatedServerSecret.cs
using System;
using System.Collections.Generic;

namespace Sandbox;

public static partial class NetworkStorage
{
	private const string DedicatedSecretLaunchKey = "network_storage_secret_key";

	private static readonly string[] DedicatedSecretLaunchKeys =
	{
		DedicatedSecretLaunchKey,
		"network-storage-secret-key",
		"sboxcool_secret_key",
		"networkStorageSecretKey",
		"sboxcoolSecretKey",
		"nsSecretKey",
		"ns_secret_key"
	};

	[ConVar( "network-storage-secret-key", ConVarFlags.Hidden )]
	public static string NetworkStorageSecretKeyDashedConVar { get; set; } = "";

	[ConVar( "network_storage_secret_key", ConVarFlags.Hidden )]
	public static string NetworkStorageSecretKeyConVar { get; set; } = "";

	[ConVar( "sboxcool_secret_key", ConVarFlags.Hidden )]
	public static string SboxCoolSecretKeyConVar { get; set; } = "";

	[ConVar( "networkStorageSecretKey", ConVarFlags.Hidden )]
	public static string NetworkStorageSecretKeyCamelConVar { get; set; } = "";

	[ConVar( "sboxcoolSecretKey", ConVarFlags.Hidden )]
	public static string SboxCoolSecretKeyCamelConVar { get; set; } = "";

	[ConVar( "nsSecretKey", ConVarFlags.Hidden )]
	public static string NsSecretKeyCamelConVar { get; set; } = "";

	[ConVar( "ns_secret_key", ConVarFlags.Hidden )]
	public static string NsSecretKeyConVar { get; set; } = "";

	private static bool _dedicatedSecretLookupAttempted;
	private static string _dedicatedServerSecretKey;
	private static string _dedicatedServerSecretKeySource;
	private static bool _dedicatedServerSecretKeyRejected;
	private static bool _dedicatedPlayerAuthSuppressedLogged;
	private static bool _dedicatedSecretMissingLogged;

	/// <summary>
	/// True when this process is a dedicated server and a runtime endpoint secret key was supplied.
	/// The key is never read on clients or listen servers.
	/// </summary>
	public static bool HasDedicatedServerSecretKey => TryGetDedicatedServerSecretKey( null, out _ );

	/// <summary>Where the dedicated endpoint secret key was loaded from, without exposing the key.</summary>
	public static string DedicatedServerSecretKeySource
	{
		get
		{
			TryGetDedicatedServerSecretKey( null, out _ );
			return _dedicatedServerSecretKeySource ?? "none";
		}
	}

	/// <summary>
	/// Manually provide a runtime endpoint secret key. This is accepted only on dedicated servers.
	/// Prefer the dedicated launch flag: +network_storage_secret_key sbox_sk_...
	/// </summary>
	public static void ConfigureDedicatedServerSecretKey( string secretKey )
	{
		if ( !CanUseDedicatedServerSecretKey() )
		{
			if ( !string.IsNullOrWhiteSpace( secretKey ) && NetworkStorageLogConfig.LogConfig )
				Log.Warning( "[NetworkStorage] Ignoring dedicated endpoint secret key because this process is not a dedicated server host." );
			return;
		}

		_dedicatedSecretLookupAttempted = true;
		_dedicatedServerSecretKey = NormalizeSecretKey( secretKey );
		_dedicatedServerSecretKeySource = string.IsNullOrEmpty( _dedicatedServerSecretKey ) ? "none" : "manual";
		_dedicatedServerSecretKeyRejected = false;
	}

	private static bool TryAddDedicatedServerSecretHeaders( Dictionary<string, string> headers, EndpointReference endpoint )
	{
		if ( !TryPrepareDedicatedServerSecret( headers, endpoint, out var secretKey ) )
			return false;

		headers["x-secret-key"] = secretKey;
		headers["x-public-key"] = ApiKey ?? "";
		TryAddDedicatedSteamIdHeader( headers );
		LogDedicatedSecretTransportOnce( "x-secret-key" );
		return true;
	}

	private static bool ShouldUseDedicatedServerSecret( EndpointReference endpoint = null )
	{
		if ( !TryGetDedicatedServerSecretKey( endpoint, out _ ) )
			return false;
		if ( CanTransportDedicatedServerSecretSecurely() )
			return true;

		LogInsecureSecretTransportOnce();
		return false;
	}

	private static bool TryBuildDedicatedStorageHeaders( out Dictionary<string, string> headers )
	{
		headers = null;
		if ( !TryGetDedicatedServerSecretKey( null, out var secretKey ) )
			return false;
		if ( !CanTransportDedicatedServerSecretSecurely() )
		{
			LogInsecureSecretTransportOnce();
			return false;
		}

		headers = BuildPublicHeaders();
		headers["x-api-key"] = secretKey;
		headers["x-public-key"] = ApiKey ?? "";
		LogDedicatedSecretTransportOnce( "x-api-key" );
		return true;
	}

	private static void TryAddDedicatedSteamIdHeader( Dictionary<string, string> headers )
	{
		if ( headers is null ) return;

		var steamId = Game.SteamId.ToString();
		if ( !string.IsNullOrWhiteSpace( steamId ) && steamId != "0" )
			headers["x-steam-id"] = steamId;
	}

	private static bool TryPrepareDedicatedServerSecret( Dictionary<string, string> headers, EndpointReference endpoint, out string secretKey )
	{
		secretKey = null;
		if ( headers is null || !TryGetDedicatedServerSecretKey( endpoint, out secretKey ) )
			return false;

		if ( CanTransportDedicatedServerSecretSecurely() )
			return true;

		LogInsecureSecretTransportOnce();
		secretKey = null;
		return false;
	}

	private static bool TryGetDedicatedServerSecretKey( EndpointReference endpoint, out string secretKey )
	{
		secretKey = null;

		if ( !CanUseDedicatedServerSecretKey() )
		{
			LogIgnoredEndpointUrlSecretOnce( endpoint );
			return false;
		}

		LoadDedicatedServerSecretKey();
		if ( _dedicatedServerSecretKeyRejected )
			return false;

		secretKey = _dedicatedServerSecretKey;
		return !string.IsNullOrWhiteSpace( secretKey );
	}

	private static bool CanUseDedicatedServerSecretKey()
		=> IsDedicatedServerHost;

	private static bool IsDedicatedServerProcess => Application.IsDedicatedServer;

	private static bool IsDedicatedServerHost => IsDedicatedServerProcess && IsHost;

	private static string DedicatedServerAuthUnavailableMessage()
	{
		var suffix = HasDedicatedServerSecretKey
			? " Secret keys are only sent to HTTPS or loopback API roots."
			: $" Start the server with one of: {SupportedDedicatedSecretLaunchFlagsText}.";
		return $"Dedicated servers cannot request s&box player auth tokens.{suffix}";
	}

	private static string SupportedDedicatedSecretLaunchFlagsText => "+network_storage_secret_key sbox_sk_..., +network-storage-secret-key sbox_sk_..., +sboxcool_secret_key sbox_sk_..., +networkStorageSecretKey sbox_sk_..., +sboxcoolSecretKey sbox_sk_..., +nsSecretKey sbox_sk_..., +ns_secret_key sbox_sk_...";

	private static void LogDedicatedSecretMissingOnce()
	{
		if ( _dedicatedSecretMissingLogged ) return;

		_dedicatedSecretMissingLogged = true;
		Log.Warning( $"[NetworkStorage] Dedicated server secret key not found. Supported launch flags: {SupportedDedicatedSecretLaunchFlagsText}" );
	}

	private static bool TryRejectDedicatedServerPlayerAuth( string tag )
	{
		if ( !IsDedicatedServerProcess )
			return false;

		LogDedicatedPlayerAuthSuppressedOnce();
		RecordEndpointError( tag, "DEDICATED_SECRET_REQUIRED", DedicatedServerAuthUnavailableMessage() );
		return true;
	}

	private static void LogDedicatedPlayerAuthSuppressedOnce()
	{
		if ( _dedicatedPlayerAuthSuppressedLogged || !NetworkStorageLogConfig.LogTokens ) return;

		_dedicatedPlayerAuthSuppressedLogged = true;
		Log.Warning( $"[NetworkStorage] {DedicatedServerAuthUnavailableMessage()}" );
	}

	private static bool CanTransportDedicatedServerSecretSecurely()
	{
		return Uri.TryCreate( ApiRoot, UriKind.Absolute, out var uri ) &&
			(string.Equals( uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase ) || uri.IsLoopback);
	}

}