Syncshells (#11)
* some groups stuff * further groups rework * fixes for pause changes * adjsut timeout interval * fixes and namespace change to file scoped * more fixes * further implement groups * fix change group ownership * add some more stuff for groups * more fixes and additions * some fixes based on analyzers, add shard info to ui * add discord command, cleanup * fix regex * add group migration and deletion on user deletion * add api method for client to check health of connection * adjust regex for vanity * fixes for server and bot * fixes some string comparison in linq queries * fixes group leave and sets alias to null * fix syntax in changeownership * add better logging, fixes for group leaving * fixes for group leave Co-authored-by: Stanley Dimant <root.darkarchon@outlook.com>
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace MareSynchronosServer.Utils;
|
||||
|
||||
public class IdBasedUserIdProvider : IUserIdProvider
|
||||
{
|
||||
public string GetUserId(HubConnectionContext context)
|
||||
{
|
||||
return context.User!.Claims.SingleOrDefault(c => string.Equals(c.Type, ClaimTypes.NameIdentifier, System.StringComparison.Ordinal))?.Value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using MareSynchronosServer.Hubs;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MareSynchronosServer.Utils;
|
||||
|
||||
public class MareHubLogger
|
||||
{
|
||||
private readonly MareHub _hub;
|
||||
private readonly ILogger<MareHub> _logger;
|
||||
|
||||
public MareHubLogger(MareHub hub, ILogger<MareHub> logger)
|
||||
{
|
||||
_hub = hub;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void LogCallInfo(string methodName, params object[] args)
|
||||
{
|
||||
string formattedArgs = args.Length != 0 ? "|" + string.Join(":", args) : string.Empty;
|
||||
_logger.LogInformation("{uid}:{method}{args}", _hub.AuthenticatedUserId, methodName, formattedArgs);
|
||||
}
|
||||
|
||||
public void LogCallWarning(string methodName, params object[] args)
|
||||
{
|
||||
string formattedArgs = args.Length != 0 ? "|" + string.Join(":", args) : string.Empty;
|
||||
_logger.LogWarning("{uid}:{method}{args}", _hub.AuthenticatedUserId, methodName, formattedArgs);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace MareSynchronosServer.Utils;
|
||||
|
||||
public enum PauseInfo
|
||||
{
|
||||
NoConnection,
|
||||
Paused,
|
||||
Unpaused
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace MareSynchronosServer.Utils;
|
||||
|
||||
public record PauseState
|
||||
{
|
||||
public string GID { get; set; }
|
||||
public bool IsPaused { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace MareSynchronosServer.Utils;
|
||||
|
||||
public record PausedEntry
|
||||
{
|
||||
public string UID { get; set; }
|
||||
public List<PauseState> PauseStates { get; set; } = new();
|
||||
|
||||
public PauseInfo IsDirectlyPaused => PauseStateWithoutGroups == null ? PauseInfo.NoConnection
|
||||
: PauseStates.First(g => g.GID == null).IsPaused ? PauseInfo.Paused : PauseInfo.Unpaused;
|
||||
|
||||
public PauseInfo IsPausedPerGroup => !PauseStatesWithoutDirect.Any() ? PauseInfo.NoConnection
|
||||
: PauseStatesWithoutDirect.All(p => p.IsPaused) ? PauseInfo.Paused : PauseInfo.Unpaused;
|
||||
|
||||
private IEnumerable<PauseState> PauseStatesWithoutDirect => PauseStates.Where(f => f.GID != null);
|
||||
private PauseState PauseStateWithoutGroups => PauseStates.SingleOrDefault(p => p.GID == null);
|
||||
|
||||
public bool IsPaused
|
||||
{
|
||||
get
|
||||
{
|
||||
var isDirectlyPaused = IsDirectlyPaused;
|
||||
bool result;
|
||||
if (isDirectlyPaused != PauseInfo.NoConnection)
|
||||
{
|
||||
result = isDirectlyPaused == PauseInfo.Paused;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = IsPausedPerGroup == PauseInfo.Paused;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public PauseInfo IsPausedForSpecificGroup(string gid)
|
||||
{
|
||||
var state = PauseStatesWithoutDirect.SingleOrDefault(g => string.Equals(g.GID, gid, StringComparison.Ordinal));
|
||||
if (state == null) return PauseInfo.NoConnection;
|
||||
return state.IsPaused ? PauseInfo.Paused : PauseInfo.NoConnection;
|
||||
}
|
||||
|
||||
public PauseInfo IsPausedExcludingGroup(string gid)
|
||||
{
|
||||
var states = PauseStatesWithoutDirect.Where(f => !string.Equals(f.GID, gid, StringComparison.Ordinal)).ToList();
|
||||
if (!states.Any()) return PauseInfo.NoConnection;
|
||||
var result = states.All(p => p.IsPaused);
|
||||
if (result) return PauseInfo.Paused;
|
||||
return PauseInfo.Unpaused;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AspNetCoreRateLimit;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace MareSynchronosServer.Utils;
|
||||
public class SignalRLimitFilter : IHubFilter
|
||||
{
|
||||
private readonly IRateLimitProcessor _processor;
|
||||
private readonly IHttpContextAccessor accessor;
|
||||
private readonly ILogger<SignalRLimitFilter> logger;
|
||||
private static readonly SemaphoreSlim ConnectionLimiterSemaphore = new(10);
|
||||
private static readonly SemaphoreSlim DisconnectLimiterSemaphore = new(10);
|
||||
|
||||
public SignalRLimitFilter(
|
||||
IOptions<IpRateLimitOptions> options, IProcessingStrategy processing, IIpPolicyStore policyStore, IHttpContextAccessor accessor, ILogger<SignalRLimitFilter> logger)
|
||||
{
|
||||
_processor = new IpRateLimitProcessor(options?.Value, policyStore, processing);
|
||||
this.accessor = accessor;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public async ValueTask<object> InvokeMethodAsync(
|
||||
HubInvocationContext invocationContext, Func<HubInvocationContext, ValueTask<object>> next)
|
||||
{
|
||||
var ip = accessor.GetIpAddress();
|
||||
var client = new ClientRequestIdentity
|
||||
{
|
||||
ClientIp = ip,
|
||||
Path = invocationContext.HubMethodName,
|
||||
HttpVerb = "ws",
|
||||
ClientId = invocationContext.Context.UserIdentifier
|
||||
};
|
||||
foreach (var rule in await _processor.GetMatchingRulesAsync(client).ConfigureAwait(false))
|
||||
{
|
||||
var counter = await _processor.ProcessRequestAsync(client, rule).ConfigureAwait(false);
|
||||
if (counter.Count > rule.Limit)
|
||||
{
|
||||
var authUserId = invocationContext.Context.User.Claims?.SingleOrDefault(c => string.Equals(c.Type, ClaimTypes.NameIdentifier, StringComparison.Ordinal))?.Value ?? "Unknown";
|
||||
var retry = counter.Timestamp.RetryAfterFrom(rule);
|
||||
logger.LogWarning("Method rate limit triggered from {ip}/{authUserId}: {method}", ip, authUserId, invocationContext.HubMethodName);
|
||||
throw new HubException($"call limit {retry}");
|
||||
}
|
||||
}
|
||||
|
||||
return await next(invocationContext).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Optional method
|
||||
public async Task OnConnectedAsync(HubLifetimeContext context, Func<HubLifetimeContext, Task> next)
|
||||
{
|
||||
await ConnectionLimiterSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var ip = accessor.GetIpAddress();
|
||||
var client = new ClientRequestIdentity
|
||||
{
|
||||
ClientIp = ip,
|
||||
Path = "Connect",
|
||||
HttpVerb = "ws",
|
||||
};
|
||||
foreach (var rule in await _processor.GetMatchingRulesAsync(client).ConfigureAwait(false))
|
||||
{
|
||||
var counter = await _processor.ProcessRequestAsync(client, rule).ConfigureAwait(false);
|
||||
if (counter.Count > rule.Limit)
|
||||
{
|
||||
var retry = counter.Timestamp.RetryAfterFrom(rule);
|
||||
logger.LogWarning("Connection rate limit triggered from {ip}", ip);
|
||||
ConnectionLimiterSemaphore.Release();
|
||||
throw new HubException($"Connection rate limit {retry}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
await Task.Delay(25).ConfigureAwait(false);
|
||||
await next(context).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Error on OnConnectedAsync");
|
||||
}
|
||||
finally
|
||||
{
|
||||
ConnectionLimiterSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task OnDisconnectedAsync(
|
||||
HubLifetimeContext context, Exception exception, Func<HubLifetimeContext, Exception, Task> next)
|
||||
{
|
||||
await DisconnectLimiterSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
if (exception != null)
|
||||
{
|
||||
logger.LogWarning(exception, "InitialException on OnDisconnectedAsync");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await next(context, exception).ConfigureAwait(false);
|
||||
await Task.Delay(25).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogWarning(e, "ThrownException on OnDisconnectedAsync");
|
||||
}
|
||||
finally
|
||||
{
|
||||
DisconnectLimiterSemaphore.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
12
MareSynchronosServer/MareSynchronosServer/Utils/UserPair.cs
Normal file
12
MareSynchronosServer/MareSynchronosServer/Utils/UserPair.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace MareSynchronosServer.Hubs;
|
||||
|
||||
public partial class MareHub
|
||||
{
|
||||
private record UserPair
|
||||
{
|
||||
public string UserUID { get; set; }
|
||||
public string OtherUserUID { get; set; }
|
||||
public bool UserPausedOther { get; set; }
|
||||
public bool OtherPausedUser { get; set; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user