97 lines
2.6 KiB
C#
97 lines
2.6 KiB
C#
using System.Security.Cryptography;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
using FinalFactory.Rendezvous.Contracts;
|
|
using FinalFactory.Rendezvous.Server.Browser;
|
|
|
|
namespace FinalFactory.Rendezvous.Server.JoinAttempts;
|
|
|
|
internal sealed class JoinAttemptCursorCodec : IDisposable
|
|
{
|
|
private const string Prefix = "rvj1";
|
|
private readonly EphemeralCursorProtector _protector = new();
|
|
|
|
public string Encode(
|
|
SessionListingId listingId,
|
|
JoinAttemptId after,
|
|
DateTimeOffset now)
|
|
{
|
|
JoinAttemptCursorPayload payload = new()
|
|
{
|
|
ListingId = listingId.ToString(),
|
|
AfterAttemptId = 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,
|
|
SessionListingId listingId,
|
|
DateTimeOffset now,
|
|
out JoinAttemptId? after)
|
|
{
|
|
after = null;
|
|
if (cursor is null)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (!_protector.TryUnprotect(Prefix, cursor, out byte[] encodedPayload))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
JoinAttemptCursorPayload? payload;
|
|
try
|
|
{
|
|
payload = JsonSerializer.Deserialize<JoinAttemptCursorPayload>(
|
|
encodedPayload,
|
|
ContractJson.Options);
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
payload = null;
|
|
}
|
|
finally
|
|
{
|
|
CryptographicOperations.ZeroMemory(encodedPayload);
|
|
}
|
|
|
|
if (payload is null
|
|
|| payload.ExpiresAtUnixSeconds <= now.ToUnixTimeSeconds()
|
|
|| !string.Equals(payload.ListingId, listingId.ToString(), StringComparison.Ordinal)
|
|
|| !JoinAttemptId.TryParse(payload.AfterAttemptId, out JoinAttemptId attemptId))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
after = attemptId;
|
|
return true;
|
|
}
|
|
|
|
public void Dispose() => _protector.Dispose();
|
|
|
|
public override string ToString() => "[JoinAttemptCursorCodec: key and cursors redacted]";
|
|
}
|
|
|
|
internal sealed class JoinAttemptCursorPayload
|
|
{
|
|
[JsonRequired]
|
|
public string ListingId { get; set; } = string.Empty;
|
|
|
|
[JsonRequired]
|
|
public string AfterAttemptId { get; set; } = string.Empty;
|
|
|
|
[JsonRequired]
|
|
public long ExpiresAtUnixSeconds { get; set; }
|
|
}
|