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:
rootdarkarchon
2022-10-04 14:13:43 +02:00
committed by GitHub
parent d866223069
commit bff21ead95
57 changed files with 3761 additions and 1239 deletions

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,8 @@
namespace MareSynchronosServer.Utils;
public enum PauseInfo
{
NoConnection,
Paused,
Unpaused
}

View File

@@ -0,0 +1,7 @@
namespace MareSynchronosServer.Utils;
public record PauseState
{
public string GID { get; set; }
public bool IsPaused { get; set; }
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}
}

View 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; }
}
}