109 lines
3.3 KiB
C#
109 lines
3.3 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 SessionStreamCursorCodec : IDisposable
|
|
{
|
|
private const string Prefix = "rvs1";
|
|
private readonly EphemeralCursorProtector _protector = new();
|
|
|
|
public string Encode(VisibleListingQuery query, long revision, DateTimeOffset now)
|
|
{
|
|
ArgumentOutOfRangeException.ThrowIfNegative(revision);
|
|
SessionStreamCursorPayload payload = new()
|
|
{
|
|
GameId = query.Scope.GameId.Value,
|
|
EnvironmentId = query.Scope.EnvironmentId.Value,
|
|
ProtocolVersion = query.ProtocolVersion,
|
|
RegionId = query.RegionId?.Value,
|
|
ExcludeFull = query.ExcludeFull,
|
|
Revision = revision,
|
|
ExpiresAtUnixSeconds = now.AddMinutes(10).ToUnixTimeSeconds(),
|
|
};
|
|
byte[] encoded = JsonSerializer.SerializeToUtf8Bytes(payload, ContractJson.Options);
|
|
try
|
|
{
|
|
return _protector.Protect(Prefix, encoded);
|
|
}
|
|
finally
|
|
{
|
|
CryptographicOperations.ZeroMemory(encoded);
|
|
}
|
|
}
|
|
|
|
public bool TryDecode(
|
|
string? cursor,
|
|
VisibleListingQuery query,
|
|
DateTimeOffset now,
|
|
out long revision)
|
|
{
|
|
revision = 0;
|
|
if (cursor is null || !_protector.TryUnprotect(Prefix, cursor, out byte[] encodedPayload))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
SessionStreamCursorPayload? payload;
|
|
try
|
|
{
|
|
payload = JsonSerializer.Deserialize<SessionStreamCursorPayload>(
|
|
encodedPayload,
|
|
ContractJson.Options);
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
payload = null;
|
|
}
|
|
finally
|
|
{
|
|
CryptographicOperations.ZeroMemory(encodedPayload);
|
|
}
|
|
|
|
if (payload is null
|
|
|| payload.Revision < 0
|
|
|| payload.ExpiresAtUnixSeconds <= now.ToUnixTimeSeconds()
|
|
|| !string.Equals(payload.GameId, query.Scope.GameId.Value, StringComparison.Ordinal)
|
|
|| !string.Equals(payload.EnvironmentId, query.Scope.EnvironmentId.Value, StringComparison.Ordinal)
|
|
|| payload.ProtocolVersion != query.ProtocolVersion
|
|
|| !string.Equals(payload.RegionId, query.RegionId?.Value, StringComparison.Ordinal)
|
|
|| payload.ExcludeFull != query.ExcludeFull)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
revision = payload.Revision;
|
|
return true;
|
|
}
|
|
|
|
public void Dispose() => _protector.Dispose();
|
|
|
|
public override string ToString() => "[SessionStreamCursorCodec: key and cursors redacted]";
|
|
}
|
|
|
|
internal sealed class SessionStreamCursorPayload
|
|
{
|
|
[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 long Revision { get; set; }
|
|
|
|
[JsonRequired]
|
|
public long ExpiresAtUnixSeconds { get; set; }
|
|
}
|