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:
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user