116 lines
3.4 KiB
C#
116 lines
3.4 KiB
C#
using System.Security.Cryptography;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
using FinalFactory.Rendezvous.Contracts;
|
|
using FinalFactory.Rendezvous.Server.State;
|
|
|
|
namespace FinalFactory.Rendezvous.Server.Browser;
|
|
|
|
internal sealed class SessionBrowserCursorCodec : IDisposable
|
|
{
|
|
private const string Prefix = "rvc1";
|
|
private readonly EphemeralCursorProtector _protector = new();
|
|
|
|
public string Encode(VisibleListingQuery query, SessionListingId after, DateTimeOffset now)
|
|
{
|
|
BrowserCursorPayload payload = new()
|
|
{
|
|
GameId = query.Scope.GameId.Value,
|
|
EnvironmentId = query.Scope.EnvironmentId.Value,
|
|
ProtocolVersion = query.ProtocolVersion,
|
|
RegionId = query.RegionId?.Value,
|
|
ExcludeFull = query.ExcludeFull,
|
|
AfterListingId = after.ToString(),
|
|
ExpiresAtUnixSeconds = now.AddMinutes(5).ToUnixTimeSeconds(),
|
|
};
|
|
byte[] encoded = JsonSerializer.SerializeToUtf8Bytes(payload, ContractJson.Options);
|
|
try
|
|
{
|
|
return _protector.Protect(Prefix, encoded);
|
|
}
|
|
finally
|
|
{
|
|
CryptographicOperations.ZeroMemory(encoded);
|
|
}
|
|
}
|
|
|
|
public bool TryDecode(
|
|
string? cursor,
|
|
TenantScope scope,
|
|
uint protocolVersion,
|
|
RegionId? regionId,
|
|
bool excludeFull,
|
|
DateTimeOffset now,
|
|
out SessionListingId? after)
|
|
{
|
|
after = null;
|
|
if (cursor is null)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (!_protector.TryUnprotect(Prefix, cursor, out byte[] encodedPayload))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
BrowserCursorPayload? payload;
|
|
try
|
|
{
|
|
payload = JsonSerializer.Deserialize<BrowserCursorPayload>(
|
|
encodedPayload,
|
|
ContractJson.Options);
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
payload = null;
|
|
}
|
|
finally
|
|
{
|
|
CryptographicOperations.ZeroMemory(encodedPayload);
|
|
}
|
|
|
|
if (payload is null
|
|
|| payload.ExpiresAtUnixSeconds <= now.ToUnixTimeSeconds()
|
|
|| !string.Equals(payload.GameId, scope.GameId.Value, StringComparison.Ordinal)
|
|
|| !string.Equals(payload.EnvironmentId, scope.EnvironmentId.Value, StringComparison.Ordinal)
|
|
|| payload.ProtocolVersion != protocolVersion
|
|
|| !string.Equals(payload.RegionId, regionId?.Value, StringComparison.Ordinal)
|
|
|| payload.ExcludeFull != excludeFull
|
|
|| !SessionListingId.TryParse(payload.AfterListingId, out SessionListingId listingId))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
after = listingId;
|
|
return true;
|
|
}
|
|
|
|
public void Dispose() => _protector.Dispose();
|
|
|
|
public override string ToString() => "[SessionBrowserCursorCodec: key and cursors redacted]";
|
|
}
|
|
|
|
internal sealed class BrowserCursorPayload
|
|
{
|
|
[JsonRequired]
|
|
public string GameId { get; set; } = string.Empty;
|
|
|
|
[JsonRequired]
|
|
public string EnvironmentId { get; set; } = string.Empty;
|
|
|
|
[JsonRequired]
|
|
public uint ProtocolVersion { get; set; }
|
|
|
|
public string? RegionId { get; set; }
|
|
|
|
[JsonRequired]
|
|
public bool ExcludeFull { get; set; }
|
|
|
|
[JsonRequired]
|
|
public string AfterListingId { get; set; } = string.Empty;
|
|
|
|
[JsonRequired]
|
|
public long ExpiresAtUnixSeconds { get; set; }
|
|
}
|