mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 23:18:56 -07:00
243 lines
10 KiB
C#
243 lines
10 KiB
C#
using System.Text.Json;
|
|
using RadicadeServer.DB;
|
|
using RadicadeServer.RecNet.Enums.Players;
|
|
|
|
namespace RadicadeServer.Auth;
|
|
|
|
public static class PlatformIdentity
|
|
{
|
|
public static readonly string[] AllowedSteamAppIds = ["480", "471710"];
|
|
|
|
public static HttpClient Client = new() { Timeout = TimeSpan.FromSeconds(10) };
|
|
|
|
public sealed record Proof(bool Verified, string? SteamId, string Detail)
|
|
{
|
|
public static Proof Ok(string? steamId) => new(true, steamId, "proved");
|
|
public static Proof No(string detail) => new(false, null, detail);
|
|
}
|
|
|
|
public static async Task<Proof> ProveAsync(
|
|
Platforms platform,
|
|
string? platformId,
|
|
string? steamTicketHeader,
|
|
string? platformAuth,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var id = platformId?.Trim() ?? string.Empty;
|
|
if (id.Length == 0) return Proof.No("A platform id is required.");
|
|
|
|
return platform switch
|
|
{
|
|
Platforms.Steam => await ProveSteamAsync(id, steamTicketHeader, platformAuth, cancellationToken),
|
|
Platforms.Oculus => ProveOculus(id, platformAuth),
|
|
_ => Proof.No("Account creation is only available from Steam or Oculus on this server.")
|
|
};
|
|
}
|
|
|
|
private static async Task<Proof> ProveSteamAsync(
|
|
string platformId,
|
|
string? steamTicketHeader,
|
|
string? platformAuth,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var ticket = string.IsNullOrWhiteSpace(steamTicketHeader)
|
|
? ReadString(platformAuth, "Ticket")
|
|
: steamTicketHeader.Trim();
|
|
|
|
if (string.IsNullOrWhiteSpace(ticket))
|
|
return Proof.No("No Steam ticket was sent (x-steam-ticket header or platform_auth.Ticket).");
|
|
|
|
if (string.IsNullOrWhiteSpace(DBConfig.SteamApiKey))
|
|
{
|
|
if (!DBConfig.TrustUnverifiedSteamIdentity)
|
|
return Proof.No("Steam identity cannot be proved: no SteamApiKey is configured.");
|
|
|
|
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [Identity] accepted Steam identity {platformId} " +
|
|
"UNPROVED - no SteamApiKey is configured and TrustUnverifiedSteamIdentity is on.");
|
|
return Proof.Ok(null);
|
|
}
|
|
|
|
var attempted = new List<string>();
|
|
foreach (var appId in CandidateAppIds(ReadString(platformAuth, "AppId")))
|
|
{
|
|
attempted.Add(appId);
|
|
var result = await AuthenticateTicketAsync(ticket, appId, cancellationToken);
|
|
|
|
if (result.OtherApp) continue;
|
|
|
|
if (!result.Ok)
|
|
return Proof.No(result.Detail);
|
|
|
|
if (!string.Equals(result.SteamId, platformId, StringComparison.Ordinal))
|
|
return Proof.No($"The Steam ticket is for {result.SteamId}, not for the identity it was sent with.");
|
|
|
|
return Proof.Ok(result.SteamId);
|
|
}
|
|
|
|
return Proof.No("The Steam ticket is not for an app this server accepts (tried " +
|
|
string.Join(", ", attempted) + ").");
|
|
}
|
|
|
|
private static Proof ProveOculus(string platformId, string? platformAuth)
|
|
{
|
|
var nonce = ReadString(platformAuth, "Nonce");
|
|
if (string.IsNullOrWhiteSpace(nonce))
|
|
return Proof.No("No Oculus nonce was sent (platform_auth.Nonce).");
|
|
|
|
return Proof.No("Oculus identity cannot be proved yet: no Oculus app secret is configured, " +
|
|
"so the nonce cannot be validated.");
|
|
}
|
|
|
|
public static async Task<string?> CheckSteamStandingAsync(string steamId, CancellationToken cancellationToken = default)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(DBConfig.SteamApiKey)) return null;
|
|
if (string.IsNullOrWhiteSpace(steamId)) return null;
|
|
|
|
var key = Uri.EscapeDataString(DBConfig.SteamApiKey);
|
|
var id = Uri.EscapeDataString(steamId);
|
|
|
|
try
|
|
{
|
|
using var summaryDoc = await GetJsonAsync(
|
|
$"https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v2/?key={key}&steamids={id}",
|
|
cancellationToken);
|
|
if (summaryDoc == null) return null;
|
|
|
|
if (!summaryDoc.RootElement.TryGetProperty("response", out var summary) ||
|
|
!summary.TryGetProperty("players", out var players) ||
|
|
players.ValueKind != JsonValueKind.Array ||
|
|
players.GetArrayLength() == 0)
|
|
return "Your Steam profile was not found, so an account cannot be created for it.";
|
|
|
|
var player = players[0];
|
|
|
|
if (player.TryGetProperty("profilestate", out var profileState) &&
|
|
profileState.ValueKind == JsonValueKind.Number &&
|
|
profileState.TryGetInt32(out var state) && state != 1)
|
|
return "Your Steam profile has to be set up before you can create an account.";
|
|
|
|
using var bansDoc = await GetJsonAsync(
|
|
$"https://api.steampowered.com/ISteamUser/GetPlayerBans/v1/?key={key}&steamids={id}",
|
|
cancellationToken);
|
|
if (bansDoc == null) return null;
|
|
|
|
if (!bansDoc.RootElement.TryGetProperty("players", out var bans) ||
|
|
bans.ValueKind != JsonValueKind.Array || bans.GetArrayLength() == 0)
|
|
return "Steam would not report this account's ban status, so an account cannot be created for it.";
|
|
|
|
var entry = bans[0];
|
|
if (entry.TryGetProperty("CommunityBanned", out var banned) && banned.ValueKind == JsonValueKind.True)
|
|
return "Steam accounts with an active community ban are not allowed to create an account.";
|
|
}
|
|
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException or JsonException)
|
|
{
|
|
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [Identity] Steam standing check could not be " +
|
|
$"completed for {steamId}: {ex.GetType().Name}: {ex.Message}");
|
|
return null;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public sealed record TicketResult(bool Ok, bool OtherApp, string? SteamId, string Detail);
|
|
|
|
public static async Task<TicketResult> AuthenticateTicketAsync(string ticket, string appId, CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
using var doc = await GetJsonAsync(
|
|
"https://api.steampowered.com/ISteamUserAuth/AuthenticateUserTicket/v1/?key=" +
|
|
$"{Uri.EscapeDataString(DBConfig.SteamApiKey)}&appid={Uri.EscapeDataString(appId)}" +
|
|
$"&ticket={Uri.EscapeDataString(ticket)}",
|
|
cancellationToken);
|
|
|
|
if (doc == null) return new TicketResult(false, false, null, "Steam did not answer the ticket check.");
|
|
|
|
if (!doc.RootElement.TryGetProperty("response", out var response))
|
|
return new TicketResult(false, false, null, "Steam did not answer the ticket check.");
|
|
|
|
if (response.TryGetProperty("error", out var error))
|
|
{
|
|
var code = error.TryGetProperty("errorcode", out var errorCode) && errorCode.TryGetInt32(out var value)
|
|
? value
|
|
: 0;
|
|
var description = error.TryGetProperty("errordesc", out var desc) ? desc.GetString() : null;
|
|
|
|
if (code == 102)
|
|
return new TicketResult(false, true, null, $"ticket for app {appId}");
|
|
|
|
return new TicketResult(false, false, null,
|
|
$"Steam refused the ticket ({(description ?? "no description")}).");
|
|
}
|
|
|
|
if (!response.TryGetProperty("params", out var parameters) ||
|
|
parameters.ValueKind != JsonValueKind.Object ||
|
|
!parameters.TryGetProperty("result", out var result) ||
|
|
result.GetString() != "OK" ||
|
|
!parameters.TryGetProperty("steamid", out var steamId) ||
|
|
steamId.ValueKind != JsonValueKind.String)
|
|
return new TicketResult(false, false, null, "Steam did not accept the ticket.");
|
|
|
|
return new TicketResult(true, false, steamId.GetString(), "ok");
|
|
}
|
|
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException or JsonException)
|
|
{
|
|
return new TicketResult(false, false, null,
|
|
$"Steam could not be reached to check the ticket ({ex.GetType().Name}).");
|
|
}
|
|
}
|
|
|
|
private static async Task<JsonDocument?> GetJsonAsync(string url, CancellationToken cancellationToken)
|
|
{
|
|
using var response = await Client.GetAsync(url, cancellationToken);
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [Identity] Steam answered {(int)response.StatusCode} for " +
|
|
$"{new Uri(url).AbsolutePath}.");
|
|
return null;
|
|
}
|
|
|
|
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
|
return await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken);
|
|
}
|
|
|
|
private static IEnumerable<string> CandidateAppIds(string? claimed)
|
|
{
|
|
var seen = new HashSet<string>(StringComparer.Ordinal);
|
|
var ordered = new List<string>();
|
|
void Add(string? candidate)
|
|
{
|
|
var value = candidate?.Trim();
|
|
if (string.IsNullOrEmpty(value) || !seen.Add(value)) return;
|
|
ordered.Add(value);
|
|
}
|
|
|
|
Add(claimed);
|
|
foreach (var id in AllowedSteamAppIds) Add(id);
|
|
return ordered;
|
|
}
|
|
|
|
private static string? ReadString(string? json, string property)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(json)) return null;
|
|
|
|
try
|
|
{
|
|
using var doc = JsonDocument.Parse(json);
|
|
if (doc.RootElement.ValueKind != JsonValueKind.Object) return null;
|
|
if (!doc.RootElement.TryGetProperty(property, out var value)) return null;
|
|
|
|
return value.ValueKind switch
|
|
{
|
|
JsonValueKind.String => value.GetString(),
|
|
JsonValueKind.Number => value.ToString(),
|
|
_ => null
|
|
};
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
}
|