543 lines
27 KiB
C#
543 lines
27 KiB
C#
using System;
|
|
using System.Diagnostics;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using game.Account;
|
|
|
|
namespace game.PlayerDataStorage
|
|
{
|
|
/// <summary>
|
|
/// Owns exactly one authenticated session and its KV bridge. Does not initialize gameplay or write.
|
|
/// The caller must await the previous context's CloseAsync before creating the next context.
|
|
/// A failed Close must never be bypassed by constructing another instance.
|
|
/// </summary>
|
|
public sealed class PlayerKVLoginContext
|
|
{
|
|
readonly object gate = new object();
|
|
readonly string requestedId, cacheRoot;
|
|
readonly long epoch;
|
|
readonly Uri api;
|
|
readonly Func<long, bool> externalCurrent;
|
|
readonly Func<S101AccountSession> sessionFactory;
|
|
readonly Func<PlayerKVGameSessionBridge> bridgeFactory;
|
|
readonly CancellationTokenSource lifetime = new CancellationTokenSource();
|
|
readonly TaskCompletionSource<bool> workSettled = NewSignal();
|
|
TaskCompletionSource<PlayerKVLoginPrepareResult> preparation;
|
|
TaskCompletionSource<bool> closing;
|
|
CancellationToken firstToken;
|
|
CancellationTokenRegistration registration;
|
|
S101AccountSession session;
|
|
CookieContainer cookies;
|
|
PlayerKVGameSessionBridge bridge;
|
|
PlayerKVGameSessionHandle handle;
|
|
PlayerKVAccountDescriptor accountDescriptor;
|
|
PlayerKVAccountLease accountLease;
|
|
bool leaseIssued;
|
|
string playerId, predicateExceptionType;
|
|
int revoked, prepared, differentTokenCount;
|
|
|
|
// Internal factories are only for synthetic transport/cleanup faults. No public session or handle escapes.
|
|
public PlayerKVLoginContext(string playerId, string cacheRoot, long epoch, Func<long, bool> isCurrent)
|
|
: this(playerId, cacheRoot, epoch, isCurrent, new Uri(S101AccountSession.DefaultApiUrl),
|
|
() => new S101AccountSession(), () => new PlayerKVGameSessionBridge()) { }
|
|
|
|
internal PlayerKVLoginContext(string playerId, string cacheRoot, long epoch, Func<long, bool> isCurrent,
|
|
Uri api, Func<S101AccountSession> sessionFactory, Func<PlayerKVGameSessionBridge> bridgeFactory)
|
|
{
|
|
requestedId = playerId; this.cacheRoot = cacheRoot; this.epoch = epoch; externalCurrent = isCurrent;
|
|
this.api = api; this.sessionFactory = sessionFactory; this.bridgeFactory = bridgeFactory;
|
|
}
|
|
|
|
internal int DifferentTokenCount => Volatile.Read(ref differentTokenCount);
|
|
|
|
internal PlayerKVAccountDescriptor AccountDescriptor =>
|
|
Volatile.Read(ref prepared) != 0 && IsCurrent ? accountDescriptor : null;
|
|
|
|
// Only the coordinator may publish this original handle after preparation and Guard release.
|
|
internal bool TryAcquireGameHandle(long expectedEpoch, out PlayerKVGameSessionHandle gameHandle)
|
|
{
|
|
gameHandle = null;
|
|
// IsCurrent can invoke external code and synchronously reenter CloseAsync.
|
|
if (expectedEpoch != epoch || !IsCurrent) return false;
|
|
lock (gate)
|
|
{
|
|
if (closing != null || revoked != 0 || prepared != 1 || handle == null || handle.Epoch != expectedEpoch)
|
|
return false;
|
|
gameHandle = handle;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// All issuance and Close admission share gate. The editor factory may not block on Close.
|
|
internal bool TryAcquireAccountLease(PlayerKVControlledLoadCapability capability,
|
|
out PlayerKVAccountLease lease)
|
|
{
|
|
lease = null;
|
|
if (capability == null || !IsCurrent) return false;
|
|
lock (gate)
|
|
{
|
|
if (closing != null || revoked != 0 || prepared == 0 || leaseIssued) return false;
|
|
leaseIssued = true;
|
|
accountLease = new PlayerKVAccountLease(api, epoch, cookies, capability, () => IsCurrent,
|
|
() => Volatile.Read(ref revoked) == 0);
|
|
// A synthetic factory can synchronously revoke this context while being invoked.
|
|
if (revoked != 0 || closing != null) return false;
|
|
lease = accountLease;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/// <summary>Historical Prepared results and their data survive Close, but their authority does not.</summary>
|
|
public bool IsCurrent
|
|
{
|
|
get
|
|
{
|
|
if (Volatile.Read(ref prepared) == 0 || !EpochCurrent(epoch)) return false;
|
|
var observed = session;
|
|
return observed != null && observed.IsAuthenticated && observed.PlayerId == playerId &&
|
|
ReferenceEquals(observed.Cookies, cookies) && Volatile.Read(ref revoked) == 0;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// One task per instance. Only the FIRST token owns the lifetime; a different token on later calls
|
|
/// is diagnosed but never replaces it. Even after Close this returns the original historical task.
|
|
/// Cancellation/failure does not complete until transport and Close have really drained. A transport
|
|
/// ignoring cancellation can hold this task indefinitely; observe CloseAsync, never bypass its barrier.
|
|
/// </summary>
|
|
public Task<PlayerKVLoginPrepareResult> PrepareAsync(CancellationToken lifetimeToken = default)
|
|
{
|
|
Task<PlayerKVLoginPrepareResult> result;
|
|
bool start, alreadyClosing;
|
|
lock (gate)
|
|
{
|
|
start = preparation == null;
|
|
if (start)
|
|
{
|
|
preparation = new TaskCompletionSource<PlayerKVLoginPrepareResult>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
firstToken = lifetimeToken;
|
|
}
|
|
else if (firstToken != lifetimeToken) Interlocked.Increment(ref differentTokenCount);
|
|
result = preparation.Task;
|
|
alreadyClosing = closing != null;
|
|
}
|
|
if (!start)
|
|
{
|
|
if (firstToken != lifetimeToken) Debug.WriteLine("KV PrepareAsync ignored a different lifetime token; the first token still owns this context.");
|
|
return result;
|
|
}
|
|
if (alreadyClosing) _ = CompleteFailureAsync(new Attempt(), PlayerKVLoginPrepareError.Cancelled);
|
|
else _ = PrepareCoreAsync();
|
|
return result;
|
|
}
|
|
|
|
sealed class Attempt
|
|
{
|
|
internal AccountLoginError? AccountError;
|
|
internal int? HttpStatus;
|
|
internal PlayerKVGameOpenError? OpenError;
|
|
internal PlayerKVSessionOpenError? OwnerError;
|
|
internal PlayerKVError? KVError;
|
|
internal PlayerKVSyncStatus? SyncStatus;
|
|
internal string OwnerId, ExceptionType;
|
|
}
|
|
|
|
async Task PrepareCoreAsync()
|
|
{
|
|
var evidence = new Attempt();
|
|
var error = PlayerKVLoginPrepareError.Unexpected;
|
|
try
|
|
{
|
|
registration = firstToken.Register(() => { _ = CloseAsync(); });
|
|
if (!Validate()) error = PlayerKVLoginPrepareError.Validation;
|
|
else if (!EpochCurrent(epoch)) error = InactiveError();
|
|
else
|
|
{
|
|
session = sessionFactory() ?? throw new InvalidOperationException("Session factory returned null.");
|
|
if (session.ApiBaseUri != api) error = PlayerKVLoginPrepareError.Validation;
|
|
else
|
|
{
|
|
var auth = await session.LoginAsync(playerId, lifetime.Token).ConfigureAwait(false);
|
|
evidence.AccountError = auth.Error; evidence.HttpStatus = auth.StatusCode;
|
|
if (!EpochCurrent(epoch)) error = InactiveError();
|
|
else if (!auth.Success) error = PlayerKVLoginPrepareError.AuthenticationFailed;
|
|
else
|
|
{
|
|
cookies = session.Cookies;
|
|
bridge = bridgeFactory() ?? throw new InvalidOperationException("Bridge factory returned null.");
|
|
var opened = await bridge.OpenAsync(session, cacheRoot, playerId, api, epoch,
|
|
EpochCurrent, lifetime.Token).ConfigureAwait(false);
|
|
evidence.OpenError = opened.Error;
|
|
if (opened.Error == PlayerKVGameOpenError.OwnerRejected)
|
|
{ evidence.OwnerError = opened.OwnerError; evidence.KVError = opened.TransportError; }
|
|
if (!EpochCurrent(epoch)) error = InactiveError();
|
|
else if (!opened.Success) error = PlayerKVLoginPrepareError.OwnerRejected;
|
|
else
|
|
{
|
|
handle = opened.Handle;
|
|
evidence.OwnerId = handle.OwnerInstanceId;
|
|
var read = handle.ReadLocalSnapshot();
|
|
evidence.SyncStatus = handle.Status;
|
|
if (!EpochCurrent(epoch)) error = InactiveError();
|
|
else if (!read.Success) error = read.Error == PlayerKVLocalReadError.StaleOrClosing
|
|
? PlayerKVLoginPrepareError.Stale : PlayerKVLoginPrepareError.NotReady;
|
|
else
|
|
{
|
|
var success = Result(evidence, PlayerKVLoginPrepareError.None, read.Data);
|
|
// External callback is never invoked under our monitor. Close/reentry is rechecked at publication.
|
|
if (!EpochCurrent(epoch)) error = InactiveError();
|
|
else lock (gate)
|
|
{
|
|
if (revoked == 0)
|
|
{
|
|
accountDescriptor = new PlayerKVAccountDescriptor(session.PlayerId,
|
|
epoch, session.InternalId, session.NewlyCreated,
|
|
session.CreateTimeUtc, session.ServerTimeUtc);
|
|
Volatile.Write(ref prepared, 1);
|
|
preparation.TrySetResult(success);
|
|
return;
|
|
}
|
|
error = PlayerKVLoginPrepareError.Cancelled;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
evidence.ExceptionType = ex.GetType().Name;
|
|
error = Volatile.Read(ref revoked) != 0 ? PlayerKVLoginPrepareError.Cancelled : PlayerKVLoginPrepareError.Unexpected;
|
|
}
|
|
finally { workSettled.TrySetResult(true); }
|
|
evidence.ExceptionType = evidence.ExceptionType ?? Volatile.Read(ref predicateExceptionType);
|
|
await CompleteFailureAsync(evidence, error).ConfigureAwait(false);
|
|
}
|
|
|
|
bool Validate()
|
|
{
|
|
if (epoch < 0 || externalCurrent == null || sessionFactory == null || bridgeFactory == null ||
|
|
string.IsNullOrWhiteSpace(cacheRoot) || !Path.IsPathFullyQualified(cacheRoot) ||
|
|
!long.TryParse(requestedId, NumberStyles.None, CultureInfo.InvariantCulture, out var id) || id <= 0 ||
|
|
api == null || !api.IsAbsoluteUri || (api.Scheme != "http" && api.Scheme != "https") ||
|
|
api.UserInfo.Length != 0 || api.Query.Length != 0 || api.Fragment.Length != 0) return false;
|
|
try { Path.GetFullPath(cacheRoot); }
|
|
catch (Exception ex) when (ex is ArgumentException || ex is NotSupportedException || ex is PathTooLongException) { return false; }
|
|
playerId = id.ToString(CultureInfo.InvariantCulture);
|
|
return true;
|
|
}
|
|
|
|
bool EpochCurrent(long observedEpoch)
|
|
{
|
|
if (Volatile.Read(ref revoked) != 0 || observedEpoch != epoch) return false;
|
|
try { return externalCurrent != null && externalCurrent(epoch) && Volatile.Read(ref revoked) == 0; }
|
|
catch (Exception ex)
|
|
{
|
|
Interlocked.CompareExchange(ref predicateExceptionType, ex.GetType().Name, null);
|
|
_ = CloseAsync();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
PlayerKVLoginPrepareError InactiveError() => Volatile.Read(ref predicateExceptionType) != null
|
|
? PlayerKVLoginPrepareError.Unexpected : Volatile.Read(ref revoked) != 0
|
|
? PlayerKVLoginPrepareError.Cancelled : PlayerKVLoginPrepareError.Stale;
|
|
|
|
PlayerKVLoginPrepareResult Result(Attempt e, PlayerKVLoginPrepareError error,
|
|
System.Collections.Generic.IReadOnlyDictionary<string, string> data = null,
|
|
string cleanupException = null, PlayerKVLoginPrepareError? original = null) =>
|
|
new PlayerKVLoginPrepareResult(error, playerId, epoch, e.AccountError, e.HttpStatus,
|
|
e.OpenError, e.OwnerError, e.KVError, e.SyncStatus, e.OwnerId, data,
|
|
e.ExceptionType, cleanupException, original);
|
|
|
|
async Task CompleteFailureAsync(Attempt evidence, PlayerKVLoginPrepareError error)
|
|
{
|
|
try
|
|
{
|
|
await CloseAsync().ConfigureAwait(false);
|
|
preparation.TrySetResult(Result(evidence, error));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
preparation.TrySetResult(Result(evidence, PlayerKVLoginPrepareError.CloseFailed,
|
|
cleanupException: ex.GetType().Name, original: error));
|
|
}
|
|
}
|
|
|
|
/// <summary>Synchronously revokes admission; drains preparation and bridge before disposing the session.</summary>
|
|
public Task CloseAsync()
|
|
{
|
|
TaskCompletionSource<bool> completion;
|
|
lock (gate)
|
|
{
|
|
if (closing != null) return closing.Task;
|
|
Volatile.Write(ref revoked, 1);
|
|
completion = closing = NewSignal();
|
|
if (preparation == null) workSettled.TrySetResult(true);
|
|
}
|
|
// A cancellation registration must not execute disposal of itself or user cancellation callbacks inline.
|
|
// Observe faults without changing the shared task, even when cancellation initiates a background Close.
|
|
_ = completion.Task.ContinueWith(t => { _ = t.Exception; }, CancellationToken.None,
|
|
TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
|
|
_ = Task.Run(() => CloseCoreAsync(completion));
|
|
return completion.Task;
|
|
}
|
|
|
|
async Task CloseCoreAsync(TaskCompletionSource<bool> completion)
|
|
{
|
|
try
|
|
{
|
|
Exception cancellationFailure = null;
|
|
try { lifetime.Cancel(); }
|
|
catch (Exception ex) { cancellationFailure = ex; }
|
|
await workSettled.Task.ConfigureAwait(false);
|
|
PlayerKVAccountLease borrowed;
|
|
lock (gate) borrowed = accountLease;
|
|
if (borrowed != null) await borrowed.CloseAsync().ConfigureAwait(false);
|
|
if (bridge != null) await bridge.CloseAsync().ConfigureAwait(false);
|
|
// Never dispose the session on a failed bridge close. References and the faulted task stay retained.
|
|
session?.Dispose();
|
|
registration.Dispose();
|
|
lifetime.Dispose();
|
|
if (cancellationFailure != null) throw cancellationFailure;
|
|
completion.TrySetResult(true);
|
|
}
|
|
catch (Exception ex) { completion.TrySetException(ex); }
|
|
}
|
|
|
|
static TaskCompletionSource<bool> NewSignal() =>
|
|
new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
}
|
|
|
|
// Kept with the context so its existing explicitly linked regression host retains all dependencies.
|
|
internal sealed class PlayerKVAccountDescriptor
|
|
{
|
|
internal string PlayerId { get; }
|
|
internal long Epoch { get; }
|
|
internal uint InternalId { get; }
|
|
internal bool NewlyCreated { get; }
|
|
internal DateTime CreateTimeUtc { get; }
|
|
internal DateTime ServerTimeUtc { get; }
|
|
internal PlayerKVAccountDescriptor(string playerId, long epoch, uint internalId,
|
|
bool newlyCreated, DateTime createTimeUtc, DateTime serverTimeUtc)
|
|
{
|
|
PlayerId = playerId; Epoch = epoch; InternalId = internalId; NewlyCreated = newlyCreated;
|
|
CreateTimeUtc = createTimeUtc; ServerTimeUtc = serverTimeUtc;
|
|
}
|
|
}
|
|
|
|
/// <summary>Controlled transport factory; formal and Editor validation modes remain separate.</summary>
|
|
internal sealed class PlayerKVControlledLoadCapability
|
|
{
|
|
readonly Func<CookieContainer, HttpMessageHandler> leaseHandler;
|
|
readonly bool formal;
|
|
private PlayerKVControlledLoadCapability(Func<CookieContainer, HttpMessageHandler> leaseHandler, bool formal)
|
|
{ this.leaseHandler = leaseHandler ?? throw new ArgumentNullException(nameof(leaseHandler)); this.formal = formal; }
|
|
internal bool IsFormal => formal;
|
|
|
|
internal static PlayerKVControlledLoadCapability CreateForGame(
|
|
Func<CookieContainer, HttpMessageHandler> handler) => new PlayerKVControlledLoadCapability(handler, true);
|
|
|
|
#if UNITY_EDITOR
|
|
internal static PlayerKVControlledLoadCapability CreateForValidation(
|
|
Func<CookieContainer, HttpMessageHandler> leaseHandler) => new PlayerKVControlledLoadCapability(leaseHandler, false);
|
|
#endif
|
|
|
|
internal HttpMessageHandler CreateHandler(CookieContainer cookies)
|
|
{
|
|
return CreateHandler(leaseHandler, cookies, formal);
|
|
}
|
|
|
|
internal static HttpMessageHandler CreateHandler(Func<CookieContainer, HttpMessageHandler> factory,
|
|
CookieContainer cookies, bool formal)
|
|
{
|
|
var handler = factory(cookies) ?? throw new InvalidOperationException("Transport handler required.");
|
|
var seen = new HashSet<HttpMessageHandler>();
|
|
var node = handler;
|
|
while (node != null)
|
|
{
|
|
// Disposing a cyclic DelegatingHandler graph recursively would overflow the stack.
|
|
// It never becomes a lease; the rejecting factory retains responsibility for that graph.
|
|
if (!seen.Add(node))
|
|
{
|
|
throw new InvalidOperationException("Cyclic transport is not a validation handler.");
|
|
}
|
|
if (!formal && (node is HttpClientHandler ||
|
|
node.GetType().FullName == "System.Net.Http.SocketsHttpHandler"))
|
|
{
|
|
handler.Dispose();
|
|
throw new InvalidOperationException("Real or cyclic transport is not a validation handler.");
|
|
}
|
|
if (formal && node is HttpClientHandler real &&
|
|
(!real.UseCookies || !ReferenceEquals(real.CookieContainer, cookies) || real.AllowAutoRedirect))
|
|
{
|
|
handler.Dispose();
|
|
throw new InvalidOperationException("Formal transport must share the session cookies and reject redirects.");
|
|
}
|
|
if (formal && node.GetType().FullName == "System.Net.Http.SocketsHttpHandler")
|
|
{
|
|
handler.Dispose();
|
|
throw new InvalidOperationException("Unverified formal transport handler.");
|
|
}
|
|
node = (node as DelegatingHandler)?.InnerHandler;
|
|
}
|
|
return handler;
|
|
}
|
|
}
|
|
|
|
internal sealed class PlayerKVAccountResponse
|
|
{
|
|
internal bool IsCurrent { get; }
|
|
internal int? StatusCode { get; }
|
|
internal string Body { get; }
|
|
internal PlayerKVAccountResponse(bool isCurrent, int? statusCode = null, string body = null)
|
|
{ IsCurrent = isCurrent; StatusCode = isCurrent ? statusCode : null; Body = isCurrent ? body : null; }
|
|
}
|
|
|
|
/// <summary>A single context-owned, synthetic-only borrower. Close drains before disposing its client.</summary>
|
|
internal sealed class PlayerKVAccountLease
|
|
{
|
|
readonly object gate = new object();
|
|
readonly HttpClient client;
|
|
readonly CookieContainer cookies;
|
|
readonly bool formal;
|
|
readonly Func<bool> current;
|
|
readonly Func<bool> admission;
|
|
readonly CancellationTokenSource lifetime = new CancellationTokenSource();
|
|
TaskCompletionSource<bool> drained;
|
|
TaskCompletionSource<bool> closing;
|
|
int active;
|
|
internal Uri ApiBaseUri { get; }
|
|
internal long Epoch { get; }
|
|
internal bool IsFormal => formal;
|
|
internal CookieContainer FormalCookies => formal && IsCurrent ? cookies : null;
|
|
internal bool IsCurrent
|
|
{
|
|
get
|
|
{
|
|
lock (gate) if (closing != null) return false;
|
|
return current();
|
|
}
|
|
}
|
|
|
|
internal PlayerKVAccountLease(Uri api, long epoch, CookieContainer cookies,
|
|
PlayerKVControlledLoadCapability capability, Func<bool> current, Func<bool> admission)
|
|
{
|
|
if (capability == null) throw new ArgumentNullException(nameof(capability));
|
|
ApiBaseUri = api; Epoch = epoch; this.cookies = cookies;
|
|
formal = capability.IsFormal; this.current = current; this.admission = admission;
|
|
client = new HttpClient(capability.CreateHandler(cookies)) { BaseAddress = api };
|
|
if (formal) client.Timeout = TimeSpan.FromSeconds(15);
|
|
}
|
|
|
|
internal async Task<PlayerKVAccountResponse> SendAsync(HttpMethod method, string route,
|
|
string body = null, string contentType = "application/json", CancellationToken token = default)
|
|
{
|
|
if (formal)
|
|
{
|
|
if (!IsCanonicalBusinessRoute(route))
|
|
throw new ArgumentException("Only canonical relative business routes are allowed.", nameof(route));
|
|
}
|
|
else if (route != "__kv_validation/ok" && route != "__kv_validation/unauthorized")
|
|
throw new ArgumentException("Only canonical validation routes are allowed.", nameof(route));
|
|
if (method != HttpMethod.Get && method != HttpMethod.Post)
|
|
throw new ArgumentException("Unsupported lease method.", nameof(method));
|
|
if (!IsCurrent) return new PlayerKVAccountResponse(false);
|
|
lock (gate)
|
|
{
|
|
if (closing != null || !admission()) return new PlayerKVAccountResponse(false);
|
|
active++;
|
|
}
|
|
try
|
|
{
|
|
using (var linked = CancellationTokenSource.CreateLinkedTokenSource(token, lifetime.Token))
|
|
using (var request = new HttpRequestMessage(method, route))
|
|
{
|
|
if (body != null) request.Content = new StringContent(body, System.Text.Encoding.UTF8, contentType);
|
|
using (var response = await client.SendAsync(request, linked.Token).ConfigureAwait(false))
|
|
{
|
|
if (!IsCurrent || linked.IsCancellationRequested) return new PlayerKVAccountResponse(false);
|
|
var text = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
|
// No redirect following, even if a synthetic handler returns a Location header.
|
|
if ((int)response.StatusCode >= 300 && (int)response.StatusCode < 400)
|
|
throw new InvalidOperationException("Redirects are not accepted.");
|
|
return IsCurrent && !linked.IsCancellationRequested
|
|
? new PlayerKVAccountResponse(true, (int)response.StatusCode, text)
|
|
: new PlayerKVAccountResponse(false);
|
|
}
|
|
}
|
|
}
|
|
catch (OperationCanceledException) when (!IsCurrent || token.IsCancellationRequested || lifetime.IsCancellationRequested)
|
|
{ return new PlayerKVAccountResponse(false); }
|
|
finally
|
|
{
|
|
lock (gate)
|
|
{
|
|
active--;
|
|
if (active == 0) drained?.TrySetResult(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
static bool IsCanonicalBusinessRoute(string route)
|
|
{
|
|
if (string.IsNullOrEmpty(route) || route[0] == '/' || route[0] == '\\' ||
|
|
Uri.TryCreate(route, UriKind.Absolute, out _) ||
|
|
route.IndexOf("//", StringComparison.Ordinal) >= 0 ||
|
|
route.IndexOfAny(new[] { '?', '#', '%', '\\' }) >= 0)
|
|
return false;
|
|
var segments = route.Split('/');
|
|
foreach (var segment in segments)
|
|
{
|
|
if (segment.Length == 0 || segment == "." || segment == "..") return false;
|
|
foreach (var character in segment)
|
|
if (char.IsControl(character)) return false;
|
|
}
|
|
return !route.Equals("__kv_validation", StringComparison.OrdinalIgnoreCase) &&
|
|
!route.StartsWith("__kv_validation/", StringComparison.OrdinalIgnoreCase) &&
|
|
!route.Equals("account/Login", StringComparison.OrdinalIgnoreCase) &&
|
|
!route.Equals("playerdata", StringComparison.OrdinalIgnoreCase) &&
|
|
!route.StartsWith("playerdata/", StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
internal Task CloseAsync()
|
|
{
|
|
TaskCompletionSource<bool> completion;
|
|
lock (gate)
|
|
{
|
|
if (closing != null) return closing.Task;
|
|
completion = closing = Signal();
|
|
drained = Signal();
|
|
if (active == 0) drained.TrySetResult(true);
|
|
}
|
|
_ = completion.Task.ContinueWith(t => { _ = t.Exception; }, CancellationToken.None,
|
|
TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
|
|
_ = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
Exception cancellationFailure = null;
|
|
try { lifetime.Cancel(); }
|
|
catch (Exception ex) { cancellationFailure = ex; }
|
|
await drained.Task.ConfigureAwait(false);
|
|
client.Dispose();
|
|
lifetime.Dispose();
|
|
if (cancellationFailure != null) throw cancellationFailure;
|
|
completion.TrySetResult(true);
|
|
}
|
|
catch (Exception ex) { completion.TrySetException(ex); }
|
|
});
|
|
return completion.Task;
|
|
}
|
|
|
|
static TaskCompletionSource<bool> Signal() =>
|
|
new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
}
|
|
}
|