using System.Collections.Frozen; using System.Diagnostics; using System.Net; using FinalFactory.Rendezvous.Contracts; namespace FinalFactory.Rendezvous.Server.State; internal interface IWallClock { DateTimeOffset UtcNow { get; } } internal interface IMonotonicClock { TimeSpan Elapsed { get; } } internal sealed class SystemRendezvousClock : IWallClock, IMonotonicClock { private readonly long _origin = Stopwatch.GetTimestamp(); public DateTimeOffset UtcNow => DateTimeOffset.UtcNow; public TimeSpan Elapsed => Stopwatch.GetElapsedTime(_origin); } internal sealed record EphemeralStoreOptions { public int MaxListings { get; init; } = 25_000; public int MaxPresenceBindings { get; init; } = 25_000; public int MaxJoinAttempts { get; init; } = 10_000; public int MaxOutcomeReports { get; init; } = 35_000; public int MaxReplayEntries { get; init; } = 30_000; public int MaxRevocations { get; init; } = 10_000; public int MaxIdempotencyEntries { get; init; } = 35_000; public TimeSpan LeaseLifetime { get; init; } = TimeSpan.FromSeconds(60); public TimeSpan PresenceLifetime { get; init; } = TimeSpan.FromSeconds(20); public TimeSpan JoinAttemptLifetime { get; init; } = TimeSpan.FromSeconds(30); public TimeSpan ConnectionTicketLifetime { get; init; } = TimeSpan.FromSeconds(20); public TimeSpan ReplayLifetime { get; init; } = TimeSpan.FromSeconds(30); public TimeSpan IdempotencyLifetime { get; init; } = TimeSpan.FromMinutes(2); public TimeSpan GracefulDrainLifetime { get; init; } = TimeSpan.FromSeconds(30); public void Validate() { RequirePositive(MaxListings, nameof(MaxListings)); RequirePositive(MaxPresenceBindings, nameof(MaxPresenceBindings)); RequirePositive(MaxJoinAttempts, nameof(MaxJoinAttempts)); RequirePositive(MaxOutcomeReports, nameof(MaxOutcomeReports)); RequirePositive(MaxReplayEntries, nameof(MaxReplayEntries)); RequirePositive(MaxRevocations, nameof(MaxRevocations)); RequirePositive(MaxIdempotencyEntries, nameof(MaxIdempotencyEntries)); RequireDuration(LeaseLifetime, TimeSpan.FromSeconds(60), nameof(LeaseLifetime)); RequireDuration(PresenceLifetime, TimeSpan.FromSeconds(20), nameof(PresenceLifetime)); RequireDuration(JoinAttemptLifetime, TimeSpan.FromSeconds(30), nameof(JoinAttemptLifetime)); RequireDuration(ConnectionTicketLifetime, TimeSpan.FromSeconds(20), nameof(ConnectionTicketLifetime)); RequireDuration(ReplayLifetime, TimeSpan.FromSeconds(30), nameof(ReplayLifetime)); RequireDuration(IdempotencyLifetime, TimeSpan.FromMinutes(10), nameof(IdempotencyLifetime)); RequireDuration(GracefulDrainLifetime, TimeSpan.FromSeconds(30), nameof(GracefulDrainLifetime)); if (ConnectionTicketLifetime > JoinAttemptLifetime) { throw new ArgumentOutOfRangeException( nameof(ConnectionTicketLifetime), "Connection tickets cannot outlive their join attempt."); } if (IdempotencyLifetime < LeaseLifetime || IdempotencyLifetime < JoinAttemptLifetime) { throw new ArgumentOutOfRangeException( nameof(IdempotencyLifetime), "Idempotency retention must cover every idempotent resource lifetime."); } } private static void RequirePositive(int value, string name) { if (value <= 0) { throw new ArgumentOutOfRangeException(name, "Store capacity must be positive."); } } private static void RequireDuration(TimeSpan value, TimeSpan maximum, string name) { if (value <= TimeSpan.Zero || value > maximum) { throw new ArgumentOutOfRangeException(name, $"Duration must be positive and no greater than {maximum}."); } } } internal readonly record struct TenantScope(GameId GameId, EnvironmentId EnvironmentId); internal readonly struct SecretFingerprint : IEquatable { private readonly string? _value; public SecretFingerprint(string value) { if (string.IsNullOrWhiteSpace(value) || value.Length > 128) { throw new ArgumentException("Secret fingerprints must contain 1-128 characters.", nameof(value)); } _value = value; } public bool IsValid => !string.IsNullOrWhiteSpace(_value) && _value.Length <= 128; public bool Equals(SecretFingerprint other) { ReadOnlySpan left = _value.AsSpan(); ReadOnlySpan right = other._value.AsSpan(); if (left.Length != right.Length) { return false; } int difference = 0; for (int index = 0; index < left.Length; index++) { difference |= left[index] ^ right[index]; } return difference == 0; } public override bool Equals(object? obj) => obj is SecretFingerprint other && Equals(other); public override int GetHashCode() => StringComparer.Ordinal.GetHashCode(_value ?? string.Empty); public override string ToString() => "[REDACTED]"; public static bool operator ==(SecretFingerprint left, SecretFingerprint right) => left.Equals(right); public static bool operator !=(SecretFingerprint left, SecretFingerprint right) => !left.Equals(right); } internal readonly record struct ObservedEndpoint { public ObservedEndpoint(AddressFamilyKind addressFamily, string address, int port) { if (!IPAddress.TryParse(address, out IPAddress? parsed) || (addressFamily == AddressFamilyKind.Ipv4 && parsed.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork) || (addressFamily == AddressFamilyKind.Ipv6 && parsed.AddressFamily != System.Net.Sockets.AddressFamily.InterNetworkV6)) { throw new ArgumentException("The address must match the declared address family.", nameof(address)); } if (port is < 1 or > 65_535) { throw new ArgumentOutOfRangeException(nameof(port)); } AddressFamily = addressFamily; Address = parsed.ToString(); Port = port; } public AddressFamilyKind AddressFamily { get; } public string Address { get; } public int Port { get; } public bool IsValid => !string.IsNullOrEmpty(Address) && Port is >= 1 and <= 65_535 && AddressFamily is AddressFamilyKind.Ipv4 or AddressFamilyKind.Ipv6; } internal sealed record ListingDefinition { public required SessionListingId ListingId { get; init; } public required LeaseId LeaseId { get; init; } public required TenantScope Scope { get; init; } public required string OwnerSubject { get; init; } public required RegionId RegionId { get; init; } public required uint ProtocolVersion { get; init; } public required string BuildVersion { get; init; } public required string DisplayName { get; init; } public required ListingVisibility Visibility { get; init; } public required PublisherTrustMode TrustMode { get; init; } public required int CurrentPlayers { get; init; } public required int MaximumPlayers { get; init; } public required IReadOnlyDictionary Metadata { get; init; } public NetworkEndpoint? DedicatedFallback { get; init; } public required SecretFingerprint LeaseFingerprint { get; init; } public required MediationHandle HostPresenceHandle { get; init; } public required SecretFingerprint HostPresenceFingerprint { get; init; } public required string CapabilityDerivationSalt { get; init; } } internal sealed record StoredListing { public required ListingDefinition Definition { get; init; } public required DateTimeOffset LeaseExpiresAt { get; init; } public required long Version { get; init; } public required bool HasFreshPresence { get; init; } public static ListingDefinition Freeze(ListingDefinition source) => source with { Metadata = source.Metadata.ToFrozenDictionary(StringComparer.Ordinal), DedicatedFallback = CopyEndpoint(source.DedicatedFallback), }; internal static NetworkEndpoint? CopyEndpoint(NetworkEndpoint? endpoint) => endpoint is null ? null : new NetworkEndpoint { AddressFamily = endpoint.AddressFamily, Address = endpoint.Address, Port = endpoint.Port, }; } internal sealed record CreateListingCommand( string IdempotencyKey, string RequestFingerprint, ListingDefinition Listing, int OwnerListingLimit = int.MaxValue); internal sealed record RenewLeaseCommand( SessionListingId ListingId, LeaseId LeaseId, SecretFingerprint LeaseFingerprint, string OwnerSubject, long ExpectedVersion); internal sealed record UpdateListingCommand( SessionListingId ListingId, LeaseId LeaseId, SecretFingerprint LeaseFingerprint, string OwnerSubject, string BuildVersion, string DisplayName, int CurrentPlayers, int MaximumPlayers, IReadOnlyDictionary Metadata, NetworkEndpoint? DedicatedFallback); internal sealed record DeleteListingCommand( SessionListingId ListingId, LeaseId LeaseId, SecretFingerprint LeaseFingerprint, string OwnerSubject); internal sealed record BindHostPresenceCommand( MediationHandle Handle, SecretFingerprint CapabilityFingerprint, ObservedEndpoint PublicEndpoint, ObservedEndpoint? LocalEndpoint); internal sealed record VisibleListingQuery( TenantScope Scope, uint ProtocolVersion, RegionId? RegionId, int MaximumResults = ContractLimits.BrowserPageMaxItems, SessionListingId? AfterListingId = null, bool ExcludeFull = false); internal enum AttemptPeerRole { Host = 1, Client = 2, } internal sealed record CreateJoinAttemptCommand { public required string IdempotencyOwner { get; init; } public required string IdempotencyKey { get; init; } public required string RequestFingerprint { get; init; } public required string ClientSubject { get; init; } public required JoinAttemptId AttemptId { get; init; } public required MediationHandle MediationHandle { get; init; } public required TenantScope Scope { get; init; } public required SessionListingId ListingId { get; init; } public required uint ProtocolVersion { get; init; } public required SecretFingerprint HostCapabilityFingerprint { get; init; } public required SecretFingerprint ClientCapabilityFingerprint { get; init; } public required SecretFingerprint ConnectionTicketFingerprint { get; init; } public required string CapabilityDerivationSalt { get; init; } public NetworkEndpoint? DedicatedFallback { get; init; } public int ScopeAttemptLimit { get; init; } = int.MaxValue; public override string ToString() => "[CreateJoinAttemptCommand: credentials redacted]"; } internal sealed record AttemptEndpointBinding( ObservedEndpoint PublicEndpoint, ObservedEndpoint? LocalEndpoint); internal sealed record StoredJoinAttempt { public required JoinAttemptId AttemptId { get; init; } public required MediationHandle MediationHandle { get; init; } public required TenantScope Scope { get; init; } public required SessionListingId ListingId { get; init; } public required string ClientSubject { get; init; } public required uint ProtocolVersion { get; init; } public required string IdempotencyKey { get; init; } public required string RequestFingerprint { get; init; } public required string CapabilityDerivationSalt { get; init; } public required SecretFingerprint HostCapabilityFingerprint { get; init; } public required SecretFingerprint ClientCapabilityFingerprint { get; init; } public required SecretFingerprint ConnectionTicketFingerprint { get; init; } public NetworkEndpoint? DedicatedFallback { get; init; } public required DateTimeOffset ExpiresAt { get; init; } public required TimeSpan CreatedAtMonotonic { get; init; } public required DateTimeOffset ConnectionTicketExpiresAt { get; init; } public AttemptEndpointBinding? HostEndpoint { get; init; } public AttemptEndpointBinding? ClientEndpoint { get; init; } public required bool IntroductionConsumed { get; init; } public required bool ConnectionTicketConsumed { get; init; } public required bool IsCancelled { get; init; } public override string ToString() => $"[StoredJoinAttempt {AttemptId}; credentials redacted]"; } internal sealed record HostJoinAttemptQuery( SessionListingId ListingId, SecretFingerprint LeaseFingerprint, int MaximumResults, JoinAttemptId? AfterAttemptId = null); internal sealed record BindAttemptEndpointCommand( MediationHandle Handle, AttemptPeerRole Role, SecretFingerprint CapabilityFingerprint, ObservedEndpoint PublicEndpoint, ObservedEndpoint? LocalEndpoint); internal sealed record IntroductionEndpoints( StoredJoinAttempt Attempt, AttemptEndpointBinding Host, AttemptEndpointBinding Client) { public JoinAttemptId AttemptId => Attempt.AttemptId; } internal sealed record CancelJoinAttemptCommand( JoinAttemptId AttemptId, SecretFingerprint ClientCapabilityFingerprint); internal sealed record ReportConnectionOutcomeCommand( JoinAttemptId AttemptId, SecretFingerprint ClientCapabilityFingerprint, ConnectionOutcomeKind Outcome, ConnectionElapsedBucket ElapsedBucket); internal sealed record StoredConnectionOutcome( ConnectionOutcomeKind Outcome, ConnectionElapsedBucket ElapsedBucket); internal sealed record ConsumeConnectionTicketCommand( JoinAttemptId AttemptId, SecretFingerprint ConnectionTicketFingerprint); internal sealed record ReplayConsumption( string Namespace, string Key, TimeSpan? Lifetime = null); internal enum StoreResultCode { Success = 0, NotFound = 1, Expired = 2, Revoked = 3, Conflict = 4, CapacityExceeded = 5, Draining = 6, ReplayRejected = 7, ServiceUnavailable = 8, StaleHost = 9, IncompatibleProtocol = 10, } internal sealed record StoreResult(StoreResultCode Code, T? Value = default, bool IsIdempotentReplay = false) { public bool Succeeded => Code == StoreResultCode.Success; } internal interface IEphemeralRendezvousStore { Guid InstanceId { get; } bool IsAvailable { get; } bool IsDraining { get; } StoreResult CreateListing(CreateListingCommand command, CancellationToken cancellationToken = default); StoreResult RenewLease(RenewLeaseCommand command, CancellationToken cancellationToken = default); StoreResult UpdateListing(UpdateListingCommand command, CancellationToken cancellationToken = default); StoreResult DeleteListing(DeleteListingCommand command, CancellationToken cancellationToken = default); StoreResult GetListing(SessionListingId listingId, bool requireFreshPresence, CancellationToken cancellationToken = default); StoreResult> BrowseVisibleListings(VisibleListingQuery query, CancellationToken cancellationToken = default); StoreResult BindHostPresence(BindHostPresenceCommand command, CancellationToken cancellationToken = default); StoreResult CreateJoinAttempt(CreateJoinAttemptCommand command, CancellationToken cancellationToken = default); StoreResult> BrowseHostJoinAttempts(HostJoinAttemptQuery query, CancellationToken cancellationToken = default); StoreResult CancelJoinAttempt(CancelJoinAttemptCommand command, CancellationToken cancellationToken = default); StoreResult ReportConnectionOutcome(ReportConnectionOutcomeCommand command, CancellationToken cancellationToken = default); StoreResult BindAttemptEndpoint(BindAttemptEndpointCommand command, CancellationToken cancellationToken = default); StoreResult ConsumeIntroduction(MediationHandle handle, CancellationToken cancellationToken = default); StoreResult ConsumeConnectionTicket(ConsumeConnectionTicketCommand command, CancellationToken cancellationToken = default); StoreResult ConsumeReplay(ReplayConsumption consumption, CancellationToken cancellationToken = default); StoreResult RevokeListing(SessionListingId listingId, CancellationToken cancellationToken = default); StoreResult RevokePrincipal(string subject, TimeSpan lifetime, CancellationToken cancellationToken = default); void BeginDrain(CancellationToken cancellationToken = default); }