mirror of
https://github.com/esiur/esiur-dotnet.git
synced 2026-07-30 17:30:40 +00:00
ASP.Net
This commit is contained in:
@@ -22,6 +22,7 @@ SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
using Esiur.Misc;
|
||||
using Esiur.Resource;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -217,7 +218,17 @@ public class AsyncReply
|
||||
return this;
|
||||
}
|
||||
|
||||
public void Trigger(object result)
|
||||
public void Trigger(object result) => TrySetResult(result, dispatchCallbacksAsynchronously: false);
|
||||
|
||||
/// <summary>
|
||||
/// Completes the reply immediately while dispatching callbacks independently.
|
||||
/// This is reserved for teardown paths where consumer code must not hold a
|
||||
/// transport or other infrastructure resource open.
|
||||
/// </summary>
|
||||
internal void TriggerDetached(object result) =>
|
||||
TrySetResult(result, dispatchCallbacksAsynchronously: true);
|
||||
|
||||
private bool TrySetResult(object result, bool dispatchCallbacksAsynchronously)
|
||||
{
|
||||
Action<object> singleCallback = null;
|
||||
Action<object>[] registeredCallbacks = null;
|
||||
@@ -227,7 +238,7 @@ public class AsyncReply
|
||||
lock (asyncLock)
|
||||
{
|
||||
if (exception != null || resultReady)
|
||||
return;
|
||||
return false;
|
||||
|
||||
ReadyTime = DateTime.Now;
|
||||
this.result = result;
|
||||
@@ -250,11 +261,24 @@ public class AsyncReply
|
||||
|
||||
waiter?.Set();
|
||||
|
||||
if (callbackCount == 1)
|
||||
if (dispatchCallbacksAsynchronously)
|
||||
{
|
||||
if (callbackCount == 1)
|
||||
QueueDetachedCallbacks(singleCallback, null, result);
|
||||
else if (registeredCallbacks != null)
|
||||
QueueDetachedCallbacks(null, registeredCallbacks, result);
|
||||
}
|
||||
else if (callbackCount == 1)
|
||||
{
|
||||
singleCallback(result);
|
||||
}
|
||||
else if (registeredCallbacks != null)
|
||||
{
|
||||
foreach (var callback in registeredCallbacks)
|
||||
callback(result);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public virtual void TriggerError(Exception exception)
|
||||
@@ -262,6 +286,19 @@ public class AsyncReply
|
||||
TrySetException(exception);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Faults the reply immediately while dispatching callbacks independently.
|
||||
/// This is reserved for teardown paths where consumer code must not hold a
|
||||
/// transport or other infrastructure resource open.
|
||||
/// </summary>
|
||||
internal void TriggerErrorDetached(Exception exception)
|
||||
{
|
||||
TrySetException(
|
||||
exception,
|
||||
preserveSourceForAwait: false,
|
||||
dispatchCallbacksAsynchronously: true);
|
||||
}
|
||||
|
||||
internal void TriggerErrorFromBuilder(Exception exception, bool preserveSourceForAwait)
|
||||
{
|
||||
TrySetException(exception, preserveSourceForAwait);
|
||||
@@ -347,7 +384,10 @@ public class AsyncReply
|
||||
}
|
||||
}
|
||||
|
||||
private bool TrySetException(Exception sourceException, bool preserveSourceForAwait = false)
|
||||
private bool TrySetException(
|
||||
Exception sourceException,
|
||||
bool preserveSourceForAwait = false,
|
||||
bool dispatchCallbacksAsynchronously = false)
|
||||
{
|
||||
AsyncException asyncException;
|
||||
Action<AsyncException> singleCallback = null;
|
||||
@@ -378,12 +418,52 @@ public class AsyncReply
|
||||
|
||||
waiter?.Set();
|
||||
|
||||
if (callbackCount == 1)
|
||||
if (dispatchCallbacksAsynchronously)
|
||||
{
|
||||
if (callbackCount == 1)
|
||||
QueueDetachedCallbacks(singleCallback, null, asyncException);
|
||||
else if (registeredCallbacks != null)
|
||||
QueueDetachedCallbacks(null, registeredCallbacks, asyncException);
|
||||
}
|
||||
else if (callbackCount == 1)
|
||||
{
|
||||
singleCallback(asyncException);
|
||||
}
|
||||
else if (registeredCallbacks != null)
|
||||
{
|
||||
foreach (var callback in registeredCallbacks)
|
||||
callback(asyncException);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void QueueDetachedCallbacks<T>(
|
||||
Action<T> singleCallback,
|
||||
Action<T>[] registeredCallbacks,
|
||||
T value)
|
||||
{
|
||||
ThreadPool.QueueUserWorkItem(_ =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (singleCallback != null)
|
||||
{
|
||||
singleCallback(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var callback in registeredCallbacks)
|
||||
{
|
||||
try { callback(value); }
|
||||
catch (Exception exception) { Global.Log(exception); }
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Global.Log(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
<PackageId>Esiur</PackageId>
|
||||
<Product>Esiur</Product>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<!-- Keep CallerFilePath/PDB entries deterministic and avoid embedding the
|
||||
build machine's absolute source directory in the package assembly. -->
|
||||
<PathMap>$(MSBuildProjectDirectory)=/_/Esiur</PathMap>
|
||||
<!-- Enable the nullable *annotation* context project-wide so '?' reference-type
|
||||
annotations are legal everywhere, without turning on nullable flow warnings
|
||||
(files that opt in via '#nullable enable' still get full checking). -->
|
||||
@@ -39,6 +42,8 @@
|
||||
<ItemGroup>
|
||||
<!-- Allow the unit test project to exercise internal helpers (e.g. wait-for cycle detection). -->
|
||||
<InternalsVisibleTo Include="Esiur.Tests.Unit" />
|
||||
<!-- The ASP.NET adapter supplies a host-wide transport memory budget. -->
|
||||
<InternalsVisibleTo Include="Esiur.AspNetCore" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -50,7 +55,9 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BouncyCastle.Cryptography" Version="2.6.2" />
|
||||
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" PrivateAssets="all" />
|
||||
<!-- Compile the generator against the .NET 8 Roslyn baseline so the combined
|
||||
runtime/analyzer assembly does not require System.Collections.Immutable 9. -->
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="4.14.0" PrivateAssets="all" />
|
||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||
<PackageReference Include="System.Reflection.Emit" Version="4.7.0" />
|
||||
|
||||
@@ -53,6 +53,13 @@ public abstract class NetworkServer<TConnection> : IDestructible where TConnecti
|
||||
|
||||
public event DestroyedEvent OnDestroy;
|
||||
|
||||
protected NetworkServer()
|
||||
{
|
||||
// Connection tracking is also used by externally hosted transports (for example,
|
||||
// ASP.NET Core WebSockets) that don't create an ISocket listener through Start().
|
||||
Connections = new AutoList<TConnection, NetworkServer<TConnection>>(this);
|
||||
}
|
||||
|
||||
|
||||
private void MinuteThread(object state)
|
||||
{
|
||||
@@ -93,8 +100,6 @@ public abstract class NetworkServer<TConnection> : IDestructible where TConnecti
|
||||
if (listener != null)
|
||||
return;
|
||||
|
||||
Connections = new AutoList<TConnection, NetworkServer<TConnection>>(this);
|
||||
|
||||
if (Timeout > 0 && Clock > 0)
|
||||
{
|
||||
timer = new Timer(MinuteThread, null, TimeSpan.FromMinutes(0), TimeSpan.FromSeconds(Clock));
|
||||
@@ -273,7 +278,12 @@ public abstract class NetworkServer<TConnection> : IDestructible where TConnecti
|
||||
finally
|
||||
{
|
||||
try { currentTimer?.Dispose(); } catch { }
|
||||
Global.Log("NetworkServer", LogType.Warning, $"Server on port {port} is down.");
|
||||
if (currentListener != null
|
||||
|| currentTimer != null
|
||||
|| (connections?.Length ?? 0) > 0)
|
||||
{
|
||||
Global.Log("NetworkServer", LogType.Debug, $"Server on port {port} is down.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -115,6 +115,12 @@ public partial class EpConnection : NetworkConnection, IStore
|
||||
Session _session;
|
||||
|
||||
AsyncReply<bool> _openReply;
|
||||
readonly object _connectLifecycleLock = new object();
|
||||
long _connectAttemptGeneration;
|
||||
bool _isDestroyed;
|
||||
bool _connectAttemptIsRetrying;
|
||||
CancellationTokenSource _connectRetryCancellation;
|
||||
volatile bool _autoReconnect;
|
||||
|
||||
bool _authenticated;
|
||||
bool _authenticationHandshakeSucceeded;
|
||||
@@ -191,7 +197,44 @@ public partial class EpConnection : NetworkConnection, IStore
|
||||
|
||||
|
||||
//[Attribute]
|
||||
public bool AutoReconnect { get; set; } = false;
|
||||
public bool AutoReconnect
|
||||
{
|
||||
get => _autoReconnect;
|
||||
set
|
||||
{
|
||||
AsyncReply<bool> canceledReply = null;
|
||||
CancellationTokenSource retryCancellation = null;
|
||||
|
||||
lock (_connectLifecycleLock)
|
||||
{
|
||||
_autoReconnect = value;
|
||||
if (value || !_connectAttemptIsRetrying)
|
||||
return;
|
||||
|
||||
_connectAttemptIsRetrying = false;
|
||||
retryCancellation = _connectRetryCancellation;
|
||||
_connectRetryCancellation = null;
|
||||
|
||||
var pending = Volatile.Read(ref _openReply);
|
||||
if (pending != null
|
||||
&& ReferenceEquals(
|
||||
Interlocked.CompareExchange(ref _openReply, null, pending),
|
||||
pending))
|
||||
{
|
||||
Status = EpConnectionStatus.Closed;
|
||||
canceledReply = pending;
|
||||
}
|
||||
}
|
||||
|
||||
CancelAndDispose(retryCancellation);
|
||||
TriggerOpenError(
|
||||
canceledReply,
|
||||
new AsyncException(
|
||||
ErrorType.Management,
|
||||
0,
|
||||
"Automatic reconnect was canceled."));
|
||||
}
|
||||
}
|
||||
|
||||
//[Attribute]
|
||||
public uint ReconnectInterval { get; set; } = 5;
|
||||
@@ -199,11 +242,15 @@ public partial class EpConnection : NetworkConnection, IStore
|
||||
//[Attribute]
|
||||
//public string Username { get; set; }
|
||||
|
||||
//[Attribute]
|
||||
public bool UseWebSocket { get; set; }
|
||||
/// <summary>
|
||||
/// Full WebSocket endpoint used by outbound connections. When null, EP uses
|
||||
/// its native TCP transport (except on browser runtimes).
|
||||
/// </summary>
|
||||
public Uri WebSocketUri { get; set; }
|
||||
|
||||
//[Attribute]
|
||||
public bool SecureWebSocket { get; set; }
|
||||
// Test seam for verifying the same fresh-socket policy used by native TCP and
|
||||
// FrameworkWebSocket reconnects without expanding the public API.
|
||||
internal Func<ISocket> ClientSocketFactory { get; set; }
|
||||
|
||||
//[Attribute]
|
||||
//public string Password { get; set; }
|
||||
@@ -857,17 +904,16 @@ public partial class EpConnection : NetworkConnection, IStore
|
||||
void FailPendingOpen(Exception exception)
|
||||
{
|
||||
var pending = Interlocked.Exchange(ref _openReply, null);
|
||||
TriggerOpenError(pending, exception);
|
||||
}
|
||||
|
||||
static void TriggerOpenError(AsyncReply<bool> pending, Exception exception)
|
||||
{
|
||||
if (pending == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
pending.TriggerError(exception);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Global.Log(ex);
|
||||
}
|
||||
try { pending.TriggerError(exception); }
|
||||
catch (Exception ex) { Global.Log(ex); }
|
||||
}
|
||||
|
||||
void BeginPlaintextHandshake()
|
||||
@@ -1005,14 +1051,9 @@ public partial class EpConnection : NetworkConnection, IStore
|
||||
{
|
||||
if (cancellation == null)
|
||||
return;
|
||||
try
|
||||
{
|
||||
cancellation.Cancel();
|
||||
}
|
||||
finally
|
||||
{
|
||||
cancellation.Dispose();
|
||||
}
|
||||
|
||||
try { cancellation.Cancel(); } catch (ObjectDisposedException) { }
|
||||
try { cancellation.Dispose(); } catch (ObjectDisposedException) { }
|
||||
}
|
||||
|
||||
bool TryPublishAuthenticationReady(
|
||||
@@ -1233,6 +1274,23 @@ public partial class EpConnection : NetworkConnection, IStore
|
||||
|
||||
public override void Destroy()
|
||||
{
|
||||
CancellationTokenSource retryCancellation;
|
||||
lock (_connectLifecycleLock)
|
||||
{
|
||||
_isDestroyed = true;
|
||||
_connectAttemptGeneration++;
|
||||
_autoReconnect = false;
|
||||
_connectAttemptIsRetrying = false;
|
||||
retryCancellation = _connectRetryCancellation;
|
||||
_connectRetryCancellation = null;
|
||||
Status = EpConnectionStatus.Closed;
|
||||
}
|
||||
|
||||
CancelAndDispose(retryCancellation);
|
||||
FailPendingOpen(new AsyncException(
|
||||
ErrorType.Management,
|
||||
0,
|
||||
"The connection was destroyed before it finished opening."));
|
||||
TerminateInvocations();
|
||||
UnsubscribeAll();
|
||||
EndAuthenticationAttempt();
|
||||
@@ -1479,8 +1537,24 @@ public partial class EpConnection : NetworkConnection, IStore
|
||||
Socket?.Unhold();
|
||||
|
||||
var res = new HttpResponsePacket();
|
||||
HttpConnection.Upgrade(
|
||||
req,
|
||||
res,
|
||||
new[] { FrameworkWebSocket.SubProtocol },
|
||||
out var selectedSubProtocol);
|
||||
|
||||
HttpConnection.Upgrade(req, res);
|
||||
if (selectedSubProtocol != FrameworkWebSocket.SubProtocol)
|
||||
{
|
||||
res = new HttpResponsePacket
|
||||
{
|
||||
Number = HttpResponseCode.BadRequest,
|
||||
Text = "The EP WebSocket subprotocol is required."
|
||||
};
|
||||
res.Compose(HttpComposeOption.AllCalculateLength);
|
||||
Send(res.Data);
|
||||
Close();
|
||||
return ends;
|
||||
}
|
||||
|
||||
res.Compose(HttpComposeOption.AllCalculateLength);
|
||||
Send(res.Data);
|
||||
@@ -3101,8 +3175,7 @@ public partial class EpConnection : NetworkConnection, IStore
|
||||
ReconnectInterval = epContext.ReconnectInterval;
|
||||
AuthenticationTimeout = epContext.AuthenticationTimeout;
|
||||
ExceptionLevel = epContext.ExceptionLevel;
|
||||
UseWebSocket = epContext.UseWebSocket;
|
||||
SecureWebSocket = epContext.SecureWebSocket;
|
||||
WebSocketUri = epContext.WebSocketUri;
|
||||
AutoReconnect = epContext.AutoReconnect;
|
||||
_hostname = address;
|
||||
_port = port;
|
||||
@@ -3124,77 +3197,335 @@ public partial class EpConnection : NetworkConnection, IStore
|
||||
|
||||
public AsyncReply<bool> Connect(ISocket socket = null, string hostname = null, ushort port = 0, string domain = null)
|
||||
{
|
||||
if (IsConnected || Status == EpConnectionStatus.Connected)
|
||||
throw new AsyncException(ErrorType.Exception, 0, "Connection is already established");
|
||||
var openReply = new AsyncReply<bool>();
|
||||
if (Interlocked.CompareExchange(ref _openReply, openReply, null) != null)
|
||||
throw new AsyncException(ErrorType.Exception, 0, "Connection in progress");
|
||||
AsyncReply<bool> openReply;
|
||||
long attemptGeneration;
|
||||
bool canCreateReplacement;
|
||||
|
||||
Status = EpConnectionStatus.Connecting;
|
||||
|
||||
// set auth direction to initiator
|
||||
_authDirection = AuthenticationDirection.Initiator;
|
||||
|
||||
if (hostname != null)
|
||||
lock (_connectLifecycleLock)
|
||||
{
|
||||
DisposeSessionEncryption();
|
||||
_session = new Session();
|
||||
_authDirection = AuthenticationDirection.Initiator;
|
||||
_invalidCredentials = false;
|
||||
if (_isDestroyed)
|
||||
throw new AsyncException(ErrorType.Exception, 0, "Connection was destroyed");
|
||||
if (IsConnected || Status == EpConnectionStatus.Connected)
|
||||
throw new AsyncException(ErrorType.Exception, 0, "Connection is already established");
|
||||
|
||||
_session.LocalHeaders.Domain = domain;
|
||||
_hostname = hostname;
|
||||
openReply = new AsyncReply<bool>();
|
||||
if (Interlocked.CompareExchange(ref _openReply, openReply, null) != null)
|
||||
throw new AsyncException(ErrorType.Exception, 0, "Connection in progress");
|
||||
|
||||
attemptGeneration = ++_connectAttemptGeneration;
|
||||
_connectAttemptIsRetrying = false;
|
||||
|
||||
try
|
||||
{
|
||||
Status = EpConnectionStatus.Connecting;
|
||||
|
||||
// set auth direction to initiator
|
||||
_authDirection = AuthenticationDirection.Initiator;
|
||||
|
||||
if (hostname != null)
|
||||
{
|
||||
DisposeSessionEncryption();
|
||||
_session = new Session();
|
||||
_authDirection = AuthenticationDirection.Initiator;
|
||||
_invalidCredentials = false;
|
||||
|
||||
_session.LocalHeaders.Domain = domain;
|
||||
_hostname = hostname;
|
||||
}
|
||||
|
||||
if (port > 0)
|
||||
this._port = port;
|
||||
|
||||
if (_session == null)
|
||||
throw new AsyncException(ErrorType.Exception, 0, "Session not initialized");
|
||||
|
||||
if (_authenticationProvider != null && _authenticationContext != null)
|
||||
{
|
||||
DisposeAuthenticationHandler();
|
||||
_session.AuthenticationHandler =
|
||||
_authenticationProvider.CreateAuthenticationHandler(_authenticationContext);
|
||||
}
|
||||
|
||||
BeginPlaintextHandshake();
|
||||
|
||||
canCreateReplacement = socket == null;
|
||||
socket ??= CreateClientSocket();
|
||||
}
|
||||
catch
|
||||
{
|
||||
Interlocked.CompareExchange(ref _openReply, null, openReply);
|
||||
_connectAttemptIsRetrying = false;
|
||||
Status = EpConnectionStatus.Closed;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
if (port > 0)
|
||||
this._port = port;
|
||||
|
||||
if (_session == null)
|
||||
throw new AsyncException(ErrorType.Exception, 0, "Session not initialized");
|
||||
|
||||
if (_authenticationProvider != null && _authenticationContext != null)
|
||||
{
|
||||
DisposeAuthenticationHandler();
|
||||
_session.AuthenticationHandler =
|
||||
_authenticationProvider.CreateAuthenticationHandler(_authenticationContext);
|
||||
}
|
||||
|
||||
BeginPlaintextHandshake();
|
||||
|
||||
if (socket == null)
|
||||
{
|
||||
var os = RuntimeInformation.FrameworkDescription;
|
||||
if (UseWebSocket || RuntimeInformation.OSDescription == "Browser")
|
||||
socket = new FrameworkWebSocket();
|
||||
else
|
||||
socket = new TcpSocket();
|
||||
}
|
||||
|
||||
|
||||
connectSocket(socket);
|
||||
connectSocket(socket, canCreateReplacement, openReply, attemptGeneration);
|
||||
|
||||
return openReply;
|
||||
}
|
||||
|
||||
void connectSocket(ISocket socket)
|
||||
ISocket CreateClientSocket()
|
||||
{
|
||||
socket.Connect(this._hostname, this._port).Then(x =>
|
||||
if (ClientSocketFactory != null)
|
||||
return ClientSocketFactory();
|
||||
if (WebSocketUri != null)
|
||||
return new FrameworkWebSocket(WebSocketUri);
|
||||
if (RuntimeInformation.OSDescription == "Browser")
|
||||
{
|
||||
Assign(socket);
|
||||
}).Error((x) =>
|
||||
{
|
||||
if (AutoReconnect)
|
||||
{
|
||||
Global.Log("EpConnection", LogType.Debug, "Reconnecting socket...");
|
||||
Task.Delay(TimeSpan.FromSeconds(ReconnectInterval))
|
||||
.ContinueWith((x) => connectSocket(socket));
|
||||
}
|
||||
else
|
||||
{
|
||||
FailPendingOpen(x);
|
||||
}
|
||||
});
|
||||
return new FrameworkWebSocket(
|
||||
new UriBuilder("ws", _hostname, _port).Uri);
|
||||
}
|
||||
|
||||
return new TcpSocket();
|
||||
}
|
||||
|
||||
void connectSocket(
|
||||
ISocket socket,
|
||||
bool canCreateReplacement,
|
||||
AsyncReply<bool> expectedOpenReply,
|
||||
long attemptGeneration)
|
||||
{
|
||||
if (!IsCurrentConnectAttempt(expectedOpenReply, attemptGeneration))
|
||||
{
|
||||
DestroyStaleSocket(socket);
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncReply<bool> connectReply;
|
||||
try
|
||||
{
|
||||
connectReply = socket.Connect(this._hostname, this._port);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
HandleSocketConnectFailure(
|
||||
socket,
|
||||
canCreateReplacement,
|
||||
expectedOpenReply,
|
||||
attemptGeneration,
|
||||
exception);
|
||||
return;
|
||||
}
|
||||
|
||||
connectReply.Then(connected =>
|
||||
{
|
||||
if (!connected)
|
||||
{
|
||||
HandleSocketConnectFailure(
|
||||
socket,
|
||||
canCreateReplacement,
|
||||
expectedOpenReply,
|
||||
attemptGeneration,
|
||||
new AsyncException(
|
||||
ErrorType.Management,
|
||||
0,
|
||||
"The socket did not establish a connection."));
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var staleAttempt = false;
|
||||
lock (_connectLifecycleLock)
|
||||
{
|
||||
staleAttempt = !IsCurrentConnectAttemptLocked(
|
||||
expectedOpenReply,
|
||||
attemptGeneration);
|
||||
if (!staleAttempt)
|
||||
{
|
||||
_connectAttemptIsRetrying = false;
|
||||
Assign(socket);
|
||||
// Some transports begin their receive loop as part of Connect
|
||||
// and report false here to mean "already started". Preserve that
|
||||
// valid contract; assignment/handshake failures still throw.
|
||||
socket.Begin();
|
||||
}
|
||||
}
|
||||
|
||||
if (staleAttempt)
|
||||
DestroyStaleSocket(socket);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
// A connected transport that cannot be assigned or initialize the EP
|
||||
// handshake has a deterministic configuration/protocol failure. A new
|
||||
// socket cannot correct it.
|
||||
HandleSocketConnectFailure(
|
||||
socket,
|
||||
canCreateReplacement: false,
|
||||
expectedOpenReply,
|
||||
attemptGeneration,
|
||||
exception);
|
||||
}
|
||||
}).Error(exception =>
|
||||
HandleSocketConnectFailure(
|
||||
socket,
|
||||
canCreateReplacement,
|
||||
expectedOpenReply,
|
||||
attemptGeneration,
|
||||
exception));
|
||||
}
|
||||
|
||||
void HandleSocketConnectFailure(
|
||||
ISocket socket,
|
||||
bool canCreateReplacement,
|
||||
AsyncReply<bool> expectedOpenReply,
|
||||
long attemptGeneration,
|
||||
Exception exception)
|
||||
{
|
||||
if (!IsCurrentConnectAttempt(expectedOpenReply, attemptGeneration))
|
||||
{
|
||||
DestroyStaleSocket(socket);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(Socket, socket))
|
||||
Unassign();
|
||||
|
||||
DestroyStaleSocket(socket);
|
||||
|
||||
CancellationTokenSource retryCancellation = null;
|
||||
CancellationTokenSource previousRetryCancellation = null;
|
||||
if (canCreateReplacement)
|
||||
{
|
||||
lock (_connectLifecycleLock)
|
||||
{
|
||||
if (_autoReconnect
|
||||
&& IsCurrentConnectAttemptLocked(expectedOpenReply, attemptGeneration))
|
||||
{
|
||||
_connectAttemptIsRetrying = true;
|
||||
retryCancellation = new CancellationTokenSource();
|
||||
previousRetryCancellation = _connectRetryCancellation;
|
||||
_connectRetryCancellation = retryCancellation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CancelAndDispose(previousRetryCancellation);
|
||||
|
||||
if (retryCancellation != null)
|
||||
{
|
||||
Global.Log(
|
||||
"EpConnection",
|
||||
LogType.Debug,
|
||||
$"Reconnecting with a fresh socket after: {exception.Message}");
|
||||
_ = RetryConnectSocketAsync(
|
||||
retryCancellation,
|
||||
expectedOpenReply,
|
||||
attemptGeneration);
|
||||
return;
|
||||
}
|
||||
|
||||
FailConnectAttempt(expectedOpenReply, attemptGeneration, exception);
|
||||
}
|
||||
|
||||
async Task RetryConnectSocketAsync(
|
||||
CancellationTokenSource retryCancellation,
|
||||
AsyncReply<bool> expectedOpenReply,
|
||||
long attemptGeneration)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(
|
||||
TimeSpan.FromSeconds(ReconnectInterval),
|
||||
retryCancellation.Token);
|
||||
}
|
||||
catch (OperationCanceledException) when (retryCancellation.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (_connectLifecycleLock)
|
||||
{
|
||||
if (ReferenceEquals(_connectRetryCancellation, retryCancellation))
|
||||
_connectRetryCancellation = null;
|
||||
}
|
||||
|
||||
retryCancellation.Dispose();
|
||||
}
|
||||
|
||||
lock (_connectLifecycleLock)
|
||||
{
|
||||
if (!_autoReconnect
|
||||
|| !IsCurrentConnectAttemptLocked(expectedOpenReply, attemptGeneration))
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
connectSocket(
|
||||
CreateClientSocket(),
|
||||
canCreateReplacement: true,
|
||||
expectedOpenReply,
|
||||
attemptGeneration);
|
||||
}
|
||||
catch (Exception retryException)
|
||||
{
|
||||
FailConnectAttempt(
|
||||
expectedOpenReply,
|
||||
attemptGeneration,
|
||||
retryException);
|
||||
}
|
||||
}
|
||||
|
||||
bool IsCurrentConnectAttempt(
|
||||
AsyncReply<bool> expectedOpenReply,
|
||||
long attemptGeneration)
|
||||
{
|
||||
lock (_connectLifecycleLock)
|
||||
return IsCurrentConnectAttemptLocked(expectedOpenReply, attemptGeneration);
|
||||
}
|
||||
|
||||
bool IsCurrentConnectAttemptLocked(
|
||||
AsyncReply<bool> expectedOpenReply,
|
||||
long attemptGeneration)
|
||||
=> !_isDestroyed
|
||||
&& _connectAttemptGeneration == attemptGeneration
|
||||
&& ReferenceEquals(Volatile.Read(ref _openReply), expectedOpenReply);
|
||||
|
||||
void FailConnectAttempt(
|
||||
AsyncReply<bool> expectedOpenReply,
|
||||
long attemptGeneration,
|
||||
Exception exception)
|
||||
{
|
||||
var ownsAttempt = false;
|
||||
CancellationTokenSource retryCancellation = null;
|
||||
lock (_connectLifecycleLock)
|
||||
{
|
||||
if (IsCurrentConnectAttemptLocked(expectedOpenReply, attemptGeneration))
|
||||
{
|
||||
ownsAttempt = ReferenceEquals(
|
||||
Interlocked.CompareExchange(
|
||||
ref _openReply,
|
||||
null,
|
||||
expectedOpenReply),
|
||||
expectedOpenReply);
|
||||
|
||||
if (ownsAttempt)
|
||||
{
|
||||
Status = EpConnectionStatus.Closed;
|
||||
_connectAttemptIsRetrying = false;
|
||||
retryCancellation = _connectRetryCancellation;
|
||||
_connectRetryCancellation = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!ownsAttempt)
|
||||
return;
|
||||
|
||||
CancelAndDispose(retryCancellation);
|
||||
TriggerOpenError(expectedOpenReply, exception);
|
||||
}
|
||||
|
||||
void DestroyStaleSocket(ISocket socket)
|
||||
{
|
||||
if (ReferenceEquals(Socket, socket))
|
||||
return;
|
||||
|
||||
try { socket.Destroy(); } catch { }
|
||||
}
|
||||
|
||||
public async AsyncReply<bool> Reconnect()
|
||||
@@ -3414,8 +3745,17 @@ public partial class EpConnection : NetworkConnection, IStore
|
||||
|
||||
if (AutoReconnect && !_invalidCredentials)
|
||||
{
|
||||
Task.Delay(TimeSpan.FromSeconds(ReconnectInterval))
|
||||
.ContinueWith((x) => Reconnect());
|
||||
_ = Task.Delay(TimeSpan.FromSeconds(ReconnectInterval))
|
||||
.ContinueWith(completedDelay =>
|
||||
{
|
||||
lock (_connectLifecycleLock)
|
||||
{
|
||||
if (!_autoReconnect || _isDestroyed)
|
||||
return;
|
||||
|
||||
_ = Reconnect();
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -15,23 +15,6 @@ namespace Esiur.Protocol;
|
||||
|
||||
public class EpConnectionContext : IResourceContext
|
||||
{
|
||||
//public EpConnectionContext()
|
||||
// : base(0, new Map<string, object>(), null, null)
|
||||
//{
|
||||
|
||||
//}
|
||||
|
||||
//public override void Build()
|
||||
//{
|
||||
// Attributes["AutoConnect"] = AutoReconnect;
|
||||
// Attributes["ReconnectInterval"] = ReconnectInterval;
|
||||
// Attributes["UseWebSocket"] = UseWebSocket;
|
||||
// Attributes["SecureWebSocket"] = SecureWebSocket;
|
||||
// Attributes["Domain"] = SecureWebSocket;
|
||||
// Attributes["AuthenticationProtocol"] = SecureWebSocket;
|
||||
// Attributes["Identity"] = SecureWebSocket;
|
||||
//}
|
||||
|
||||
public ExceptionLevel ExceptionLevel { get; set; }
|
||||
= ExceptionLevel.Code | ExceptionLevel.Message | ExceptionLevel.Source | ExceptionLevel.Trace;
|
||||
|
||||
@@ -74,9 +57,11 @@ public class EpConnectionContext : IResourceContext
|
||||
|
||||
//public string Username { get; set; }
|
||||
|
||||
public bool UseWebSocket { get; set; }
|
||||
|
||||
public bool SecureWebSocket { get; set; }
|
||||
/// <summary>
|
||||
/// Uses WebSocket transport when set. The absolute <c>ws</c> or <c>wss</c>
|
||||
/// URI can include the endpoint path and query string.
|
||||
/// </summary>
|
||||
public Uri WebSocketUri { get; set; }
|
||||
|
||||
// public string Password { get; set; }
|
||||
|
||||
|
||||
@@ -52,9 +52,11 @@ public class EpServer : NetworkServer<EpConnection>, IResource
|
||||
|
||||
readonly object _peerConnectionsLock = new object();
|
||||
readonly Dictionary<IPAddress, int> _peerConnectionCounts = new Dictionary<IPAddress, int>();
|
||||
readonly Dictionary<EpConnection, IPAddress> _admittedConnections = new Dictionary<EpConnection, IPAddress>();
|
||||
readonly Dictionary<EpConnection, IPAddress?> _admittedConnections =
|
||||
new Dictionary<EpConnection, IPAddress?>();
|
||||
readonly Dictionary<IPAddress, PeerAttemptWindow> _peerAttemptWindows =
|
||||
new Dictionary<IPAddress, PeerAttemptWindow>();
|
||||
readonly PeerAttemptWindow _globalAttemptWindow = new PeerAttemptWindow();
|
||||
uint _attemptSweepSequence;
|
||||
|
||||
|
||||
@@ -136,6 +138,12 @@ public class EpServer : NetworkServer<EpConnection>, IResource
|
||||
set;
|
||||
} = 10518;
|
||||
|
||||
/// <summary>
|
||||
/// Controls whether warehouse initialization opens Esiur's native TCP listener.
|
||||
/// Disable this when an external host, such as ASP.NET Core, supplies connections.
|
||||
/// </summary>
|
||||
public bool EnableTcpListener { get; set; } = true;
|
||||
|
||||
|
||||
//[Attribute]
|
||||
public ExceptionLevel ExceptionLevel { get; set; }
|
||||
@@ -157,6 +165,9 @@ public class EpServer : NetworkServer<EpConnection>, IResource
|
||||
{
|
||||
if (operation == ResourceOperation.Initialize)
|
||||
{
|
||||
if (!EnableTcpListener)
|
||||
return new AsyncReply<bool>(true);
|
||||
|
||||
TcpSocket listener;
|
||||
|
||||
if (IP != null)
|
||||
@@ -194,15 +205,31 @@ public class EpServer : NetworkServer<EpConnection>, IResource
|
||||
}
|
||||
|
||||
public override void Add(EpConnection connection)
|
||||
=> TryAdd(connection);
|
||||
|
||||
/// <summary>
|
||||
/// Applies admission controls, configures, and tracks an accepted EP connection.
|
||||
/// The connection must already have its socket assigned so peer-based policies can
|
||||
/// inspect the actual remote endpoint.
|
||||
/// </summary>
|
||||
/// <returns><see langword="true"/> when the connection was admitted.</returns>
|
||||
public bool TryAdd(EpConnection connection)
|
||||
{
|
||||
if (connection == null)
|
||||
throw new ArgumentNullException(nameof(connection));
|
||||
|
||||
if (connection.Socket == null)
|
||||
throw new InvalidOperationException(
|
||||
"Assign a socket before adding an EP connection to the server.");
|
||||
|
||||
if (!TryAdmitConnection(connection, out var rejectionReason))
|
||||
{
|
||||
Global.Log(
|
||||
"EpServer:ConnectionLimit",
|
||||
LogType.Warning,
|
||||
LogType.Debug,
|
||||
$"Rejected connection from {connection.RemoteEndPoint?.Address}: {rejectionReason}");
|
||||
connection.Close();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
@@ -218,6 +245,7 @@ public class EpServer : NetworkServer<EpConnection>, IResource
|
||||
connection.AuthenticationTimeout = AuthenticationTimeout;
|
||||
connection.RestartAuthenticationDeadline();
|
||||
base.Add(connection);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -243,14 +271,50 @@ public class EpServer : NetworkServer<EpConnection>, IResource
|
||||
{
|
||||
rejectionReason = null;
|
||||
var address = NormalizeAddress(connection.RemoteEndPoint?.Address);
|
||||
if (address == null)
|
||||
return true;
|
||||
|
||||
lock (_peerConnectionsLock)
|
||||
{
|
||||
var configuration = Instance.Warehouse.Configuration.Connections;
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
if (_admittedConnections.ContainsKey(connection))
|
||||
{
|
||||
rejectionReason = "connection is already admitted";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (configuration.MaximumConnectionAttempts > 0
|
||||
&& configuration.ConnectionAttemptWindow > TimeSpan.Zero)
|
||||
{
|
||||
if (now - _globalAttemptWindow.StartedUtc
|
||||
>= configuration.ConnectionAttemptWindow)
|
||||
{
|
||||
_globalAttemptWindow.StartedUtc = now;
|
||||
_globalAttemptWindow.Count = 0;
|
||||
}
|
||||
|
||||
if (_globalAttemptWindow.Count >= configuration.MaximumConnectionAttempts)
|
||||
{
|
||||
rejectionReason = "global connection-attempt rate reached";
|
||||
return false;
|
||||
}
|
||||
|
||||
_globalAttemptWindow.Count++;
|
||||
}
|
||||
|
||||
var globalLimit = configuration.MaximumConnections;
|
||||
if (globalLimit > 0 && _admittedConnections.Count >= globalLimit)
|
||||
{
|
||||
rejectionReason = "global concurrent-connection limit reached";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (address == null)
|
||||
{
|
||||
_admittedConnections[connection] = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (++_attemptSweepSequence % 256 == 0)
|
||||
{
|
||||
foreach (var expired in _peerAttemptWindows
|
||||
@@ -310,6 +374,9 @@ public class EpServer : NetworkServer<EpConnection>, IResource
|
||||
|
||||
_admittedConnections.Remove(connection);
|
||||
|
||||
if (address == null)
|
||||
return;
|
||||
|
||||
if (!_peerConnectionCounts.TryGetValue(address, out var count))
|
||||
return;
|
||||
|
||||
|
||||
+42
-28
@@ -41,7 +41,7 @@ Esiur has implementations in C#, JavaScript, and Dart, making it highly versatil
|
||||
* Game Development: Enable multiplayer synchronization of game states and events, ensuring real-time feedback for interactive experiences.
|
||||
* Financial Services: Support real-time, distributed data sharing for trading platforms or monitoring systems where latency is critical.
|
||||
|
||||
Esiur’s robust, feature-rich approach allows developers to focus on building applications without worrying about the complexities of distributed systems, while its self-describing API and broad data type support enable flexible and scalable application design.
|
||||
Esiur’s robust, feature-rich approach allows developers to focus on building applications without worrying about the complexities of distributed systems, while its self-describing API and broad data type support enable flexible and scalable application design.
|
||||
|
||||
## Installation
|
||||
- Nuget
|
||||
@@ -68,42 +68,53 @@ Esiur for C# uses source generator feature of .Net framework to implement the ne
|
||||
>```
|
||||
|
||||
### Setting up the server
|
||||
Esiur resources are arrange by stores (IStore) and accessed in a similar *nix file system paths, each store is responsible for storing and retriving of it's resources from memory, files or database.
|
||||
Esiur resources are arranged into stores (`IStore`) and accessed using paths similar to a Unix file system. Each store is responsible for keeping its resources in memory, files, or a database.
|
||||
|
||||
In this example we're going to use the built-in MemoryStore, which keeps its resources in RAM.
|
||||
This example creates a Warehouse and uses the built-in `MemoryStore`, which keeps its resources in RAM.
|
||||
|
||||
```C#
|
||||
// Warehouse is the singleton instance that holds all stores and active resources.
|
||||
await Warehouse.Put("sys", new MemoryStore());
|
||||
var warehouse = new Warehouse();
|
||||
await warehouse.Put("sys", new MemoryStore());
|
||||
```
|
||||
|
||||
Now we can add our resource to the memory store using ***Warehouse.Put***
|
||||
Now we can add our resource to the memory store using `warehouse.Put`.
|
||||
|
||||
```C#
|
||||
await Warehouse.Put("sys/hello", new HelloResource());
|
||||
await warehouse.Put("sys/hello", new HelloResource());
|
||||
```
|
||||
|
||||
To distribute our resource using Esiur EP Protocol we need to add a DistributedServer
|
||||
Add an `EpServer` to expose the resource through the Esiur EP protocol. Anonymous access is enabled here only to make the development example easy to run; production applications should configure an authentication provider.
|
||||
|
||||
|
||||
```C#
|
||||
await Warehouse.Put("sys/server", new DistributedServer());
|
||||
await warehouse.Put("sys/server", new EpServer
|
||||
{
|
||||
AllowUnauthorizedAccess = true, // Development only.
|
||||
});
|
||||
```
|
||||
|
||||
Finally we call ***Warehouse.Open*** to initialize the system.
|
||||
Finally, open the Warehouse to initialize the system.
|
||||
|
||||
```C#
|
||||
await Warehouse.Open();
|
||||
await warehouse.Open();
|
||||
```
|
||||
|
||||
To sum up
|
||||
|
||||
>***Program.cs***
|
||||
>```C#
|
||||
> await Warehouse.Put("sys", new MemoryStore());
|
||||
> await Warehouse.Put("sys/hello", new HelloResource());
|
||||
> await Warehouse.Put("sys/server", new DistributedServer());
|
||||
> await Warehouse.Open();
|
||||
>using Esiur.Protocol;
|
||||
>using Esiur.Resource;
|
||||
>using Esiur.Stores;
|
||||
>
|
||||
>var warehouse = new Warehouse();
|
||||
>await warehouse.Put("sys", new MemoryStore());
|
||||
>await warehouse.Put("sys/hello", new HelloResource());
|
||||
>await warehouse.Put("sys/server", new EpServer
|
||||
>{
|
||||
> AllowUnauthorizedAccess = true, // Development only.
|
||||
>});
|
||||
>await warehouse.Open();
|
||||
>```
|
||||
|
||||
|
||||
@@ -111,7 +122,8 @@ To sum up
|
||||
To access our resource remotely, we need to use it's full path including the protocol, host and instance link.
|
||||
|
||||
```C#
|
||||
dynamic res = await Warehouse.Get<IResource>("EP://localhost/sys/hello");
|
||||
var warehouse = new Warehouse();
|
||||
dynamic res = await warehouse.Get<IResource>("ep://localhost/sys/hello");
|
||||
```
|
||||
|
||||
Now we can invoke the exported functions and read/write properties;
|
||||
@@ -126,14 +138,15 @@ Summing up
|
||||
|
||||
>***Program.cs***
|
||||
>```C#
|
||||
> using Esiur.Resource;
|
||||
>
|
||||
> dynamic res = await Warehouse.Get<IResource>("EP://localhost/sys/hello");
|
||||
>
|
||||
> var reply = await res.SayHi("Hi, I'm calling you from dotnet");
|
||||
>
|
||||
> Console.WriteLine(reply);
|
||||
> Console.WriteLine($"Number of people said hi {res.Counts}");
|
||||
>using Esiur.Resource;
|
||||
>
|
||||
>var warehouse = new Warehouse();
|
||||
>dynamic res = await warehouse.Get<IResource>("ep://localhost/sys/hello");
|
||||
>
|
||||
>var reply = await res.SayHi("Hi, I'm calling you from dotnet");
|
||||
>
|
||||
>Console.WriteLine(reply);
|
||||
>Console.WriteLine($"Number of people said hi {res.Counts}");
|
||||
>
|
||||
>```
|
||||
|
||||
@@ -143,7 +156,7 @@ In the above client example, we relied on Esiur support for dynamic objects, but
|
||||
|
||||
Esiur has a self describing feature which comes with every language it supports, allowing the developer to fetch and generate classes that match the ones on the other side (i.e. server).
|
||||
|
||||
After installing the Esiur nuget package a new command is added to Visual Studio Package Console Manager that is called ***Get-Types***, which generates client side classes for robust static typing.
|
||||
After installing the Esiur NuGet package, a new command named ***Get-Types*** is added to the Visual Studio Package Manager Console. It generates client-side classes for robust static typing.
|
||||
|
||||
```ps
|
||||
Get-Types ep://localhost/sys/hello
|
||||
@@ -153,8 +166,9 @@ This will generate and add wrappers for all types needed by our resource.
|
||||
|
||||
Allowing us to use
|
||||
```C#
|
||||
var res = await Warehouse.Get<MyResource>("EP://localhost/sys/hello");
|
||||
var reply = await res.SayHi("Static typing is better");
|
||||
Console.WriteLine(reply);
|
||||
var warehouse = new Warehouse();
|
||||
var res = await warehouse.Get<MyResource>("ep://localhost/sys/hello");
|
||||
var reply = await res.SayHi("Static typing is better");
|
||||
Console.WriteLine(reply);
|
||||
```
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ using System.Data;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
@@ -89,7 +90,8 @@ public class Warehouse
|
||||
KeyList<string, KeyList<TypeDefKind, KeyList<string, Type>>> _proxyTypeDefs = new();
|
||||
|
||||
|
||||
Map<string, IAuthenticationProvider> _authenticationProviders = new Map<string, IAuthenticationProvider>();
|
||||
readonly ConcurrentDictionary<string, IAuthenticationProvider> _authenticationProviders
|
||||
= new ConcurrentDictionary<string, IAuthenticationProvider>(StringComparer.Ordinal);
|
||||
readonly ConcurrentDictionary<string, IEncryptionProvider> _encryptionProviders
|
||||
= new ConcurrentDictionary<string, IEncryptionProvider>(StringComparer.Ordinal);
|
||||
readonly ConcurrentDictionary<Type, IResourceManager> _resourceManagers
|
||||
@@ -102,7 +104,10 @@ public class Warehouse
|
||||
|
||||
object _typeDefsLock = new object();
|
||||
|
||||
bool _warehouseIsOpen = false;
|
||||
readonly object _warehouseLifecycleLock = new object();
|
||||
readonly SemaphoreSlim _warehouseLifecycleGate = new SemaphoreSlim(1, 1);
|
||||
volatile bool _warehouseIsOpen = false;
|
||||
bool _warehouseRequiresTermination = false;
|
||||
|
||||
public delegate void StoreEvent(IStore store);
|
||||
public event StoreEvent StoreConnected;
|
||||
@@ -117,19 +122,46 @@ public class Warehouse
|
||||
/// </summary>
|
||||
public WarehouseConfiguration Configuration { get; }
|
||||
|
||||
/// <summary>Indicates whether this Warehouse is currently open.</summary>
|
||||
public bool IsOpen
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_warehouseLifecycleLock)
|
||||
return _warehouseIsOpen;
|
||||
}
|
||||
}
|
||||
|
||||
private Regex urlRegex = new Regex(@"^(?:([\S]*)://([^/]*)/?)");
|
||||
|
||||
|
||||
public void RegisterAuthenticationProvider(IAuthenticationProvider provider)
|
||||
{
|
||||
if (provider == null)
|
||||
throw new ArgumentNullException(nameof(provider));
|
||||
|
||||
RegisterAuthenticationProvider(provider.DefaultName, provider);
|
||||
}
|
||||
|
||||
public void RegisterAuthenticationProvider(string name, IAuthenticationProvider provider)
|
||||
{
|
||||
_authenticationProviders.Add(name, provider);
|
||||
if (string.IsNullOrWhiteSpace(name)
|
||||
|| !string.Equals(name, name.Trim(), StringComparison.Ordinal))
|
||||
throw new ArgumentException("An authentication provider name is required.", nameof(name));
|
||||
if (provider == null)
|
||||
throw new ArgumentNullException(nameof(provider));
|
||||
if (!_authenticationProviders.TryAdd(name, provider))
|
||||
throw new InvalidOperationException(
|
||||
$"An authentication provider named `{name}` is already registered.");
|
||||
}
|
||||
|
||||
/// <summary>Removes an authentication provider only when the same instance is registered.</summary>
|
||||
public bool UnregisterAuthenticationProvider(string name, IAuthenticationProvider provider)
|
||||
=> !string.IsNullOrWhiteSpace(name)
|
||||
&& provider != null
|
||||
&& ((ICollection<KeyValuePair<string, IAuthenticationProvider>>)_authenticationProviders)
|
||||
.Remove(new KeyValuePair<string, IAuthenticationProvider>(name, provider));
|
||||
|
||||
/// <summary>
|
||||
/// Registers an encryption provider using its default protocol name.
|
||||
/// </summary>
|
||||
@@ -146,7 +178,8 @@ public class Warehouse
|
||||
/// </summary>
|
||||
public void RegisterEncryptionProvider(string name, IEncryptionProvider provider)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
if (string.IsNullOrWhiteSpace(name)
|
||||
|| !string.Equals(name, name.Trim(), StringComparison.Ordinal))
|
||||
throw new ArgumentException("An encryption provider name is required.", nameof(name));
|
||||
if (provider == null)
|
||||
throw new ArgumentNullException(nameof(provider));
|
||||
@@ -154,6 +187,13 @@ public class Warehouse
|
||||
throw new InvalidOperationException($"An encryption provider named `{name}` is already registered.");
|
||||
}
|
||||
|
||||
/// <summary>Removes an encryption provider only when the same instance is registered.</summary>
|
||||
public bool UnregisterEncryptionProvider(string name, IEncryptionProvider provider)
|
||||
=> !string.IsNullOrWhiteSpace(name)
|
||||
&& provider != null
|
||||
&& ((ICollection<KeyValuePair<string, IEncryptionProvider>>)_encryptionProviders)
|
||||
.Remove(new KeyValuePair<string, IEncryptionProvider>(name, provider));
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Registers a manager instance by its concrete type. Type attributes and
|
||||
@@ -560,15 +600,16 @@ public class Warehouse
|
||||
|
||||
public IAuthenticationProvider GetAuthenticationProvider(string name)
|
||||
{
|
||||
if (_authenticationProviders.ContainsKey(name))
|
||||
return _authenticationProviders[name];
|
||||
if (_authenticationProviders.TryGetValue(name, out var provider))
|
||||
return provider;
|
||||
throw new Exception("Authentication provider not found.");
|
||||
}
|
||||
|
||||
public IAuthenticationProvider? TryGetAuthenticationProvider(string name)
|
||||
{
|
||||
if (_authenticationProviders.ContainsKey(name))
|
||||
return _authenticationProviders[name];
|
||||
if (!string.IsNullOrWhiteSpace(name)
|
||||
&& _authenticationProviders.TryGetValue(name, out var provider))
|
||||
return provider;
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -689,52 +730,70 @@ public class Warehouse
|
||||
/// <returns>True, if no problem occurred.</returns>
|
||||
public async AsyncReply<bool> Open()
|
||||
{
|
||||
if (_warehouseIsOpen)
|
||||
return false;
|
||||
|
||||
// Load generated models
|
||||
//LoadGenerated();
|
||||
|
||||
|
||||
_warehouseIsOpen = true;
|
||||
|
||||
var resSnap = _resources.Select(x =>
|
||||
await _warehouseLifecycleGate.WaitAsync();
|
||||
try
|
||||
{
|
||||
IResource r;
|
||||
if (x.Value.TryGetTarget(out r))
|
||||
return r;
|
||||
else
|
||||
return null;
|
||||
}).Where(r => r != null).ToArray();
|
||||
|
||||
foreach (var r in resSnap)
|
||||
{
|
||||
//IResource r;
|
||||
//if (rk.Value.TryGetTarget(out r))
|
||||
//{
|
||||
var rt = await r.Handle(ResourceOperation.Initialize);
|
||||
//if (!rt)
|
||||
// return false;
|
||||
|
||||
if (!rt)
|
||||
lock (_warehouseLifecycleLock)
|
||||
{
|
||||
Global.Log("Warehouse", LogType.Warning, $"Resource failed at Initialize {r.Instance.Name} [{r.Instance.Definition.Name}]");
|
||||
if (_warehouseIsOpen || _warehouseRequiresTermination)
|
||||
return false;
|
||||
|
||||
_warehouseIsOpen = true;
|
||||
_warehouseRequiresTermination = true;
|
||||
}
|
||||
//}
|
||||
}
|
||||
|
||||
foreach (var r in resSnap)
|
||||
{
|
||||
var rt = await r.Handle(ResourceOperation.SystemReady);
|
||||
if (!rt)
|
||||
try
|
||||
{
|
||||
Global.Log("Warehouse", LogType.Warning, $"Resource failed at SystemInitialized {r.Instance.Name} [{r.Instance.Definition.Name}]");
|
||||
// Load generated models
|
||||
//LoadGenerated();
|
||||
|
||||
var resSnap = _resources.Select(x =>
|
||||
{
|
||||
IResource r;
|
||||
if (x.Value.TryGetTarget(out r))
|
||||
return r;
|
||||
else
|
||||
return null;
|
||||
}).Where(r => r != null).ToArray();
|
||||
|
||||
foreach (var r in resSnap)
|
||||
{
|
||||
//IResource r;
|
||||
//if (rk.Value.TryGetTarget(out r))
|
||||
//{
|
||||
var rt = await r.Handle(ResourceOperation.Initialize);
|
||||
//if (!rt)
|
||||
// return false;
|
||||
|
||||
if (!rt)
|
||||
{
|
||||
Global.Log("Warehouse", LogType.Warning, $"Resource failed at Initialize {r.Instance.Name} [{r.Instance.Definition.Name}]");
|
||||
}
|
||||
//}
|
||||
}
|
||||
|
||||
foreach (var r in resSnap)
|
||||
{
|
||||
var rt = await r.Handle(ResourceOperation.SystemReady);
|
||||
if (!rt)
|
||||
{
|
||||
Global.Log("Warehouse", LogType.Warning, $"Resource failed at SystemInitialized {r.Instance.Name} [{r.Instance.Definition.Name}]");
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
lock (_warehouseLifecycleLock)
|
||||
_warehouseIsOpen = false;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
|
||||
finally
|
||||
{
|
||||
_warehouseLifecycleGate.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -742,56 +801,109 @@ public class Warehouse
|
||||
/// This function issues terminate trigger to all resources and stores.
|
||||
/// </summary>
|
||||
/// <returns>True, if no problem occurred.</returns>
|
||||
public AsyncReply<bool> Close()
|
||||
public async AsyncReply<bool> Close()
|
||||
{
|
||||
await _warehouseLifecycleGate.WaitAsync();
|
||||
try
|
||||
{
|
||||
lock (_warehouseLifecycleLock)
|
||||
{
|
||||
if (!_warehouseRequiresTermination)
|
||||
return false;
|
||||
|
||||
var bag = new AsyncBag<bool>();
|
||||
// Resources added after this point must not initialize while the
|
||||
// existing graph is terminating.
|
||||
_warehouseIsOpen = false;
|
||||
}
|
||||
|
||||
// Use the same stable resource graph for both shutdown phases. Stores are
|
||||
// also retained by _stores, so include that registry in case its weak
|
||||
// _resources entry was concurrently removed or became unavailable.
|
||||
var resources = SnapshotResources();
|
||||
|
||||
// Every Terminate callback must settle before SystemTerminated starts.
|
||||
// Each phase captures failures per resource so one synchronous throw or
|
||||
// failed reply cannot prevent the remaining callbacks from running.
|
||||
var terminate = await SettleOperation(resources, ResourceOperation.Terminate);
|
||||
var systemTerminated = await SettleOperation(resources, ResourceOperation.SystemTerminated);
|
||||
|
||||
var errors = terminate.Errors.Concat(systemTerminated.Errors).ToArray();
|
||||
if (errors.Length == 1)
|
||||
throw errors[0];
|
||||
if (errors.Length > 1)
|
||||
throw new AggregateException(
|
||||
"One or more resources failed while closing the warehouse.",
|
||||
errors);
|
||||
|
||||
return terminate.Success && systemTerminated.Success;
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (_warehouseLifecycleLock)
|
||||
{
|
||||
_warehouseIsOpen = false;
|
||||
_warehouseRequiresTermination = false;
|
||||
}
|
||||
|
||||
_warehouseLifecycleGate.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private IResource[] SnapshotResources()
|
||||
{
|
||||
var resources = new HashSet<IResource>(ResourceReferenceComparer.Instance);
|
||||
|
||||
foreach (var resource in _resources.Values)
|
||||
{
|
||||
IResource r;
|
||||
if (resource.TryGetTarget(out r))
|
||||
{
|
||||
if (!(r is IStore))
|
||||
bag.Add(r.Handle(ResourceOperation.Terminate));
|
||||
if (resource.TryGetTarget(out var target))
|
||||
resources.Add(target);
|
||||
|
||||
}
|
||||
foreach (var store in _stores.Keys)
|
||||
resources.Add(store);
|
||||
|
||||
return resources.ToArray();
|
||||
}
|
||||
|
||||
private static async Task<(bool Success, Exception[] Errors)> SettleOperation(
|
||||
IResource[] resources,
|
||||
ResourceOperation operation)
|
||||
{
|
||||
var pending = resources
|
||||
.Select(resource => SettleResourceOperation(resource, operation))
|
||||
.ToArray();
|
||||
var outcomes = await Task.WhenAll(pending);
|
||||
|
||||
return (
|
||||
outcomes.All(outcome => outcome.Success),
|
||||
outcomes
|
||||
.Where(outcome => outcome.Error != null)
|
||||
.Select(outcome => outcome.Error!)
|
||||
.ToArray());
|
||||
}
|
||||
|
||||
private static async Task<(bool Success, Exception? Error)> SettleResourceOperation(
|
||||
IResource resource,
|
||||
ResourceOperation operation)
|
||||
{
|
||||
try
|
||||
{
|
||||
var reply = resource.Handle(operation)
|
||||
?? throw new InvalidOperationException(
|
||||
$"Resource `{resource.GetType().FullName}` returned no reply for `{operation}`.");
|
||||
return (await reply, null);
|
||||
}
|
||||
|
||||
foreach (var store in _stores)
|
||||
bag.Add(store.Key.Handle(ResourceOperation.Terminate));
|
||||
|
||||
|
||||
foreach (var resource in _resources.Values)
|
||||
catch (Exception exception)
|
||||
{
|
||||
IResource r;
|
||||
if (resource.TryGetTarget(out r))
|
||||
{
|
||||
if (!(r is IStore))
|
||||
bag.Add(r.Handle(ResourceOperation.SystemTerminated));
|
||||
}
|
||||
return (false, exception);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ResourceReferenceComparer : IEqualityComparer<IResource>
|
||||
{
|
||||
internal static readonly ResourceReferenceComparer Instance = new ResourceReferenceComparer();
|
||||
|
||||
foreach (var store in _stores)
|
||||
bag.Add(store.Key.Handle(ResourceOperation.SystemTerminated));
|
||||
public bool Equals(IResource x, IResource y) => ReferenceEquals(x, y);
|
||||
|
||||
bag.Seal();
|
||||
|
||||
var rt = new AsyncReply<bool>();
|
||||
bag.Then((x) =>
|
||||
{
|
||||
foreach (var b in x)
|
||||
if (!b)
|
||||
{
|
||||
rt.Trigger(false);
|
||||
return;
|
||||
}
|
||||
|
||||
rt.Trigger(true);
|
||||
});
|
||||
|
||||
return rt;
|
||||
public int GetHashCode(IResource resource) => RuntimeHelpers.GetHashCode(resource);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -65,8 +65,18 @@ public sealed class ResourceAttachmentConfiguration
|
||||
/// </summary>
|
||||
public sealed class ConnectionConfiguration
|
||||
{
|
||||
/// <summary>Maximum concurrent connections admitted by one EP server.</summary>
|
||||
public int MaximumConnections { get; set; } = 1_024;
|
||||
|
||||
public int MaximumConnectionsPerIpAddress { get; set; } = 64;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum connection attempts admitted by one EP server during
|
||||
/// <see cref="ConnectionAttemptWindow"/>. This also bounds clients that rotate IP
|
||||
/// addresses. A value of zero disables the limit.
|
||||
/// </summary>
|
||||
public int MaximumConnectionAttempts { get; set; } = 4_096;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum connection attempts admitted from one IP during
|
||||
/// <see cref="ConnectionAttemptWindow"/>. This limits repeated pre-authentication
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Esiur.Core;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace Esiur.Security.Authority.Providers
|
||||
@@ -12,15 +13,41 @@ namespace Esiur.Security.Authority.Providers
|
||||
/// </summary>
|
||||
public const string ProtocolName = "password-sha3-v1";
|
||||
|
||||
/// <summary>
|
||||
/// Previous protocol name, retained only to make explicit migration aliases possible.
|
||||
/// New connections should use <see cref="ProtocolName"/>.
|
||||
/// </summary>
|
||||
[Obsolete("Use ProtocolName (`password-sha3-v1`) for new connections.")]
|
||||
public const string LegacyProtocolName = "hash";
|
||||
|
||||
public string DefaultName => ProtocolName;
|
||||
|
||||
/// <summary>
|
||||
/// Creates the salted credential stored by a server for the
|
||||
/// <c>password-sha3-v1</c> protocol. Call this once during account enrollment,
|
||||
/// persist the returned hash and salt, and discard the plaintext password.
|
||||
/// The hash is a password-equivalent protocol verifier and must be protected
|
||||
/// from disclosure, logs, and client-visible data.
|
||||
/// </summary>
|
||||
public static PasswordHash CreateCredential(byte[] password)
|
||||
{
|
||||
if (password == null)
|
||||
throw new ArgumentNullException(nameof(password));
|
||||
if (password.Length == 0)
|
||||
throw new ArgumentException("A password cannot be empty.", nameof(password));
|
||||
|
||||
var salt = new byte[32];
|
||||
using (var random = RandomNumberGenerator.Create())
|
||||
random.GetBytes(salt);
|
||||
|
||||
var material = new byte[password.Length + salt.Length];
|
||||
try
|
||||
{
|
||||
Buffer.BlockCopy(password, 0, material, 0, password.Length);
|
||||
Buffer.BlockCopy(salt, 0, material, password.Length, salt.Length);
|
||||
return new PasswordHash(
|
||||
PasswordAuthenticationHandler.ComputeSha3(material),
|
||||
salt);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Array.Clear(material, 0, material.Length);
|
||||
}
|
||||
}
|
||||
|
||||
public IAuthenticationHandler CreateAuthenticationHandler(AuthenticationContext context)
|
||||
{
|
||||
var authHandler = new PasswordAuthenticationHandler(context.Mode,
|
||||
|
||||
Reference in New Issue
Block a user